Compare commits

..
Author SHA1 Message Date
Bill He b26106ba20 Fix status empty bug 2023-11-09 14:07:46 -08:00
82 changed files with 1320 additions and 3297 deletions
+2 -4
View File
@@ -39,16 +39,14 @@
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^1.0.25",
"@dydxprotocol/v4-abacus": "^1.0.24",
"@dydxprotocol/v4-client-js": "^1.0.0",
"@dydxprotocol/v4-localization": "^1.0.5",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.1.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-radio-group": "^1.1.3",
@@ -111,7 +109,7 @@
"styled-components": "^5.3.11",
"use-latest": "^1.2.1",
"viem": "^1.12.2",
"wagmi": "^1.4.5"
"wagmi": "^1.3.7"
},
"devDependencies": {
"@babel/core": "^7.22.5",
+73 -114
View File
@@ -27,8 +27,8 @@ dependencies:
specifier: ^0.31.0
version: 0.31.0
'@dydxprotocol/v4-abacus':
specifier: ^1.0.25
version: 1.0.25
specifier: ^1.0.24
version: 1.0.24
'@dydxprotocol/v4-client-js':
specifier: ^1.0.0
version: 1.0.0
@@ -41,9 +41,6 @@ dependencies:
'@js-joda/core':
specifier: ^5.5.3
version: 5.5.3
'@radix-ui/react-checkbox':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-collapsible':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@@ -53,9 +50,6 @@ dependencies:
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.5
version: 2.0.5(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-icons':
specifier: ^1.3.0
version: 1.3.0(react@18.2.0)
'@radix-ui/react-navigation-menu':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@@ -243,8 +237,8 @@ dependencies:
specifier: ^1.12.2
version: 1.12.2(typescript@5.1.3)
wagmi:
specifier: ^1.4.5
version: 1.4.5(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
specifier: ^1.3.7
version: 1.3.7(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
devDependencies:
'@babel/core':
@@ -988,8 +982,8 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true
/@dydxprotocol/v4-abacus@1.0.25:
resolution: {integrity: sha512-DYueeM4MFeMLoucuzEU100oxM7mUYhlbyGuTkeE2gz8GX4hUZksH6osuh6fo5895cEAS2fmbOQbX+fNrai56jQ==}
/@dydxprotocol/v4-abacus@1.0.24:
resolution: {integrity: sha512-wDGSjkrc3Se6Ev7UTjPgJV7PiyzZSz2mJwOTZikLwH7W3k1iPYGQmaCd7TudFk8h2aSdlEwNsQBCf0sLoyvHaQ==}
dev: false
/@dydxprotocol/v4-client-js@1.0.0:
@@ -2187,34 +2181,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==}
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
dependencies:
'@babel/runtime': 7.22.10
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.14)(react@18.2.0)
'@types/react': 18.2.14
'@types/react-dom': 18.2.6
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.6)(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies:
@@ -2516,14 +2482,6 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-icons@1.3.0(react@18.2.0):
resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==}
peerDependencies:
react: ^16.x || ^17.x || ^18.x
dependencies:
react: 18.2.0
dev: false
/@radix-ui/react-id@1.0.0(react@18.2.0):
resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
peerDependencies:
@@ -5979,12 +5937,26 @@ packages:
pretty-format: 27.5.1
dev: true
/@wagmi/connectors@3.1.3(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-UgwsQKQDFObJVJMf9pDfFoXTv710o4zrTHyhIWKBTMMkLpCMsMxN5+ZaDhBYt/BgoRinfRYQo8uwuwLhxE6Log==}
/@wagmi/chains@1.4.0(typescript@5.1.3):
resolution: {integrity: sha512-9HwJrhcZ1TxyrCbE10y7s1eSiSiyfGam7AHIOLYExaOX+vpOZ+MNTt4orFEDbEpz1fxwJDPPI38lanAUix1OSA==}
peerDependencies:
typescript: '>=5.0.4'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
typescript: 5.1.3
dev: false
/@wagmi/connectors@2.6.5(@wagmi/chains@1.4.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-klF31togMDd0qQqEcLl5cCGxjMbL0RRXQ8I1vxmEa3KdGzw6Z3ICVzX7/bDfnNEZcOW7BKyAnZDq7rCt5jTOiw==}
peerDependencies:
'@wagmi/chains': '>=1.3.0'
typescript: '>=5.0.4'
viem: '>=0.3.35'
peerDependenciesMeta:
'@wagmi/chains':
optional: true
typescript:
optional: true
dependencies:
@@ -5992,17 +5964,17 @@ packages:
'@ledgerhq/connect-kit-loader': 1.1.2
'@safe-global/safe-apps-provider': 0.17.1(typescript@5.1.3)
'@safe-global/safe-apps-sdk': 8.1.0(typescript@5.1.3)
'@walletconnect/ethereum-provider': 2.10.2(@walletconnect/modal@2.6.2)
'@wagmi/chains': 1.4.0(typescript@5.1.3)
'@walletconnect/ethereum-provider': 2.8.6(@walletconnect/modal@2.5.9)
'@walletconnect/legacy-provider': 2.0.0
'@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/utils': 2.10.2
'@walletconnect/modal': 2.5.9(react@18.2.0)
'@walletconnect/utils': 2.8.6
abitype: 0.8.7(typescript@5.1.3)
eventemitter3: 4.0.7
typescript: 5.1.3
viem: 1.12.2(typescript@5.1.3)
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
- '@types/react'
- bufferutil
- encoding
- lokijs
@@ -6012,8 +5984,8 @@ packages:
- zod
dev: false
/@wagmi/core@1.4.5(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-N9luRb1Uk4tBN9kaYcQSWKE9AsRt/rvZaFt5IZech4JPzNN2sQlfhKd9GEjOXYRDqEPHdDvos7qyBKiDNTz4GA==}
/@wagmi/core@1.3.6(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-TXv9ZlRR5aySfERFuWMuo+lKXC/CoqtxVJZVHPqhK1jY+nldMx3AvrWzzF4CccRaMYcVdvPFepvmxzq2A2VvWg==}
peerDependencies:
typescript: '>=5.0.4'
viem: '>=0.3.35'
@@ -6021,7 +5993,8 @@ packages:
typescript:
optional: true
dependencies:
'@wagmi/connectors': 3.1.3(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
'@wagmi/chains': 1.4.0(typescript@5.1.3)
'@wagmi/connectors': 2.6.5(@wagmi/chains@1.4.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
abitype: 0.8.7(typescript@5.1.3)
eventemitter3: 4.0.7
typescript: 5.1.3
@@ -6066,8 +6039,8 @@ packages:
- utf-8-validate
dev: false
/@walletconnect/core@2.10.2:
resolution: {integrity: sha512-JQz/xp3SLEpTeRQctdck2ugSBVEpMxoSE+lFi2voJkZop1hv6P+uqr6E4PzjFluAjeAnKlT1xvra0aFWjPWVcw==}
/@walletconnect/core@2.8.6:
resolution: {integrity: sha512-rnSqm1KJLcww/v6+UH8JeibQkJ3EKgyUDPfEK0stSEkrIUIcXaFlq3Et8S+vgV8bPhI0MVUhAhFL5OJZ3t2ryg==}
dependencies:
'@walletconnect/heartbeat': 1.2.1
'@walletconnect/jsonrpc-provider': 1.0.13
@@ -6080,8 +6053,8 @@ packages:
'@walletconnect/relay-auth': 1.0.4
'@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.10.2
'@walletconnect/utils': 2.10.2
'@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.8.6
events: 3.3.0
lodash.isequal: 4.5.0
uint8arrays: 3.1.1
@@ -6117,8 +6090,8 @@ packages:
tslib: 1.14.1
dev: false
/@walletconnect/ethereum-provider@2.10.2(@walletconnect/modal@2.6.2):
resolution: {integrity: sha512-QMYFZ6+rVq2CJLdIPdKK0j1Qm66UA27oQU5V2SrL8EVwl7wFfm0Bq7fnL+qAWeDpn612dNeNErpk/ROa1zWlWg==}
/@walletconnect/ethereum-provider@2.8.6(@walletconnect/modal@2.5.9):
resolution: {integrity: sha512-wUvJEsXTLmMihrOhQxAs1k9hrWEOT03QBn54P9r9GpJbJ1zEfIjQaXFfi8uup6gldhH+vN38PsbOiLyv/6d3qQ==}
peerDependencies:
'@walletconnect/modal': '>=2'
peerDependenciesMeta:
@@ -6129,11 +6102,11 @@ packages:
'@walletconnect/jsonrpc-provider': 1.0.13
'@walletconnect/jsonrpc-types': 1.0.3
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/modal': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/sign-client': 2.10.2
'@walletconnect/types': 2.10.2
'@walletconnect/universal-provider': 2.10.2
'@walletconnect/utils': 2.10.2
'@walletconnect/modal': 2.5.9(react@18.2.0)
'@walletconnect/sign-client': 2.8.6
'@walletconnect/types': 2.8.6
'@walletconnect/universal-provider': 2.8.6
'@walletconnect/utils': 2.8.6
events: 3.3.0
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
@@ -6284,34 +6257,32 @@ packages:
tslib: 1.14.1
dev: false
/@walletconnect/modal-core@2.6.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-cv8ibvdOJQv2B+nyxP9IIFdxvQznMz8OOr/oR/AaUZym4hjXNL/l1a2UlSQBXrVjo3xxbouMxLb3kBsHoYP2CA==}
/@walletconnect/modal-core@2.5.9(react@18.2.0):
resolution: {integrity: sha512-isIebwF9hOknGouhS/Ob4YJ9Sa/tqNYG2v6Ua9EkCqIoLimepkG5eC53tslUWW29SLSfQ9qqBNG2+iE7yQXqgw==}
dependencies:
valtio: 1.11.2(@types/react@18.2.14)(react@18.2.0)
buffer: 6.0.3
valtio: 1.10.6(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- react
dev: false
/@walletconnect/modal-ui@2.6.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-rbdstM1HPGvr7jprQkyPggX7rP4XiCG85ZA+zWBEX0dVQg8PpAgRUqpeub4xQKDgY7pY/xLRXSiCVdWGqvG2HA==}
/@walletconnect/modal-ui@2.5.9(react@18.2.0):
resolution: {integrity: sha512-nfBaAT9Ls7RZTBBgAq+Nt/3AoUcinIJ9bcq5UHXTV3lOPu/qCKmUC/0HY3GvUK8ykabUAsjr0OAGmcqkB91qug==}
dependencies:
'@walletconnect/modal-core': 2.6.2(@types/react@18.2.14)(react@18.2.0)
lit: 2.8.0
'@walletconnect/modal-core': 2.5.9(react@18.2.0)
lit: 2.7.5
motion: 10.16.2
qrcode: 1.5.3
transitivePeerDependencies:
- '@types/react'
- react
dev: false
/@walletconnect/modal@2.6.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-eFopgKi8AjKf/0U4SemvcYw9zlLpx9njVN8sf6DAkowC2Md0gPU/UNEbH1Wwj407pEKnEds98pKWib1NN1ACoA==}
/@walletconnect/modal@2.5.9(react@18.2.0):
resolution: {integrity: sha512-Zs2RvPwbBNRdBhb50FuJCxi3FJltt1KSpI7odjU/x9GTpTOcSOkmR66PBCy2JvNA0+ztnS1Xs0LVEr3lu7/Jzw==}
dependencies:
'@walletconnect/modal-core': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/modal-ui': 2.6.2(@types/react@18.2.14)(react@18.2.0)
'@walletconnect/modal-core': 2.5.9(react@18.2.0)
'@walletconnect/modal-ui': 2.5.9(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- react
dev: false
@@ -6367,17 +6338,17 @@ packages:
- utf-8-validate
dev: false
/@walletconnect/sign-client@2.10.2:
resolution: {integrity: sha512-vviSLV3f92I0bReX+OLr1HmbH0uIzYEQQFd1MzIfDk9PkfFT/LLAHhUnDaIAMkIdippqDcJia+5QEtT4JihL3Q==}
/@walletconnect/sign-client@2.8.6:
resolution: {integrity: sha512-rOFTKTHP7oJfXgYHX7+SdB8VbcsEE3ZFG/bMdmZboWaBim1mrY3vUyDdKrNr0VgI3AwBiEQezQDfKxBX0pMSQQ==}
dependencies:
'@walletconnect/core': 2.10.2
'@walletconnect/core': 2.8.6
'@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.1
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/logger': 2.0.1
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.10.2
'@walletconnect/utils': 2.10.2
'@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.8.6
events: 3.3.0
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
@@ -6406,8 +6377,8 @@ packages:
- lokijs
dev: false
/@walletconnect/types@2.10.2:
resolution: {integrity: sha512-luNV+07Wdla4STi9AejseCQY31tzWKQ5a7C3zZZaRK/di+rFaAAb7YW04OP4klE7tw/mJRGPTlekZElmHxO8kQ==}
/@walletconnect/types@2.8.6:
resolution: {integrity: sha512-Z/PFa3W1XdxeTcCtdR6lUsFgZfU/69wWJBPyclPwn7cu1+eriuCr6XZXQpJjib3flU+HnwHiXeUuqZaheehPxw==}
dependencies:
'@walletconnect/events': 1.0.1
'@walletconnect/heartbeat': 1.2.1
@@ -6420,17 +6391,17 @@ packages:
- lokijs
dev: false
/@walletconnect/universal-provider@2.10.2:
resolution: {integrity: sha512-wFgI0LbQ3D56sgaUMsgOHCM5m8WLxiC71BGuCKQfApgsbNMVKugYVy2zWHyUyi8sqTQHI+uSaVpDev4UHq9LEw==}
/@walletconnect/universal-provider@2.8.6:
resolution: {integrity: sha512-ln1RVv8+oHu9enOJ/oVkjiarneB+4vJCk16znOklIN2JtDHwB8iObDHlQH3UE6ynNTw1iRvaGuPR4g+YdIfB6w==}
dependencies:
'@walletconnect/jsonrpc-http-connection': 1.0.7
'@walletconnect/jsonrpc-provider': 1.0.13
'@walletconnect/jsonrpc-types': 1.0.3
'@walletconnect/jsonrpc-utils': 1.0.8
'@walletconnect/logger': 2.0.1
'@walletconnect/sign-client': 2.10.2
'@walletconnect/types': 2.10.2
'@walletconnect/utils': 2.10.2
'@walletconnect/sign-client': 2.8.6
'@walletconnect/types': 2.8.6
'@walletconnect/utils': 2.8.6
events: 3.3.0
transitivePeerDependencies:
- '@react-native-async-storage/async-storage'
@@ -6462,8 +6433,8 @@ packages:
- lokijs
dev: false
/@walletconnect/utils@2.10.2:
resolution: {integrity: sha512-syxXRpc2yhSknMu3IfiBGobxOY7fLfLTJuw+ppKaeO6WUdZpIit3wfuGOcc0Ms3ZPFCrGfyGOoZsCvgdXtptRg==}
/@walletconnect/utils@2.8.6:
resolution: {integrity: sha512-wcy6e5+COYo7tfNnW8YqidnATdJDIW6vDiWWE7A1F78Sl/VflkaevB9cIgyn8eLdxC1SxXgGoeC2oLP90nnHJg==}
dependencies:
'@stablelib/chacha20poly1305': 1.0.1
'@stablelib/hkdf': 1.0.1
@@ -6473,7 +6444,7 @@ packages:
'@walletconnect/relay-api': 1.0.9
'@walletconnect/safe-json': 1.0.2
'@walletconnect/time': 1.0.2
'@walletconnect/types': 2.10.2
'@walletconnect/types': 2.8.6
'@walletconnect/window-getters': 1.0.1
'@walletconnect/window-metadata': 1.0.1
detect-browser: 5.3.0
@@ -10402,14 +10373,6 @@ packages:
lit-html: 2.8.0
dev: false
/lit@2.8.0:
resolution: {integrity: sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==}
dependencies:
'@lit/reactive-element': 1.6.3
lit-element: 3.3.3
lit-html: 2.8.0
dev: false
/loader-runner@4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'}
@@ -13543,19 +13506,15 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/valtio@1.11.2(@types/react@18.2.14)(react@18.2.0):
resolution: {integrity: sha512-1XfIxnUXzyswPAPXo1P3Pdx2mq/pIqZICkWN60Hby0d9Iqb+MEIpqgYVlbflvHdrp2YR/q3jyKWRPJJ100yxaw==}
/valtio@1.10.6(react@18.2.0):
resolution: {integrity: sha512-SxN1bHUmdhW6V8qsQTpCgJEwp7uHbntuH0S9cdLQtiohuevwBksbpXjwj5uDMA7bLwg1WKyq9sEpZrx3TIMrkA==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=16.8'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
dependencies:
'@types/react': 18.2.14
proxy-compare: 2.5.1
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
@@ -13778,8 +13737,8 @@ packages:
- supports-color
dev: true
/wagmi@1.4.5(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-Ph62E6cO5n2Z8Z5LTyZrkaNprxTsbC4w0qZJT4OJdXrEELziI8z/b4FO6amVFXdu2rDp/wpvF56e4mhKC8/Kdw==}
/wagmi@1.3.7(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2):
resolution: {integrity: sha512-n4Qwby0QB8/JSGjc0gGn4MWZ0BT229agEg6saPGS6VswmPjJLdkLq9qxKn1JptXxPREmb/ZvwHxU++3Z+gQJVA==}
peerDependencies:
react: '>=17.0.0'
typescript: '>=5.0.4'
@@ -13791,7 +13750,7 @@ packages:
'@tanstack/query-sync-storage-persister': 4.32.6
'@tanstack/react-query': 4.32.6(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-query-persist-client': 4.32.6(@tanstack/react-query@4.32.6)
'@wagmi/core': 1.4.5(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
'@wagmi/core': 1.3.6(@types/react@18.2.14)(react@18.2.0)(typescript@5.1.3)(viem@1.12.2)
abitype: 0.8.7(typescript@5.1.3)
react: 18.2.0
typescript: 5.1.3
Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

After

Width:  |  Height:  |  Size: 817 B

+1 -3
View File
@@ -13,9 +13,7 @@ export const Checkboxes: Story<CheckboxProps> = (args) => {
<Checkbox
{...args}
checked={isChecked}
onCheckedChange={setIsChecked}
id="story-checkbox"
label="example label"
onClick={(e: React.ChangeEvent) => setIsChecked((e.target as HTMLInputElement).checked)}
/>
</StoryWrapper>
);
+73 -46
View File
@@ -1,14 +1,10 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { Root, Indicator } from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import { layoutMixins } from '@/styles/layoutMixins';
import { breakpoints } from '@/styles';
type ElementProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
id?: string;
label?: React.ReactNode;
onClick: (e: React.ChangeEvent) => void;
};
type StyleProps = {
@@ -17,54 +13,85 @@ type StyleProps = {
export type CheckboxProps = ElementProps & StyleProps;
export const Checkbox: React.FC<CheckboxProps> = ({
checked,
className,
onCheckedChange,
id,
label,
}) => (
<Styled.Container>
<Styled.Root className={className} checked={checked} onCheckedChange={onCheckedChange} id={id}>
<Styled.Indicator>
<CheckIcon />
</Styled.Indicator>
</Styled.Root>
{label && <label htmlFor={id}>{label}</label>}
</Styled.Container>
);
export const Checkbox: React.FC<CheckboxProps> = ({ checked, className, onClick }) => {
return (
<Styled.CheckboxWrapper className={className}>
<Styled.Checkbox
type="checkbox"
checked={checked}
onChange={onClick}
/>
<Styled.CustomCheckbox />
</Styled.CheckboxWrapper>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Container = styled.div`
${layoutMixins.row}
gap: 1ch;
font: var(--font-small-book);
Styled.CustomCheckbox = styled.span`
width: 1.25em;
height: 1.25em;
label {
cursor: pointer;
}
`;
Styled.Root = styled(Root)`
--checkbox-backgroundColor: var(--color-layer-0);
min-width: 1.25rem;
height: 1.25rem;
border-radius: 0.375rem;
border: var(--border-width) solid var(--color-border);
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: var(--checkbox-backgroundColor);
&[data-state='checked'] {
--checkbox-backgroundColor: var(--color-accent);
&::after {
position: absolute;
content: '';
top: 0.25em;
left: 0.4375em;
width: 0.3125em;
height: 0.5em;
border: solid var(--checkbox-checkColor);
border-width: 0 0.125em 0.125em 0;
border-radius: 0.0625em;
opacity: 0;
transform: rotate(0deg) scale(0);
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
@media ${breakpoints.tablet} {
width: 1.5em;
height: 1.5em;
&::after {
top: 0.25em;
left: 0.4375em;
width: 0.4375em;
height: 0.625em;
}
}
`;
Styled.Indicator = styled(Indicator)`
display: flex;
align-items: center;
justify-content: center;
Styled.CheckboxWrapper = styled.div`
--checkbox-backgroundColor: var(--color-layer-1);
--checkbox-checkColor: var(--color-text-1);
color: var(--color-text-2);
display: flex;
border-radius: 0.25em;
overflow: hidden;
position: relative;
cursor: pointer;
> input:checked ~ ${Styled.CustomCheckbox}::after {
opacity: 1;
transform: rotate(40deg) scale(1);
}
`;
Styled.Checkbox = styled.input`
width: 1.25em;
height: 1.25em;
z-index: 1;
cursor: pointer;
opacity: 0;
@media ${breakpoints.tablet} {
width: 1.5em;
height: 1.5em;
}
`;
+6 -5
View File
@@ -1,13 +1,16 @@
import type { Story } from '@ladle/react';
import { Collapsible, type CollapsibleProps } from '@/components/Collapsible';
import { Collapsible } from '@/components/Collapsible';
import { StoryWrapper } from '.ladle/components';
import { IconName } from './Icon';
export const CollapsibleStory: Story<CollapsibleProps> = (args) => (
export const CollapsibleStory: Story<Parameters<typeof Collapsible>> = (args) => (
<StoryWrapper>
<Collapsible {...args}>
<Collapsible
label="Collapsible List of Items"
{...args}
>
<ul>
<li>Collapsible Item 1</li>
<li>Collapsible Item 2</li>
@@ -20,8 +23,6 @@ export const CollapsibleStory: Story<CollapsibleProps> = (args) => (
CollapsibleStory.args = {
disabled: false,
withTrigger: true,
label: 'Collapsible List of Items',
};
CollapsibleStory.argTypes = {
+18 -24
View File
@@ -14,7 +14,6 @@ type ElementProps = {
label: React.ReactNode;
triggerIcon?: IconName;
children: React.ReactNode;
withTrigger?: boolean;
};
type StyleProps = {
@@ -24,8 +23,6 @@ type StyleProps = {
fullWidth?: boolean;
};
export type CollapsibleProps = ElementProps & StyleProps;
export const Collapsible = ({
defaultOpen = false,
disabled,
@@ -38,29 +35,26 @@ export const Collapsible = ({
triggerIconSide = 'left',
fullWidth,
className,
withTrigger = true,
}: CollapsibleProps) => (
}: ElementProps & StyleProps) => (
<Styled.Root defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}>
{withTrigger && (
<Styled.Trigger className={className} disabled={disabled}>
{triggerIconSide === 'right' && (
<>
{label}
{fullWidth && <HorizontalSeparatorFiller />}
</>
)}
<Styled.Trigger className={className} disabled={disabled}>
{triggerIconSide === 'right' && (
<>
{label}
{fullWidth && <HorizontalSeparatorFiller />}
</>
)}
<Styled.TriggerIcon>
<Icon iconName={triggerIcon} />
</Styled.TriggerIcon>
{triggerIconSide === 'left' && (
<>
{fullWidth && <HorizontalSeparatorFiller />}
{label}
</>
)}
</Styled.Trigger>
)}
<Styled.TriggerIcon>
<Icon iconName={triggerIcon} />
</Styled.TriggerIcon>
{triggerIconSide === 'left' && (
<>
{fullWidth && <HorizontalSeparatorFiller />}
{label}
</>
)}
</Styled.Trigger>
<Styled.Content $transitionDuration={transitionDuration}>{children}</Styled.Content>
</Styled.Root>
);
+2 -12
View File
@@ -22,12 +22,7 @@ type PickComboxMenuProps<
MenuGroupValue extends string | number
> = Pick<
ComboboxMenuProps<MenuItemValue, MenuGroupValue>,
| 'inputPlaceholder'
| 'onItemSelected'
| 'slotEmpty'
| 'withItemBorders'
| 'withSearch'
| 'withStickyLayout'
'inputPlaceholder' | 'onItemSelected' | 'slotEmpty' | 'withSearch' | 'withStickyLayout'
>;
type PickDialogProps = Pick<
@@ -58,7 +53,6 @@ export const ComboboxDialogMenu = <
onItemSelected,
inputPlaceholder,
slotEmpty,
withItemBorders,
withSearch,
withStickyLayout = true,
children,
@@ -89,7 +83,6 @@ export const ComboboxDialogMenu = <
title={title}
inputPlaceholder={inputPlaceholder}
slotEmpty={slotEmpty}
withItemBorders={withItemBorders}
withSearch={withSearch}
withStickyLayout={withStickyLayout}
/>
@@ -102,8 +95,6 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)`
/* Params */
--comboboxDialogMenu-backgroundColor: var(--color-layer-2);
--comboboxDialogMenu-item-gap: 0.5rem;
--comboboxDialogMenu-item-padding: 0.5rem 1rem;
/* Overrides */
& {
@@ -127,6 +118,5 @@ Styled.Dialog = styled(Dialog)`
Styled.ComboboxMenu = styled(ComboboxMenu)`
--comboboxMenu-backgroundColor: var(--comboboxDialogMenu-backgroundColor);
--comboboxMenu-item-gap: var(--comboboxDialogMenu-item-gap);
--comboboxMenu-item-padding: var(--comboboxDialogMenu-item-padding);
--comboboxMenu-item-gap: var(--comboxDialogMenu-item-gap, 0.5rem);
`;
+17 -50
View File
@@ -2,7 +2,7 @@ import { Fragment, type ReactNode, useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { Command } from 'cmdk';
import { MenuItem, type MenuConfig } from '@/constants/menus';
import { type MenuConfig } from '@/constants/menus';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -20,7 +20,6 @@ type ElementProps<MenuItemValue extends string | number, MenuGroupValue extends
type StyleProps = {
className?: string;
withItemBorders?: boolean;
withStickyLayout?: boolean;
};
@@ -39,7 +38,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
withSearch = true,
className,
withItemBorders,
withStickyLayout,
}: ComboboxMenuProps<MenuItemValue, MenuGroupValue>) => {
const [highlightedCommand, setHighlightedCommand] = useState<MenuItemValue>();
@@ -82,7 +80,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
<Styled.Group
key={group.group}
heading={group.groupLabel}
$withItemBorders={withItemBorders}
$withStickyLayout={withStickyLayout}
>
{group.items.map((item) => (
@@ -102,31 +99,22 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
}
}}
disabled={item.disabled}
$withItemBorders={withItemBorders}
>
{
<>
{item.slotBefore}
{item.slotCustomContent ?? (
<Styled.ItemLabel>
<span>
{typeof item.label === 'string'
? `${item.label}${item.subitems?.length ? '…' : ''}`
: item.label}
{item.tag && (
<>
{' '}
<Tag>{item.tag}</Tag>
</>
)}
</span>
{item.description && <span>{item.description}</span>}
</Styled.ItemLabel>
{item.slotBefore}
<Styled.ItemLabel>
<span>
{`${item.label}${item.subitems?.length ? '…' : ''}`}
{item.tag && (
<>
{' '}
<Tag>{item.tag}</Tag>
</>
)}
{item.slotAfter}
{item.subitems && '→'}
</>
}
</span>
{item.description && <span>{item.description}</span>}
</Styled.ItemLabel>
{item.slotAfter}
{item.subitems && '→'}
</Styled.Item>
{searchValue &&
@@ -149,7 +137,6 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
onItemSelected?.();
}}
disabled={subitem.disabled}
$withItemBorders={withItemBorders}
>
{subitem.slotBefore}
<Styled.ItemLabel>
@@ -182,17 +169,14 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>`
--comboboxMenu-backgroundColor: var(--color-layer-2);
--comboboxMenu-input-backgroundColor: var(--color-layer-3);
--comboboxMenu-input-height: 2.5rem;
--comboboxMenu-item-checked-backgroundColor: ;
--comboboxMenu-item-checked-textColor: ;
--comboboxMenu-item-highlighted-backgroundColor: var(--color-layer-3);
--comboboxMenu-item-highlighted-textColor: var(--color-text-1);
--comboboxMenu-item-backgroundColor: ;
--comboboxMenu-item-gap: 0.5rem;
--comboboxMenu-item-padding: 0.5em 1em;
display: grid;
align-content: start;
@@ -241,7 +225,7 @@ Styled.Input = styled(Command.Input)`
gap: 0.5rem;
`;
Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLayout?: boolean }>`
Styled.Group = styled(Command.Group)<{ $withStickyLayout?: boolean }>`
color: var(--color-text-0);
> [cmdk-group-heading] {
@@ -264,14 +248,6 @@ Styled.Group = styled(Command.Group)<{ $withItemBorders?: boolean; $withStickyLa
${layoutMixins.stickyArea3}
}
`}
${({ $withItemBorders }) =>
$withItemBorders &&
css`
> [cmdk-group-items] {
padding: var(--border-width) 0;
}
`}
`;
Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
@@ -298,14 +274,13 @@ Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>`
`}
`;
Styled.Item = styled(Command.Item)<{ $withItemBorders?: boolean }>`
Styled.Item = styled(Command.Item)`
${layoutMixins.scrollSnapItem}
${popoverMixins.item}
--item-checked-backgroundColor: var(--comboboxMenu-item-checked-backgroundColor);
--item-checked-textColor: var(--comboboxMenu-item-checked-textColor);
--item-highlighted-textColor: var(--comboboxMenu-item-highlighted-textColor);
--item-gap: var(--comboboxMenu-item-gap);
--item-padding: var(--comboboxMenu-item-padding);
background-color: var(--comboboxMenu-backgroundColor, inherit);
@@ -316,12 +291,6 @@ Styled.Item = styled(Command.Item)<{ $withItemBorders?: boolean }>`
opacity: 0.75;
cursor: not-allowed;
}
${({ $withItemBorders }) =>
$withItemBorders &&
css`
${layoutMixins.withOuterBorder}
`}
`;
Styled.ItemLabel = styled.div`
@@ -340,8 +309,6 @@ Styled.ItemLabel = styled.div`
opacity: 0.8;
}
}
min-width: 0;
`;
Styled.Empty = styled(Command.Empty)`
+3 -3
View File
@@ -15,10 +15,10 @@ CopyButtonStory.args = {
};
CopyButtonStory.argTypes = {
buttonType: {
options: ["text", "icon", "default"],
shownAsText: {
options: [true, false],
control: { type: 'select' },
defaultValue: "default",
defaultValue: false,
},
children: {
options: ['some text to copy'],
+4 -36
View File
@@ -9,20 +9,14 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { Button, ButtonProps } from './Button';
import { Icon, IconName } from './Icon';
import { IconButton } from './IconButton';
export type CopyButtonProps = {
value?: string;
buttonType?: 'text' | 'icon' | 'default';
shownAsText?: boolean;
children?: React.ReactNode;
} & ButtonProps;
export const CopyButton = ({
value,
buttonType = 'default',
children,
...buttonProps
}: CopyButtonProps) => {
export const CopyButton = ({ value, shownAsText, children, ...buttonProps }: CopyButtonProps) => {
const stringGetter = useStringGetter();
const [copied, setCopied] = useState(false);
@@ -34,19 +28,11 @@ export const CopyButton = ({
setTimeout(() => setCopied(false), 500);
};
return buttonType === 'text' ? (
return shownAsText ? (
<Styled.InlineRow onClick={onCopy} copied={copied}>
{children}
<Styled.Icon copied={copied} iconName={copied ? IconName.Check : IconName.Copy} />
<Icon iconName={IconName.Copy} />
</Styled.InlineRow>
) : buttonType === 'icon' ? (
<Styled.IconButton
{...buttonProps}
copied={copied}
action={ButtonAction.Base}
iconName={copied ? IconName.Check : IconName.Copy}
onClick={onCopy}
/>
) : (
<Button
{...buttonProps}
@@ -77,21 +63,3 @@ Styled.InlineRow = styled.div<{ copied: boolean }>`
}
`}
`;
Styled.Icon = styled(Icon)<{ copied: boolean }>`
${({ copied }) =>
copied &&
css`
color: var(--color-positive);
`}
`;
Styled.IconButton = styled(IconButton)<{ copied: boolean }>`
${({ copied }) =>
copied &&
css`
svg {
color: var(--color-positive);
}
`}
`;
+28 -38
View File
@@ -38,7 +38,6 @@ type ElementProps = {
slotTrigger?: React.ReactNode;
slotHeaderInner?: React.ReactNode;
slotFooter?: React.ReactNode;
withClose?: boolean;
};
type StyleProps = {
@@ -47,7 +46,6 @@ type StyleProps = {
hasHeaderBorder?: boolean;
children?: React.ReactNode;
className?: string;
withAnimation?: boolean;
};
export type DialogProps = ElementProps & StyleProps;
@@ -81,11 +79,9 @@ export const Dialog = ({
slotTrigger,
slotHeaderInner,
slotFooter,
withClose = true,
placement = DialogPlacement.Default,
portalContainer,
hasHeaderBorder = false,
withAnimation = false,
children,
className,
}: DialogProps) => {
@@ -109,7 +105,6 @@ export const Dialog = ({
e.preventDefault();
}
}}
$withAnimation={withAnimation}
>
<Styled.Header $withBorder={hasHeaderBorder}>
<Styled.HeaderTopRow>
@@ -119,7 +114,7 @@ export const Dialog = ({
{title && <Styled.Title>{title}</Styled.Title>}
{!preventClose && withClose && (
{!preventClose && (
<Styled.Close ref={closeButtonRef}>
<Icon iconName={IconName.Close} />
</Styled.Close>
@@ -173,7 +168,7 @@ Styled.Overlay = styled(Overlay)`
}
`;
Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?: boolean }>`
Styled.Container = styled(Content)<{ placement: DialogPlacement }>`
/* Params */
--dialog-inset: 1rem;
--dialog-width: 30rem;
@@ -229,7 +224,7 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?
outline: none;
${({ placement, $withAnimation }) =>
${({ placement }) =>
({
[DialogPlacement.Default]: css`
inset: var(--dialog-inset);
@@ -265,11 +260,9 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?
padding-bottom: var(--dialog-radius); */
}
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
opacity: 0;
}
@@ -277,19 +270,18 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?
max-height: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
&[data-state='closed'] {
animation: ${keyframes`
&[data-state='closed'] {
animation: ${keyframes`
to {
opacity: 0;
scale: 0.9;
max-height: 0;
}
`} 0.15s;
}
}
`}
}
`,
[DialogPlacement.Sidebar]: css`
--dialog-width: var(--sidebar-width);
@@ -299,52 +291,50 @@ Styled.Container = styled(Content)<{ placement: DialogPlacement; $withAnimation?
margin-left: auto;
}
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
translate: 100% 0;
opacity: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
&[data-state='closed'] {
animation: ${keyframes`
&[data-state='closed'] {
animation: ${keyframes`
to {
translate: 100% 0;
opacity: 0;
}
`} 0.15s var(--ease-out-expo);
}
}
`}
}
`,
[DialogPlacement.Inline]: css`
${$withAnimation &&
css`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
@media (prefers-reduced-motion: no-preference) {
&[data-state='open'] {
animation: ${keyframes`
from {
scale: 0.99;
opacity: 0;
/* filter: blur(2px); */
/* backdrop-filter: none; */
}
`} 0.15s var(--ease-out-expo);
}
}
&[data-state='closed'] {
animation: ${keyframes`
&[data-state='closed'] {
animation: ${keyframes`
to {
scale: 0.99;
opacity: 0;
/* filter: blur(2px); */
/* backdrop-filter: none; */
}
`} 0.15s var(--ease-out-expo);
}
}
`}
}
`,
[DialogPlacement.FullScreen]: css`
--dialog-width: 100vw;
+1 -4
View File
@@ -9,7 +9,6 @@ import {
RadioItem,
Portal,
} from '@radix-ui/react-dropdown-menu';
import { CheckIcon } from '@radix-ui/react-icons';
import { type MenuItem } from '@/constants/menus';
@@ -91,9 +90,7 @@ export const DropdownSelectMenu = <MenuItemValue extends string>({
{slotAfter}
<Styled.ItemIndicator>
<CheckIcon />
</Styled.ItemIndicator>
<Styled.ItemIndicator>{/* <CheckIcon /> */}</Styled.ItemIndicator>
</Styled.RadioItem>
))}
</RadioGroup>
+29 -2
View File
@@ -1,16 +1,43 @@
import styled, { AnyStyledComponent } from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
import { Icon, IconName } from '@/components/Icon';
export const GreenCheckCircle = ({ className }: { className?: string }) => (
<Styled.Icon className={className} iconName={IconName.CheckCircle} />
<Styled.GreenCheckCircle className={className}>
<Icon iconName={IconName.Check} />
</Styled.GreenCheckCircle>
);
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Icon = styled(Icon)`
Styled.GreenCheckCircle = styled.div`
${layoutMixins.stack}
--icon-size: 1.25rem;
--icon-border-width: 3px;
width: var(--icon-size);
height: var(--icon-size);
border-radius: 50%;
align-items: center;
&:before {
content: '';
width: calc(var(--icon-size) - var(--icon-border-width) * 2);
height: calc(var(--icon-size) - var(--icon-border-width) * 2);
border: solid var(--icon-border-width) var(--color-positive);
border-radius: 50%;
opacity: 0.6;
}
svg {
width: calc(var(--icon-size) / 3);
height: calc(var(--icon-size) / 3);
color: var(--color-positive);
justify-self: center;
}
`;
-3
View File
@@ -15,7 +15,6 @@ import {
CautionCircleIcon,
ChatIcon,
CheckIcon,
CheckCircleIcon,
ChevronLeftIcon,
ChevronRightIcon,
ClockIcon,
@@ -87,7 +86,6 @@ export enum IconName {
CautionCircleStroked = 'CautionCircleStroked',
Chat = 'Chat',
Check = 'Check',
CheckCircle = 'CheckCircle',
ChevronLeft = 'ChevronLeft',
ChevronRight = 'ChevronRight',
Clock = 'Clock',
@@ -160,7 +158,6 @@ const icons = {
[IconName.CautionCircleStroked]: CautionCircleStrokeIcon,
[IconName.Chat]: ChatIcon,
[IconName.Check]: CheckIcon,
[IconName.CheckCircle]: CheckCircleIcon,
[IconName.ChevronLeft]: ChevronLeftIcon,
[IconName.ChevronRight]: ChevronRightIcon,
[IconName.Clock]: ClockIcon,
+64 -11
View File
@@ -1,4 +1,4 @@
import { forwardRef, Ref } from 'react';
import { forwardRef, Ref, useEffect, useRef, useState } from 'react';
import styled, { type AnyStyledComponent, css, keyframes } from 'styled-components';
import { NavLink, matchPath, useLocation } from 'react-router-dom';
@@ -21,6 +21,8 @@ import {
import { layoutMixins } from '@/styles/layoutMixins';
import { isTruthy } from '@/lib/isTruthy';
import { Tag } from './Tag';
import { Icon, IconName } from './Icon';
@@ -59,6 +61,7 @@ const NavItem = forwardRef(
<>
{slotBefore}
<span>
{/* {`${label}${subitems?.length ? ' ' : ''}`} */}
{label}
{tag && (
<>
@@ -97,6 +100,8 @@ const NavItem = forwardRef(
}
);
type TriggerRef = HTMLAnchorElement | HTMLDivElement | HTMLButtonElement | null;
export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue extends string>({
onSelectItem,
onSelectGroup,
@@ -107,6 +112,33 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
dir = 'ltr',
className,
}: ElementProps<MenuItemValue, MenuGroupValue> & StyleProps) => {
// Disable click (close) in the first 500ms after hover (open)
// https://github.com/radix-ui/primitives/issues/1630#issuecomment-1545995075
const [clickIsDisabled, setClickIsDisabled] = useState(false);
const triggerRefs = useRef({} as { [value: string]: TriggerRef });
useEffect(() => {
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'data-state' &&
(mutation.target as unknown as HTMLOrSVGElement).dataset.state === 'open' &&
mutation.target !== document.activeElement
) {
setClickIsDisabled(true);
setTimeout(() => setClickIsDisabled(false), 500);
}
}
});
for (const element of Object.values(triggerRefs.current).filter(isTruthy)) {
observer.observe(element, { attributes: true });
}
return () => observer.disconnect();
}, []);
const renderSubitems = ({
item,
depth = 0,
@@ -116,18 +148,18 @@ export const NavigationMenu = <MenuItemValue extends string, MenuGroupValue exte
}) => (
<>
<Styled.SubMenuTrigger
asChild
onPointerMove={(e: MouseEvent) => e.preventDefault()}
onPointerLeave={(e: MouseEvent) => e.preventDefault()}
asChild={depth > 0}
ref={(ref: TriggerRef) => (triggerRefs.current[item.value] = ref)}
onClick={(e: MouseEvent) => {
if (clickIsDisabled) {
e.preventDefault();
}
}}
>
<Styled.NavItem onSelect={onSelectGroup} orientation={itemOrientation} {...item} />
</Styled.SubMenuTrigger>
<Styled.Content
onPointerEnter={(e: MouseEvent) => e.preventDefault()}
onPointerLeave={(e: MouseEvent) => e.preventDefault()}
data-placement={submenuPlacement}
>
<Styled.Content data-placement={submenuPlacement}>
<Styled.Sub data-placement={submenuPlacement}>
<Styled.List
data-orientation={depth > 0 ? 'menu' : orientation === 'vertical' ? 'vertical' : 'menu'}
@@ -378,12 +410,25 @@ Styled.List = styled(List)`
`;
Styled.ListItem = styled(Item)`
/* display: contents; */
display: grid;
position: relative;
${Styled.List}[data-orientation="horizontal"] > & {
gap: var(--submenu-side-offset);
}
/* ${Styled.List}[data-orientation="menu"] > & {
grid-template-columns: 1fr 0;
align-items: start;
gap: 2rem;
} */
/* &:has([data-state="open"]) {
position: sticky;
left: 0;
right: 0;
} */
`;
Styled.SubMenuTrigger = styled(Trigger)`
@@ -391,6 +436,11 @@ Styled.SubMenuTrigger = styled(Trigger)`
outline-offset: -2px;
&[data-state='open'] {
div {
background-color: var(--navigationMenu-item-checked-backgroundColor);
color: var(--navigationMenu-item-checked-textColor);
}
svg {
rotate: 0.5turn;
}
@@ -402,10 +452,11 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
subitems?.length
? css`
${popoverMixins.trigger}
--trigger-open-backgroundColor: var(--navigationMenu-item-checked-backgroundColor);
--trigger-backgroundColor: transparent;
--trigger-open-backgroundColor: var(--color-layer-3);
`
: css`
&:hover:not(:active) {
&:hover:not(.active) {
background-color: var(--navigationMenu-item-highlighted-backgroundColor);
color: var(--navigationMenu-item-highlighted-textColor);
}
@@ -419,6 +470,8 @@ Styled.NavItem = styled(NavItem)<{ orientation: 'horizontal' | 'vertical' }>`
--item-radius: var(--navigationMenu-item-radius);
--item-padding: var(--navigationMenu-item-padding);
/* ${popoverMixins.backdropOverlay} */
${layoutMixins.scrollSnapItem}
min-height: var(--navigationMenu-item-height);
-201
View File
@@ -1,201 +0,0 @@
import React, { type MouseEvent } from 'react';
import styled, { css } from 'styled-components';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import {
NotificationStatus,
type Notification as NotificationDataType,
} from '@/constants/notifications';
import { useNotifications } from '@/hooks/useNotifications';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import { IconButton } from '@/components/IconButton';
import { IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
type ElementProps = {
notification: NotificationDataType;
onClick?: () => void;
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotTitleLeft?: React.ReactNode;
slotTitleRight?: React.ReactNode;
slotDescription?: React.ReactNode;
slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
withClose?: boolean;
};
type StyleProps = {
className?: string;
isToast?: boolean;
};
export type NotificationProps = ElementProps & StyleProps;
export const Notification = ({
className,
isToast,
notification,
onClick,
slotIcon,
slotTitle,
slotTitleLeft,
slotTitleRight,
slotDescription,
slotCustomContent,
slotAction,
withClose = !isToast,
}: NotificationProps) => {
const { markCleared, markSeen } = useNotifications();
return (
<$Container className={className} isToast={isToast} onClick={onClick}>
<$Header>
{slotTitleLeft ?? (slotIcon && <$Icon>{slotIcon}</$Icon>)}
<$Title>{slotTitle}</$Title>
{slotTitleRight}
{withClose && !isToast && (
<$ActionItems>
<$Output
type={OutputType.RelativeTime}
value={notification.timestamps[NotificationStatus.Triggered]}
/>
{notification.status < NotificationStatus.Seen ? <$UnreadIndicator /> : null}
{notification.status < NotificationStatus.Cleared ? (
<$IconButton
iconName={IconName.Close}
shape={ButtonShape.Square}
size={ButtonSize.XSmall}
onClick={(e: MouseEvent) => {
e.stopPropagation();
if (notification.status < NotificationStatus.Seen) {
markSeen(notification);
} else if (notification.status < NotificationStatus.Cleared) {
markCleared(notification);
}
}}
/>
) : null}
</$ActionItems>
)}
</$Header>
<$Description>{slotCustomContent ?? slotDescription}</$Description>
{slotAction && <$Action>{slotAction}</$Action>}
</$Container>
);
};
const $Container = styled.div<{ isToast?: boolean }>`
// Params
--toast-icon-size: 1.75em;
// Rules
${popoverMixins.popover}
overflow: visible;
padding: 1rem 1.25rem;
${({ isToast }) =>
isToast
? css`
padding: 1rem;
box-shadow: 0 0 0 var(--border-width) var(--color-border),
// border
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
`
: css`
flex: 1;
background-color: transparent;
border-radius: 0;
backdrop-filter: none;
`}
`;
const $Header = styled.header`
${layoutMixins.row}
position: relative;
`;
const $Icon = styled.div`
${layoutMixins.row}
float: left;
margin-right: 0.5rem;
line-height: 1;
> svg,
img {
width: 1.5rem;
height: 1.5rem;
}
`;
const $Title = styled.div`
flex: 1;
font: var(--font-base-medium);
color: var(--color-text-2);
overflow: hidden;
text-overflow: ellipsis;
`;
const $Description = styled.div`
margin-top: 0.5rem;
color: var(--color-text-0);
font: var(--font-small-book);
`;
const $Action = styled.div`
margin-top: 0.5rem;
font: var(--font-small-book);
`;
const $ActionItems = styled.div`
min-width: fit-content;
padding-left: 0.5rem;
height: 100%;
${layoutMixins.row};
justify-content: end;
gap: 0.5rem;
position: absolute;
right: 0;
top: 0;
background-color: var(--color-layer-2);
font: var(--font-mini-book);
display: none;
z-index: 1;
${$Container}:hover & {
display: flex;
}
`;
const $UnreadIndicator = styled.div`
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: var(--color-accent);
border: 1px solid var(--color-layer-2);
`;
const $Output = styled(Output)`
color: var(--color-text-0);
`;
const $IconButton = styled(IconButton)`
--button-border: none;
--button-textColor: var(--color-text-0);
--button-hover-textColor: var(--color-text-1);
`;
+1 -7
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useState } from 'react';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -22,11 +22,6 @@ export const SearchInput = ({
}: SearchInputProps) => {
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (isOpen) inputRef?.current?.focus();
}, [inputRef, isOpen]);
return (
<Styled.Search>
@@ -46,7 +41,6 @@ export const SearchInput = ({
/>
<Styled.Input
autoFocus
ref={inputRef}
value={value}
isOpen={isOpen}
type={type}
+7 -12
View File
@@ -12,8 +12,6 @@ import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { WithLabel } from '@/components/WithLabel';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import breakpoints from '@/styles/breakpoints';
type ElementProps = {
asChild?: boolean;
@@ -86,30 +84,27 @@ export const SearchSelectMenu = ({
const Styled: Record<string, AnyStyledComponent> = {};
Styled.SearchSelectMenu = styled.div`
${layoutMixins.column}
`;
Styled.MenuTrigger = styled.div`
height: var(--form-input-height);
${layoutMixins.spacedRow}
align-items: center;
padding: var(--form-input-paddingY) var(--form-input-paddingX);
@media ${breakpoints.tablet} {
height: var(--form-input-height-mobile);
}
padding: 0 1rem;
`;
Styled.WithLabel = styled(WithLabel)`
${formMixins.inputLabel}
--label-textColor: var(--color-text-0);
gap: 0.25rem;
label {
font: var(--font-mini-book);
}
`;
Styled.SearchSelectMenu = styled.div`
${layoutMixins.column}
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
+9 -15
View File
@@ -1,5 +1,5 @@
import React from 'react';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import styled, { type AnyStyledComponent } from 'styled-components';
import {
Root,
@@ -13,13 +13,11 @@ import {
Portal,
Icon as SelectIcon,
} from '@radix-ui/react-select';
import { CheckIcon } from '@radix-ui/react-icons';
import { popoverMixins } from '@/styles/popoverMixins';
import { formMixins } from '@/styles/formMixins';
import { WithLabel } from '@/components/WithLabel';
import { Icon, IconName } from './Icon';
export const SelectMenu = <T extends string>({
children,
@@ -46,9 +44,7 @@ export const SelectMenu = <T extends string>({
) : (
<Value />
)}
{React.Children.toArray(children).length > 1 && (
<Styled.TriggerIcon iconName={IconName.Triangle} />
)}
{React.Children.toArray(children).length > 1 && <Styled.DropdownIcon />}
</Styled.Trigger>
<Portal>
<Styled.Content className={className}>
@@ -72,9 +68,7 @@ export const SelectItem = <T extends string>({
}) => (
<Styled.Item className={className} value={value}>
<ItemText>{label}</ItemText>
<Styled.ItemIndicator>
<CheckIcon />
</Styled.ItemIndicator>
<Styled.ItemIndicator>{/* <CheckIcon /> */}</Styled.ItemIndicator>
</Styled.Item>
);
@@ -94,6 +88,12 @@ Styled.Trigger = styled(Trigger)<{ $withBlur?: boolean }>`
}
`;
Styled.DropdownIcon = styled(SelectIcon)`
font-size: 0.675em;
color: var(--color-text-1);
margin-left: auto;
`;
Styled.Content = styled(Content)`
--select-menu-content-maxWidth: ;
max-width: var(--select-menu-content-maxWidth);
@@ -116,9 +116,3 @@ Styled.WithLabel = styled(WithLabel)`
${formMixins.inputLabel}
border-radius: 0;
`;
Styled.TriggerIcon = styled(Icon)`
width: 0.625rem;
height: 0.375rem;
color: var(--color-text-0);
`;
+8 -12
View File
@@ -831,7 +831,6 @@ Styled.SortArrow = styled.span<{ sortDirection: 'ascending' | 'descending' }>`
Styled.Thead = styled.thead<StyleProps>`
${layoutMixins.stickyHeader}
scroll-snap-align: none;
font: var(--font-mini-book);
> * {
height: var(--stickyArea-topHeight);
@@ -840,17 +839,18 @@ Styled.Thead = styled.thead<StyleProps>`
color: var(--tableHeader-textColor);
background-color: var(--tableHeader-backgroundColor);
${({ withInnerBorders, withGradientCardRows }) =>
withInnerBorders &&
!withGradientCardRows &&
css`
${layoutMixins.withInnerHorizontalBorders}
`}
@media ${breakpoints.notTablet} {
${({ withInnerBorders, withGradientCardRows }) =>
withInnerBorders &&
!withGradientCardRows &&
css`
${layoutMixins.withInnerHorizontalBorders}
`}
}
`;
Styled.Tbody = styled.tbody<StyleProps>`
${layoutMixins.stickyArea2}
font: var(--font-small-book);
// If <table> height is fixed with not enough rows to overflow, vertically center the rows
&:before,
@@ -869,10 +869,6 @@ Styled.Tbody = styled.tbody<StyleProps>`
--stickyArea2-paddingBottom: var(--border-width);
--stickyArea2-paddingLeft: var(--border-width);
--stickyArea2-paddingRight: var(--border-width);
tr:first-of-type {
box-shadow: none;
}
`}
${({ withGradientCardRows }) =>
+78 -72
View File
@@ -1,18 +1,24 @@
import { type MouseEvent, useEffect, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import { Root, Action, Close } from '@radix-ui/react-toast';
import React, { useEffect, useRef, useState } from 'react';
import { Root, Title, Description, Action, Close } from '@radix-ui/react-toast';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { popoverMixins } from '@/styles/popoverMixins';
import { Notification, type NotificationProps } from '@/components/Notification';
import { IconButton } from './IconButton';
import { IconName } from './Icon';
import { CloseIcon } from '@/icons';
import styled, { keyframes } from 'styled-components';
import { popoverMixins } from '@/styles/popoverMixins';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
isOpen?: boolean;
setIsOpen?: (isOpen: boolean, isClosedFromTimeout?: boolean) => void;
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotDescription?: React.ReactNode;
slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
actionDescription?: string;
actionAltText?: string;
sensitivity?: 'foreground' | 'background';
@@ -20,22 +26,13 @@ type ElementProps = {
lastUpdated?: number;
};
type StyleProps = {
className?: string;
};
type ToastProps = NotificationProps & ElementProps & StyleProps;
type StyleProps = {};
export const Toast = ({
className,
isOpen = true,
notification,
onClick,
setIsOpen,
slotIcon,
slotTitle,
slotTitleLeft,
slotTitleRight,
slotDescription,
slotCustomContent,
slotAction,
@@ -44,7 +41,7 @@ export const Toast = ({
sensitivity = 'background',
duration = Infinity,
lastUpdated,
}: ToastProps) => {
}: ElementProps & StyleProps) => {
// Timeout
const timeout = useRef<number>();
const [isPaused, setIsPaused] = useState(false);
@@ -61,45 +58,37 @@ export const Toast = ({
return (
<$Root
className={className}
type={sensitivity}
duration={Infinity}
open={isOpen}
onOpenChange={setIsOpen}
onPause={() => setIsPaused(true)}
onResume={() => setIsPaused(false)}
onClick={() => {
setIsOpen?.(false, false);
onClick?.();
}}
>
<Close asChild>
<$CloseButton
iconName={IconName.Close}
shape={ButtonShape.Square}
size={ButtonSize.XSmall}
onClick={(e: MouseEvent) => e.stopPropagation()}
/>
</Close>
<div>
<$Container>
<$Header>
{slotIcon && <$Icon>{slotIcon}</$Icon>}
{slotCustomContent ?? (
<$Notification
isToast
notification={notification}
slotIcon={slotIcon}
slotTitle={slotTitle}
slotTitleLeft={slotTitleLeft}
slotTitleRight={slotTitleRight}
slotDescription={slotDescription}
slotAction={
actionDescription && (
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
)
}
/>
)}
<Close asChild>
<$CloseButton
iconComponent={CloseIcon}
shape={ButtonShape.Square}
size={ButtonSize.XSmall}
/>
</Close>
<$Title>{slotTitle}</$Title>
</$Header>
{!slotCustomContent && <$Description>{slotDescription}</$Description>}
{slotCustomContent}
{actionDescription && (
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
)}
</$Container>
</div>
</$Root>
);
};
@@ -125,7 +114,7 @@ const $Root = styled(Root)`
translate: var(--x) var(--y);
will-change: translate, margin-top;
/* margin-bottom: var(--toasts-gap); */
margin-bottom: var(--toasts-gap);
&[data-swipe-direction='right'] {
&[data-state='open'] {
@@ -174,10 +163,6 @@ const $Root = styled(Root)`
cursor: n-resize;
}
&:focus:not([data-swipe='end']) & {
outline: var(--color-accent) 1px solid;
}
&[data-swipe='move'] {
transition-property: opacity;
opacity: 0.98;
@@ -186,10 +171,6 @@ const $Root = styled(Root)`
* {
cursor: inherit;
}
& > * {
opacity: 0.5;
}
}
&[data-state='closed'],
@@ -217,44 +198,69 @@ const $Root = styled(Root)`
}
`;
const $Notification = styled(Notification)`
const $Container = styled.div`
// Params
--toast-icon-size: 1.75em;
// Rules
${popoverMixins.popover}
overflow: visible;
padding: 1rem;
box-shadow: 0 0 0 var(--border-width) var(--color-border),
// border
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
${$Root}:focus:not([data-swipe='end']) & {
outline: var(--color-accent) 1px solid;
}
> * {
transition: opacity 0.2s;
}
${$Root}[data-swipe='move'] & > * {
opacity: 0.5;
}
`;
const $Header = styled.header`
display: block;
`;
const $Icon = styled.div`
${layoutMixins.row}
float: left;
width: 1em;
height: 1em;
margin-right: 0.4em;
line-height: 1;
`;
const $CloseButton = styled(IconButton)`
--button-textColor: var(--color-text-0);
--button-border: none;
--button-icon-size: 0.85em;
display: none;
/* float: right; */
float: right;
margin: -0.42rem -0.42rem -0.42rem 0.42rem;
`;
// Absolute
position: absolute;
top: 0;
right: 0;
const $Title = styled(Title)`
flex: 1;
border: solid var(--border-width) var(--color-border);
border-radius: 50%;
font: var(--font-base-medium);
color: var(--color-text-2);
${$Root}:hover & {
display: block;
z-index: 2;
}
overflow: hidden;
text-overflow: ellipsis;
`;
const $Description = styled(Description)`
margin-top: 0.5rem;
color: var(--color-text-0);
font: var(--font-small-book);
`;
const $Action = styled(Action)`
-2
View File
@@ -34,7 +34,6 @@ const $ToastArea = styled.aside`
> * {
display: contents;
pointer-events: none;
position: relative;
> ol {
/* display: block; */
@@ -44,7 +43,6 @@ const $ToastArea = styled.aside`
// Desired display order: latest to earliest (¯\_(ツ)_/¯)
display: flex;
flex-direction: column-reverse;
position: relative;
pointer-events: none;
-4
View File
@@ -52,19 +52,15 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.BaseButton = styled(BaseButton)`
--button-toggle-off-backgroundColor: var(--color-layer-3);
--button-toggle-off-textColor: var(--color-text-0);
--button-toggle-off-border: solid var(--border-width) var(--border-color);
--button-toggle-on-backgroundColor: var(--color-layer-1);
--button-toggle-on-textColor: var(--color-text-2);
--button-toggle-on-border: solid var(--border-width) var(--border-color);
--button-backgroundColor: var(--button-toggle-off-backgroundColor);
--button-textColor: var(--button-toggle-off-textColor);
--button-border: var(--button-toggle-off-border);
&[data-state='on'],
&[data-state='active'] {
--button-backgroundColor: var(--button-toggle-on-backgroundColor);
--button-textColor: var(--button-toggle-on-textColor);
--button-border: var(--button-toggle-on-border);
}
`;
+1 -34
View File
@@ -124,8 +124,6 @@ export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPo
export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder;
export type OrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus;
export const AbacusOrderStatus = Abacus.exchange.dydx.abacus.output.input.OrderStatus;
const abacusOrderStatuses = [...AbacusOrderStatus.values()] as const;
export type AbacusOrderStatuses = (typeof abacusOrderStatuses)[number];
export type SubaccountFills = Abacus.exchange.dydx.abacus.output.SubaccountFill[];
export type SubaccountFill = Abacus.exchange.dydx.abacus.output.SubaccountFill;
export type SubaccountFundingPayment = Abacus.exchange.dydx.abacus.output.SubaccountFundingPayment;
@@ -211,13 +209,11 @@ type ReadonlyKeysOf<T> = {
[K in keyof T]: IfEquals<{ [_ in K]: T[K] }, { readonly [_ in K]: T[K] }, K, never>;
}[keyof T];
export type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>;
type KotlinIrEnumValues<T> = Exclude<ReadonlyKeysOf<T>, 'Companion' | '$serializer'>;
export const ORDER_SIDES: Record<KotlinIrEnumValues<typeof AbacusOrderSide>, OrderSide> = {
[AbacusOrderSide.buy.name]: OrderSide.BUY,
[AbacusOrderSide.sell.name]: OrderSide.SELL,
[AbacusOrderSide.buy.rawValue]: OrderSide.BUY,
[AbacusOrderSide.sell.rawValue]: OrderSide.SELL,
};
export const POSITION_SIDES: Record<KotlinIrEnumValues<typeof AbacusPositionSide>, PositionSide> = {
@@ -238,25 +234,12 @@ export const HISTORICAL_PNL_PERIODS: Record<
export const ORDER_STATUS_STRINGS: Record<KotlinIrEnumValues<typeof AbacusOrderStatus>, string> = {
[AbacusOrderStatus.open.name]: STRING_KEYS.OPEN_STATUS,
[AbacusOrderStatus.open.rawValue]: STRING_KEYS.OPEN_STATUS,
[AbacusOrderStatus.partiallyFilled.name]: STRING_KEYS.PARTIALLY_FILLED,
[AbacusOrderStatus.partiallyFilled.rawValue]: STRING_KEYS.PARTIALLY_FILLED,
[AbacusOrderStatus.filled.name]: STRING_KEYS.ORDER_FILLED,
[AbacusOrderStatus.filled.rawValue]: STRING_KEYS.ORDER_FILLED,
[AbacusOrderStatus.cancelled.name]: STRING_KEYS.CANCELED,
[AbacusOrderStatus.cancelled.rawValue]: STRING_KEYS.CANCELED,
[AbacusOrderStatus.canceling.name]: STRING_KEYS.CANCELING,
[AbacusOrderStatus.canceling.rawValue]: STRING_KEYS.CANCELING,
[AbacusOrderStatus.pending.name]: STRING_KEYS.PENDING,
[AbacusOrderStatus.pending.rawValue]: STRING_KEYS.PENDING,
[AbacusOrderStatus.untriggered.name]: STRING_KEYS.UNTRIGGERED,
[AbacusOrderStatus.untriggered.rawValue]: STRING_KEYS.UNTRIGGERED,
};
export const TRADE_TYPES: Record<
@@ -264,31 +247,15 @@ export const TRADE_TYPES: Record<
Nullable<TradeTypes>
> = {
[AbacusOrderType.limit.name]: TradeTypes.LIMIT,
[AbacusOrderType.limit.rawValue]: TradeTypes.LIMIT,
[AbacusOrderType.market.name]: TradeTypes.MARKET,
[AbacusOrderType.market.rawValue]: TradeTypes.MARKET,
[AbacusOrderType.stopLimit.name]: TradeTypes.STOP_LIMIT,
[AbacusOrderType.stopLimit.rawValue]: TradeTypes.STOP_LIMIT,
[AbacusOrderType.stopMarket.name]: TradeTypes.STOP_MARKET,
[AbacusOrderType.stopMarket.rawValue]: TradeTypes.STOP_MARKET,
[AbacusOrderType.takeProfitLimit.name]: TradeTypes.TAKE_PROFIT,
[AbacusOrderType.takeProfitLimit.rawValue]: TradeTypes.TAKE_PROFIT,
[AbacusOrderType.takeProfitMarket.name]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.takeProfitMarket.rawValue]: TradeTypes.TAKE_PROFIT_MARKET,
[AbacusOrderType.liquidated.name]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidation.name]: null,
[AbacusOrderType.liquidation.rawValue]: null,
[AbacusOrderType.trailingStop.name]: null,
[AbacusOrderType.trailingStop.rawValue]: null,
};
// Custom types involving Abacus
-2
View File
@@ -10,7 +10,6 @@ export enum DialogTypes {
MobileSignIn = 'MobileSignIn',
Onboarding = 'Onboarding',
OrderDetails = 'OrderDetails',
Preferences = 'Preferences',
RateLimit = 'RateLimit',
Receive = 'Receive',
RestrictedGeo = 'RestrictedGeo',
@@ -18,7 +17,6 @@ export enum DialogTypes {
Trade = 'Trade',
Transfer = 'Transfer',
Withdraw = 'Withdraw',
ManageFunds = 'ManageFunds',
}
export enum TradeBoxDialogTypes {
-4
View File
@@ -13,7 +13,6 @@ export enum LocalStorageKey {
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
TransferNotifications = 'dydx.TransferNotifications',
NotificationPreferences = 'dydx.NotificationPreferences',
// UI State
LastViewedMarket = 'dydx.LastViewedMarket',
@@ -26,8 +25,5 @@ export enum LocalStorageKey {
export const LOCAL_STORAGE_VERSIONS = {
[LocalStorageKey.EvmDerivedAddresses]: 'v2',
[LocalStorageKey.NotificationPreferences]: 'v1',
[LocalStorageKey.TransferNotifications]: 'v1',
[LocalStorageKey.Notifications]: 'v1',
// TODO: version all localStorage keys
};
-8
View File
@@ -6,8 +6,6 @@ import {
LOCALE_DATA,
TOOLTIPS,
WARNINGS_STRING_KEYS,
NOTIFICATIONS_STRING_KEYS,
NOTIFICATIONS,
} from '@dydxprotocol/v4-localization';
import { type LinksConfigs } from '@/hooks/useURLConfigs';
@@ -29,7 +27,6 @@ export enum SupportedLocales {
export const EN_LOCALE_DATA = {
...LOCALE_DATA[SupportedLocales.EN],
...NOTIFICATIONS[SupportedLocales.EN],
TOOLTIPS: TOOLTIPS[SupportedLocales.EN],
};
@@ -37,13 +34,8 @@ export const STRING_KEYS = {
...APP_STRING_KEYS,
...ERRORS_STRING_KEYS,
...WARNINGS_STRING_KEYS,
...NOTIFICATIONS_STRING_KEYS,
};
export const STRING_KEY_VALUES = Object.fromEntries(
Object.values(STRING_KEYS).map((key) => [key, key])
);
export type StringKey = keyof typeof STRING_KEYS;
export type LocaleData = typeof EN_LOCALE_DATA;
-1
View File
@@ -26,7 +26,6 @@ export type MenuItem<MenuItemValue, MenuItemTypes = string> = {
tag?: React.ReactNode;
slotAfter?: React.ReactNode;
description?: string;
slotCustomContent?: React.ReactNode;
href?: string;
onSelect?: (key: MenuItemValue) => void;
+8 -24
View File
@@ -2,7 +2,7 @@ import { StatusResponse } from '@0xsquid/sdk';
/** implemented in useNotificationTypes */
export enum NotificationType {
AbacusGenerated = 'AbacusGenerated',
OrderStatusChanged = 'OrderStatusChanged',
SquidTransfer = 'SquidTransfer',
}
@@ -59,7 +59,7 @@ export enum NotificationStatus {
/** Toast or NotificationsMenu item interacted with or dismissed. "Seen" in NotificationsMenu. */
Seen,
/** Notification marked for deletion. Hidden in NotificationsMenu. */
/** Notification marked for deletion. "Archived" in NotificationsMenu. */
Cleared,
}
@@ -80,20 +80,14 @@ export type Notifications = Record<NotificationId, Notification<any>>;
/** Notification display data derived from app state at runtime. */
export type NotificationDisplayData = {
icon?: React.ReactNode;
title: string; // Title for Toast, Notification, and Push Notification
body?: string; // Description body for Toast, Notification, and Push Notification
slotTitleLeft?: React.ReactNode;
slotTitleRight?: React.ReactNode;
title?: string;
// Overrides title/body for Notification in NotificationMenu
renderCustomBody?: ({
isToast,
notification,
}: {
isToast?: boolean;
notification: Notification;
}) => React.ReactNode; // Custom Notification
description?: React.ReactNode;
customContent?: React.ReactNode;
customMenuContent?: React.ReactNode;
actionDescription?: string;
@@ -128,15 +122,5 @@ export type TransferNotifcation = {
fromChainId?: string;
toAmount?: number;
triggeredAt?: number;
errorCount?: number;
status?: StatusResponse;
};
/**
* @description Struct to store whether a NotificationType should be triggered
*/
export type NotificationPreferences = {
[key in NotificationType]: boolean;
} & { version: string };
export const DEFAULT_TOAST_AUTO_CLOSE_MS = 5000;
+2 -2
View File
@@ -136,8 +136,8 @@ const useAccountsContext = () => {
getSubaccounts: async ({ dydxAddress }: { dydxAddress: DydxAddress }) => {
try {
const response = await compositeClient?.indexerClient.account.getSubaccounts(dydxAddress);
setDydxSubaccounts(response?.subaccounts);
return response?.subaccounts ?? [];
setDydxSubaccounts(response.subaccounts);
return response.subaccounts;
} catch (error) {
// 404 is expected if the user has no subaccounts
if (error.status === 404) {
+1 -2
View File
@@ -22,7 +22,6 @@ export const useCurrentMarketId = () => {
const dispatch = useDispatch();
const selectedNetwork = useSelector(getSelectedNetwork);
const marketIds = useSelector(getMarketIds, shallowEqual);
const hasMarketIds = marketIds.length > 0;
const [lastViewedMarket, setLastViewedMarket] = useLocalStorage({
key: LocalStorageKey.LastViewedMarket,
defaultValue: DEFAULT_MARKETID,
@@ -49,5 +48,5 @@ export const useCurrentMarketId = () => {
if (marketIds) {
abacusStateManager.setMarket(marketId ?? DEFAULT_MARKETID);
}
}, [selectedNetwork, hasMarketIds, marketId]);
}, [selectedNetwork, marketIds, marketId]);
};
+47 -68
View File
@@ -2,11 +2,11 @@ import { createContext, useContext, useCallback, useEffect, useMemo } from 'reac
import { useQuery } from 'react-query';
import type { StatusResponse } from '@0xsquid/sdk';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
import { LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts';
import { STATUS_ERROR_GRACE_PERIOD, useSquid } from '@/hooks/useSquid';
import { useLocalStorage } from './useLocalStorage';
const LocalNotificationsContext = createContext<
@@ -22,32 +22,16 @@ export const LocalNotificationsProvider = ({ ...props }) => (
export const useLocalNotifications = () => useContext(LocalNotificationsContext)!;
const TRANSFER_STATUS_FETCH_INTERVAL = 10_000;
const ERROR_COUNT_THRESHOLD = 3;
const useLocalNotificationsContext = () => {
// transfer notifications
const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{
[key: `dydx${string}`]: TransferNotifcation[];
version: string;
}>({
key: LocalStorageKey.TransferNotifications,
defaultValue: {
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications],
},
defaultValue: {},
});
// Ensure version matches, otherwise wipe
useEffect(() => {
if (
allTransferNotifications?.version !==
LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications]
) {
setAllTransferNotifications({
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.TransferNotifications],
});
}
}, [allTransferNotifications]);
const { dydxAddress } = useAccounts();
const transferNotifications = dydxAddress ? allTransferNotifications[dydxAddress] || [] : [];
@@ -70,61 +54,56 @@ const useLocalNotificationsContext = () => {
const squid = useSquid();
useQuery({
queryKey: 'getTransactionStatus',
const { data: transferStatuses } = useQuery({
queryKey: ['getTransactionStatus', transferNotifications],
queryFn: async () => {
const processTransferNotifications = async (transferNotifications: TransferNotifcation[]) => {
const newTransferNotifications = await Promise.all(
transferNotifications.map(async (transferNotification) => {
const {
txHash,
toChainId,
fromChainId,
triggeredAt,
errorCount,
status: currentStatus,
} = transferNotification;
const statuses: { [key: string]: StatusResponse } = {};
for (const {
txHash,
toChainId,
fromChainId,
triggeredAt,
status: currentStatus,
} of transferNotifications) {
try {
// skip if error is returned or if the transaction is not ongoing
if (
// @ts-ignore status.errors is not in the type definition but can be returned
currentStatus?.errors ||
currentStatus?.error ||
(currentStatus?.squidTransactionStatus &&
currentStatus?.squidTransactionStatus !== 'ongoing')
) {
continue;
}
if (
// @ts-ignore status.errors is not in the type definition but can be returned
!currentStatus?.errors &&
!currentStatus?.error &&
(!currentStatus?.squidTransactionStatus ||
currentStatus?.squidTransactionStatus === 'ongoing')
) {
try {
const status = await squid?.getStatus({
transactionId: txHash,
toChainId,
fromChainId,
});
if (status) {
transferNotification.status = status;
}
} catch (error) {
if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) {
if (errorCount && errorCount > ERROR_COUNT_THRESHOLD) {
transferNotification.status = error;
} else {
transferNotification.errorCount = errorCount ? errorCount + 1 : 1;
}
}
}
}
return transferNotification;
})
);
return newTransferNotifications;
};
const newTransferNotifications = await processTransferNotifications(transferNotifications);
setTransferNotifications(newTransferNotifications);
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
if (status) statuses[txHash] = status;
} catch (error) {
// ignore errors for the first 120s since the status might not be available yet
if (!triggeredAt || Date.now() - triggeredAt > STATUS_ERROR_GRACE_PERIOD) {
statuses[txHash] = error;
}
}
}
return statuses;
},
refetchInterval: TRANSFER_STATUS_FETCH_INTERVAL,
});
useEffect(() => {
if (!transferStatuses) return;
const newTransferNotifications = transferNotifications.map((notification) => {
const status = transferStatuses[notification.txHash];
if (!status) return notification;
return {
...notification,
status,
};
});
setTransferNotifications(newTransferNotifications);
}, [transferStatuses]);
return {
transferNotifications,
addTransferNotification,
+157 -163
View File
@@ -1,147 +1,125 @@
import { type ReactNode, useEffect } from 'react';
import styled from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { isEqual, groupBy } from 'lodash';
import { useNavigate } from 'react-router-dom';
import { useCallback, useEffect, useMemo } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash';
import { AlertType } from '@/constants/alerts';
import { AbacusOrderStatus, ORDER_SIDES } from '@/constants/abacus';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, StringKey } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { AppRoute } from '@/constants/routes';
import { DydxChainAsset } from '@/constants/wallets';
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { ORDER_SIDE_STRINGS } from '@/constants/trade';
import {
STRING_KEYS,
STRING_KEY_VALUES,
type StringGetterFunction,
type StringKey,
} from '@/constants/localization';
import {
type NotificationTypeConfig,
NotificationType,
DEFAULT_TOAST_AUTO_CLOSE_MS,
} from '@/constants/notifications';
import { useSelectedNetwork, useStringGetter } from '@/hooks';
import { useSelectedNetwork } from '@/hooks';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { Icon, IconName } from '@/components/Icon';
import { TradeNotification } from '@/views/notifications/TradeNotification';
import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification';
import { Output, OutputType } from '@/components/Output';
import { TransferStatusToast } from '@/views/TransferStatus';
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { getAbacusNotifications } from '@/state/notificationsSelectors';
import { getMarketIds } from '@/state/perpetualsSelectors';
import { formatSeconds } from '@/lib/timeUtils';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
const parseStringParamsForNotification = ({
stringGetter,
value,
}: {
stringGetter: StringGetterFunction;
value: unknown;
}): ReactNode => {
if (STRING_KEY_VALUES[value as StringKey]) {
return stringGetter({ key: value as StringKey });
}
import { useStringGetter } from './useStringGetter';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
return value as ReactNode;
};
export const notificationTypes: NotificationTypeConfig[] = [
export const notificationTypes = [
{
type: NotificationType.AbacusGenerated,
useTrigger: ({ trigger }) => {
type: NotificationType.OrderStatusChanged,
useTrigger: ({ trigger, lastUpdated }) => {
const stringGetter = useStringGetter();
const abacusNotifications = useSelector(getAbacusNotifications, isEqual);
const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
const fills = useSelector(getSubaccountFills, shallowEqual) || [];
const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
const orderIds = useMemo(
() => [...Object.keys(ordersByOrderId), ...Object.keys(fillsByOrderId)],
[orders, fills]
);
useEffect(() => {
for (const abacusNotif of abacusNotifications) {
const [abacusNotificationType = '', id = ''] = abacusNotif.id.split(':');
const parsedData = abacusNotif.data ? JSON.parse(abacusNotif.data) : {};
for (const orderId of orderIds) {
const fills = fillsByOrderId[orderId];
const params = Object.fromEntries(
Object.entries(parsedData).map(([key, value]) => {
return [key, parseStringParamsForNotification({ stringGetter, value })];
})
);
switch (abacusNotificationType) {
case 'order': {
trigger(
abacusNotif.id,
{
icon: abacusNotif.image && <$Icon src={abacusNotif.image} alt="" />,
title: stringGetter({ key: abacusNotif.title }),
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
toastSensitivity: 'foreground',
renderCustomBody: ({ isToast, notification }) => (
<TradeNotification
isToast={isToast}
data={parsedData}
notification={notification}
/>
const order =
ordersByOrderId[orderId] ??
(fills?.length
? {
...fills[fills.length - 1],
id: orderId,
createdAtMilliseconds: Math.max(
...fills.map((fill) => fill.createdAtMilliseconds)
),
},
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data],
true
);
break;
}
default:
trigger(
abacusNotif.id,
{
icon: abacusNotif.image && <$Icon src={abacusNotif.image} alt="" />,
title: stringGetter({ key: abacusNotif.title }),
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
toastSensitivity: 'foreground',
},
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data]
);
break;
}
}
}, [abacusNotifications, stringGetter]);
},
useNotificationAction: () => {
const dispatch = useDispatch();
const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
const ordersById = groupBy(orders, 'id');
const fills = useSelector(getSubaccountFills, shallowEqual) || [];
const fillsById = groupBy(fills, 'id');
const marketIds = useSelector(getMarketIds, shallowEqual);
const navigate = useNavigate();
status: AbacusOrderStatus.filled,
}
: undefined);
return (notificationId: string) => {
const [abacusNotificationType = '', id = ''] = notificationId.split(':');
if (ordersById[id]) {
dispatch(
openDialog({
type: DialogTypes.OrderDetails,
dialogProps: { orderId: id },
})
);
} else if (fillsById[id]) {
dispatch(
openDialog({
type: DialogTypes.FillDetails,
dialogProps: { fillId: id },
})
);
} else if (marketIds.includes(id)) {
navigate(`${AppRoute.Trade}/${id}`, {
replace: true,
});
if (order)
trigger(
order.id,
{
icon: (
<OrderStatusIcon status={order.status} totalFilled={order.totalFilled ?? 0} />
),
title: `${stringGetter({
key: order.resources.typeStringKey as StringKey,
})} ${
order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0
? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED })
: stringGetter({ key: order.resources.statusStringKey as StringKey })
}`,
description: `${stringGetter({
key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]],
})} ${order.size} ${order.marketId} @ $${order.price}`,
actionDescription: 'View Order',
actionAltText: 'View this order in the Orders tab or the Notifications menu.',
toastSensitivity:
order.status === AbacusOrderStatus.pending ? 'foreground' : 'background',
toastDuration: 5000,
},
[order.status.name, order.size],
!order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated
);
}
};
}, [orderIds]);
},
},
// useNotificationAction: () => {
// const dispatch = useDispatch();
// const orders = useSelector(getSubaccountOrders, shallowEqual) || [];
// const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order]));
// const fills = useSelector(getSubaccountFills, shallowEqual) || [];
// const fillsByOrderId = groupBy(fills, (fill) => fill.orderId);
// return (id) => {
// if (ordersByOrderId[id]) {
// dispatch(
// openDialog({
// type: DialogTypes.OrderDetails,
// dialogProps: { orderId: id },
// })
// );
// } else if (fillsByOrderId[id]) {
// const fillId = fillsByOrderId[id][0].id;
// dispatch(
// openDialog({
// type: DialogTypes.FillDetails,
// dialogProps: { fillId },
// })
// );
// }
// };
// },
} as NotificationTypeConfig<string, [string, number]>,
{
type: NotificationType.SquidTransfer,
useTrigger: ({ trigger }) => {
@@ -149,65 +127,81 @@ export const notificationTypes: NotificationTypeConfig[] = [
const { transferNotifications } = useLocalNotifications();
const { selectedNetwork } = useSelectedNetwork();
const getTitleStringKey = useCallback((type: 'deposit' | 'withdrawal', finished: boolean) => {
if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS;
if (type === 'deposit' && finished) return STRING_KEYS.DEPOSIT;
if (type === 'withdrawal' && !finished) return STRING_KEYS.WITHDRAW_IN_PROGRESS;
return STRING_KEYS.WITHDRAW;
}, []);
useEffect(() => {
for (const transfer of transferNotifications) {
const { fromChainId, status, txHash, toAmount } = transfer;
const isFinished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const icon = <Icon iconName={isFinished ? IconName.Transfer : IconName.Clock} />;
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const type = fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId ? 'withdrawal' : 'deposit';
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
const type =
fromChainId === ENVIRONMENT_CONFIG_MAP[selectedNetwork].dydxChainId
? 'withdrawal'
: 'deposit';
// TODO: confirm with design what the description should be
const description = (
<div>
<Styled.TransferText>
{type === 'deposit' ? 'Deposit of ' : 'Withdraw of '}
<Output type={OutputType.Fiat} value={toAmount} />
</Styled.TransferText>
const title = stringGetter({
key: {
deposit: isFinished ? STRING_KEYS.DEPOSIT : STRING_KEYS.DEPOSIT_IN_PROGRESS,
withdrawal: isFinished ? STRING_KEYS.WITHDRAW : STRING_KEYS.WITHDRAW_IN_PROGRESS,
}[type],
});
const toChainEta = status?.toChain?.chainData?.estimatedRouteDuration || 0;
const estimatedDuration = formatSeconds(Math.max(toChainEta, 0));
const body = stringGetter({
key: STRING_KEYS.DEPOSIT_STATUS,
params: {
AMOUNT_USD: `${toAmount} ${DydxChainAsset.USDC.toUpperCase()}`,
ESTIMATED_DURATION: estimatedDuration,
},
});
{error && (
<Styled.ErrorMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE:
error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
})}
</Styled.ErrorMessage>
)}
</div>
);
trigger(
txHash,
{
icon,
title,
body,
renderCustomBody: ({ isToast, notification }) => (
<TransferStatusNotification
isToast={isToast}
slotIcon={icon}
slotTitle={title}
transfer={transfer}
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title: stringGetter({ key: getTitleStringKey(type, finished) }),
description: description,
customContent: (
<TransferStatusToast
type={type}
toAmount={transfer.toAmount}
triggeredAt={transfer.triggeredAt}
notification={notification}
status={transfer.status}
/>
),
customMenuContent: !finished && (
<div>
{description}
<TransferStatusSteps status={transfer.status} type={type} />
</div>
),
toastSensitivity: 'foreground',
},
[isFinished]
[]
);
}
}, [transferNotifications, stringGetter]);
},
useNotificationAction: () => {
return () => {};
}, [transferNotifications]);
},
},
];
] satisfies NotificationTypeConfig[];
const $Icon = styled.img`
height: 1.5rem;
width: 1.5rem;
const Styled: Record<string, AnyStyledComponent> = {};
Styled.TransferText = styled.span`
display: inline-flex;
align-items: center;
gap: 0.5ch;
`;
Styled.ErrorMessage = styled.div`
max-width: 13rem;
`;
+36 -81
View File
@@ -1,13 +1,11 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { LOCAL_STORAGE_VERSIONS, LocalStorageKey } from '@/constants/localStorage';
import { LocalStorageKey } from '@/constants/localStorage';
import {
type Notification,
type NotificationDisplayData,
type NotificationPreferences,
type Notifications,
NotificationStatus,
NotificationType,
} from '@/constants/notifications';
import { useLocalStorage } from './useLocalStorage';
@@ -15,10 +13,8 @@ import { notificationTypes } from './useNotificationTypes';
import { renderSvgToDataUrl } from '../lib/renderSvgToDataUrl';
type NotificationsContextType = ReturnType<typeof useNotificationsContext>;
const NotificationsContext = createContext<NotificationsContextType>(
{} as NotificationsContextType
const NotificationsContext = createContext<ReturnType<typeof useNotificationsContext> | undefined>(
undefined
);
NotificationsContext.displayName = 'Notifications';
@@ -31,7 +27,6 @@ export const useNotifications = () => useContext(NotificationsContext)!;
const useNotificationsContext = () => {
// Local storage
// const [notifications, setNotifications] = useState<Notifications>({});
const [notifications, setNotifications] = useLocalStorage<Notifications>({
key: LocalStorageKey.Notifications,
defaultValue: {},
@@ -42,25 +37,12 @@ const useNotificationsContext = () => {
defaultValue: Date.now(),
});
const [notificationPreferences, setNotificationPreferences] =
useLocalStorage<NotificationPreferences>({
key: LocalStorageKey.NotificationPreferences,
defaultValue: {
[NotificationType.AbacusGenerated]: true,
[NotificationType.SquidTransfer]: true,
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
},
});
useEffect(() => {
setNotificationsLastUpdated(Date.now());
}, [notifications]);
const getKey = useCallback(
<T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) =>
`${notification.type}/${notification.id}`,
[]
);
const getKey = <T extends string | number>(notification: Pick<Notification<T>, 'type' | 'id'>) =>
`${notification.type}/${notification.id}`;
// Display data
const [notificationsDisplayData, setNotificationsDisplayData] = useState(
@@ -72,46 +54,29 @@ const useNotificationsContext = () => {
[notificationsDisplayData]
);
// Check for version changes
useEffect(() => {
if (
notificationPreferences.version !==
LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences]
) {
setNotificationPreferences({
[NotificationType.AbacusGenerated]: true,
[NotificationType.SquidTransfer]: true,
version: LOCAL_STORAGE_VERSIONS[LocalStorageKey.NotificationPreferences],
});
}
}, []);
// Status changes
const updateStatus = useCallback(
(notification: Notification, status: NotificationStatus) => {
notification.status = status;
notification.timestamps[notification.status] = Date.now();
setNotifications({ ...notifications, [getKey(notification)]: notification });
setNotifications({ ...notifications });
},
[notifications, getKey]
[notifications]
);
const { markUnseen, markSeen, markCleared } = useMemo(
() => ({
markUnseen: (notification: Notification) => {
if (notification.status < NotificationStatus.Unseen) {
if (notification.status < NotificationStatus.Unseen)
updateStatus(notification, NotificationStatus.Unseen);
}
},
markSeen: (notification: Notification) => {
if (notification.status < NotificationStatus.Seen) {
if (notification.status < NotificationStatus.Seen)
updateStatus(notification, NotificationStatus.Seen);
}
},
markCleared: (notification: Notification) => {
if (notification.status < NotificationStatus.Cleared) {
if (notification.status < NotificationStatus.Cleared)
updateStatus(notification, NotificationStatus.Cleared);
}
},
}),
[updateStatus]
@@ -132,37 +97,33 @@ const useNotificationsContext = () => {
const notification = notifications[key];
// Filter out notifications that are not enabled
if (notificationPreferences[type] !== false) {
// New unique key - create new notification
if (!notification) {
const notification = (notifications[key] = {
id,
type,
timestamps: {},
updateKey,
} as Notification);
updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
} else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
// updateKey changed - update existing notification
// New unique key - create new notification
if (!notification) {
const notification = (notifications[key] = {
id,
type,
timestamps: {},
updateKey,
} as Notification);
const notification = notifications[key];
updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
}
notification.updateKey = updateKey;
updateStatus(notification, NotificationStatus.Updated);
}
} else {
// Notification is disabled - remove it
delete notifications[key];
// updateKey changed - update existing notification
else if (JSON.stringify(updateKey) !== JSON.stringify(notification.updateKey)) {
const notification = notifications[key];
notification.updateKey = updateKey;
updateStatus(notification, NotificationStatus.Updated);
}
notificationsDisplayData[key] = displayData;
setNotificationsDisplayData({ ...notificationsDisplayData });
},
[notifications, updateStatus, notificationPreferences[type]]
[notifications, updateStatus]
),
lastUpdated: notificationsLastUpdated,
@@ -175,8 +136,10 @@ const useNotificationsContext = () => {
)
);
const onNotificationAction = async (notification: Notification) =>
await actions[notification.type]?.(notification.id);
const onNotificationAction = useCallback(
async (notification: Notification) => await actions[notification.type]?.(notification.id),
[actions]
);
// Push notifications
const [hasEnabledPush, setHasEnabledPush] = useLocalStorage({
@@ -223,7 +186,7 @@ const useNotificationsContext = () => {
renotify: true,
tag: getKey(notification),
data: notification,
description: displayData.body,
body: displayData.description,
icon: iconUrl ?? '/favicon.svg',
badge: iconUrl ?? '/favicon.svg',
image: iconUrl ?? '/favicon.svg',
@@ -245,7 +208,7 @@ const useNotificationsContext = () => {
setPushNotificationsLastUpdated(Date.now());
})();
}, [hasEnabledPush, notifications, markSeen]);
}, [hasEnabledPush, notifications, onNotificationAction, markSeen]);
// Menu state
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -276,13 +239,5 @@ const useNotificationsContext = () => {
// Menu state
isMenuOpen,
setIsMenuOpen,
// Notification Preferences
notificationPreferences,
setNotificationPreferences,
getNotificationPreferenceForType: useCallback(
(type: NotificationType) => notificationPreferences[type],
[notificationPreferences]
),
};
};
-6
View File
@@ -1,6 +0,0 @@
<svg width="27" height="28" viewBox="0 0 27 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Vector" d="M9.5 15L12.5 18L17.5 11M25.5 14C25.5 15.5759 25.1896 17.1363 24.5866 18.5922C23.9835 20.0481 23.0996 21.371 21.9853 22.4853C20.871 23.5996 19.5481 24.4835 18.0922 25.0866C16.6363 25.6896 15.0759 26 13.5 26C11.9241 26 10.3637 25.6896 8.9078 25.0866C7.45189 24.4835 6.12902 23.5996 5.01472 22.4853C3.90042 21.371 3.0165 20.0481 2.41345 18.5922C1.81039 17.1363 1.5 15.5759 1.5 14C1.5 10.8174 2.76428 7.76516 5.01472 5.51472C7.26516 3.26428 10.3174 2 13.5 2C16.6826 2 19.7348 3.26428 21.9853 5.51472C24.2357 7.76516 25.5 10.8174 25.5 14Z" stroke="#3ED9A4" stroke-opacity="0.5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M9.5 15L12.5 18L17.5 11" stroke="#3ED9A4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 914 B

+1 -1
View File
@@ -1,4 +1,4 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 6.39098L5.68182 10L11 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"
<path d="M2 6.39098L5.68182 10L11 2" stroke="currentColor" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 235 B

-1
View File
@@ -10,7 +10,6 @@ export { default as CautionCircleIcon } from './caution-circle.svg';
export { default as CautionCircleStrokeIcon } from './caution-circle-stroke.svg';
export { default as ChatIcon } from './chat-bubble.svg';
export { default as CheckIcon } from './check.svg';
export { default as CheckCircleIcon } from './check-circle.svg';
export { default as ChevronLeftIcon } from './chevron-left.svg';
export { default as ChevronRightIcon } from './chevron-right.svg';
export { default as ClockIcon } from './clock.svg';
+4 -12
View File
@@ -1,13 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Wallet" clip-path="url(#clip0_14122_34311)">
<g id="Group 151">
<path id="Exclude" fill-rule="evenodd" clip-rule="evenodd" d="M11.4979 10.9976C11.1764 11.3193 10.7404 11.5 10.2857 11.5L1.71429 11.5C1.25963 11.5 0.823594 11.3193 0.502102 10.9976C0.180612 10.6758 -4.76837e-07 10.2395 -4.76837e-07 9.78455L-4.76837e-07 2.06502C-4.76837e-07 2.51999 0.180612 2.95632 0.502102 3.27803C0.823594 3.59974 1.25963 3.78047 1.71429 3.78047L10.2857 3.78047C10.7404 3.78047 11.1764 3.96121 11.4979 4.28292C11.8194 4.60462 12 5.04096 12 5.49592V9.78455C12 10.2395 11.8194 10.6758 11.4979 10.9976ZM9.64294 8.7126C10.2347 8.7126 10.7144 8.23258 10.7144 7.64044C10.7144 7.0483 10.2347 6.56828 9.64294 6.56828C9.05121 6.56828 8.57151 7.0483 8.57151 7.64044C8.57151 8.23258 9.05121 8.7126 9.64294 8.7126Z" fill="#C8C7D8"/>
<path id="Vector" d="M8.57031 2.92186V2.06413C8.57031 1.60917 8.3897 1.17284 8.06821 0.851128C7.74672 0.529419 7.31068 0.348684 6.85603 0.348684L2.14174 0.348684C1.80075 0.348684 1.47372 0.484235 1.2326 0.725517C0.991486 0.966799 0.856028 1.29405 0.856028 1.63527C0.856028 1.9765 0.991486 2.30374 1.2326 2.54503C1.47372 2.78631 1.80075 2.92186 2.14174 2.92186L8.57031 2.92186Z" fill="#C8C7D8"/>
</g>
</g>
<defs>
<clipPath id="clip0_14122_34311">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g fill="#FFFFFF">
<path d="M461.2 128H80c-8.84 0-16-7.16-16-16s7.16-16 16-16h384c8.84 0 16-7.16 16-16 0-26.51-21.49-48-48-48H64C28.65 32 0 60.65 0 96v320c0 35.35 28.65 64 64 64h397.2c28.02 0 50.8-21.53 50.8-48V176c0-26.47-22.78-48-50.8-48zM416 336c-17.67 0-32-14.33-32-32s14.33-32 32-32 32 14.33 32 32-14.33 32-32 32z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 405 B

-4
View File
@@ -15,7 +15,6 @@ import { ExternalNavKeplrDialog } from '@/views/dialogs/ExternalNavKeplrDialog';
import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog';
import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog';
import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
import { PreferencesDialog } from '@/views/dialogs/PreferencesDialog';
import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog';
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog';
@@ -23,7 +22,6 @@ import { TradeDialog } from '@/views/dialogs/TradeDialog';
import { TransferDialog } from '@/views/dialogs/TransferDialog';
import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog';
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
import { ManageFundsDialog } from '@/views/dialogs/ManageFundsDialog';
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
@@ -58,7 +56,6 @@ export const DialogManager = () => {
[DialogTypes.MobileSignIn]: <MobileSignInDialog {...modalProps} />,
[DialogTypes.Onboarding]: <OnboardingDialog {...modalProps} />,
[DialogTypes.OrderDetails]: <OrderDetailsDialog {...modalProps} />,
[DialogTypes.Preferences]: <PreferencesDialog {...modalProps} />,
[DialogTypes.RateLimit]: <RateLimitDialog {...modalProps} />,
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
[DialogTypes.RestrictedGeo]: <RestrictedGeoDialog {...modalProps} />,
@@ -66,6 +63,5 @@ export const DialogManager = () => {
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
[DialogTypes.ManageFunds]: <ManageFundsDialog {...modalProps} />,
}[type];
};
+37 -64
View File
@@ -1,22 +1,19 @@
import { useMemo } from 'react';
import styled, { css } from 'styled-components';
import { NotificationStatus } from '@/constants/notifications';
import { ButtonSize } from '@/constants/buttons';
import { useNotifications } from '@/hooks/useNotifications';
import { useBreakpoints } from '@/hooks/useBreakpoints';
import { Button } from '@/components/Button';
import { Toast } from '@/components/Toast';
import { ToastArea } from '@/components/ToastArea';
import { Toast } from '@/components/Toast';
import { Button } from '@/components/Button';
import styled from 'styled-components';
type StyleProps = {
className?: string;
};
const MAX_TOASTS = 10;
export const NotificationsToastArea = ({ className }: StyleProps) => {
const {
notifications,
@@ -29,9 +26,10 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
} = useNotifications();
const { isMobile } = useBreakpoints();
const notificationMap = useMemo(() => {
return (
Object.values(notifications)
return (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{Object.values(notifications)
// Sort by time of first trigger
.sort(
(n1, n2) =>
@@ -44,69 +42,44 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
displayData: getDisplayData(notification),
}))
.filter(({ displayData }) => displayData)
.slice(-MAX_TOASTS)
);
}, [notifications, getKey, getDisplayData]);
if (isMenuOpen) return null;
return (
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
{notificationMap.map(({ notification, key, displayData }, idx) => (
<StyledToast
key={key}
layer={notificationMap.length - 1 - idx}
isOpen={notification.status < NotificationStatus.Unseen}
notification={notification}
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotTitleLeft={displayData.slotTitleLeft}
slotTitleRight={displayData.slotTitleRight}
slotDescription={displayData.body}
slotCustomContent={displayData.renderCustomBody?.({ isToast: true, notification })}
onClick={() => onNotificationAction(notification)}
slotAction={
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
{displayData.actionDescription}
</Button>
}
actionDescription={displayData.actionDescription}
actionAltText={displayData.actionAltText}
duration={displayData.toastDuration ?? Infinity}
sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen, isClosedFromTimeout) => {
if (!isOpen)
if (isClosedFromTimeout)
// Toast timer expired without user interaction
markUnseen(notification);
// Toast interacted with or dismissed
else markSeen(notification);
}}
lastUpdated={notification.timestamps[notification.status]}
/>
))}
.map(({ notification, key, displayData }) => (
<Toast
key={key}
isOpen={notification.status < NotificationStatus.Unseen}
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotDescription={displayData.description}
slotCustomContent={displayData.customContent}
slotAction={
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
{displayData.actionDescription}
</Button>
}
actionDescription={displayData.actionDescription}
actionAltText={displayData.actionAltText}
duration={isMenuOpen ? Infinity : displayData.toastDuration}
sensitivity={displayData.toastSensitivity}
setIsOpen={(isOpen, isClosedFromTimeout) => {
if (!isOpen)
if (isClosedFromTimeout)
// Toast timer expired without user interaction
markUnseen(notification);
// Toast interacted with or dismissed
else markSeen(notification);
}}
lastUpdated={notification.timestamps[notification.status]}
/>
))}
</StyledToastArea>
);
};
const StyledToastArea = styled(ToastArea)`
position: absolute;
width: min(17.5rem, 100%);
width: min(16rem, 100%);
inset: 0 0 0 auto;
padding: 0.75rem 0.75rem 0.75rem 0;
mask-image: linear-gradient(to left, transparent, white 0.5rem);
`;
const StyledToast = styled(Toast)<{ layer: number }>`
// Stacked toast
position: absolute;
top: 0;
left: 0;
right: 0;
${({ layer }) => css`
right: calc(${layer} * -2px);
top: calc(${layer} * 2px);
`}
`;
+7 -12
View File
@@ -1,7 +1,6 @@
import Abacus, { type Nullable } from '@dydxprotocol/v4-abacus';
import Long from 'long';
import type { IndexedTx } from '@cosmjs/stargate';
import { encodeJson } from '@dydxprotocol/v4-client-js';
import {
CompositeClient,
@@ -196,9 +195,8 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
throw new StatefulOrderError('Stateful order has failed to commit.', tx);
}
const encodedTx = encodeJson(tx);
const parsedTx = JSON.parse(encodedTx);
const hash = parsedTx.hash.toUpperCase();
const parsedTx = this.parseToPrimitives(tx);
const hash = parsedTx?.hash;
if (isTestnet) {
console.log(
@@ -208,7 +206,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
);
} else console.log(`txHash: ${hash}`);
return encodedTx;
return JSON.stringify(parsedTx);
} catch (error) {
if (error?.name !== 'BroadcastError') {
log('DydxChainTransactions/placeOrderTransaction', error);
@@ -238,14 +236,9 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
goodTilBlockTime || undefined
);
const encodedTx = encodeJson(tx);
const parsedTx = this.parseToPrimitives(tx);
if (import.meta.env.MODE === 'development') {
const parsedTx = JSON.parse(encodedTx);
console.log(parsedTx, parsedTx.hash.toUpperCase());
}
return encodedTx;
return JSON.stringify(parsedTx);
} catch (error) {
log('DydxChainTransactions/cancelOrderTransaction', error);
@@ -441,6 +434,8 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const parseDelegations = this.parseToPrimitives(delegations);
callback(JSON.stringify(parseDelegations));
break;
// Do not implement Transfers (yet)
case QueryType.Transfers:
default:
break;
}
-2
View File
@@ -35,7 +35,6 @@ import { setApiState } from '@/state/app';
import { setAssets } from '@/state/assets';
import { setConfigs } from '@/state/configs';
import { setInputs } from '@/state/inputs';
import { updateNotifications } from '@/state/notifications';
import { setHistoricalFundings, setLiveTrades, setMarkets, setOrderbook } from '@/state/perpetuals';
import { isTruthy } from '../isTruthy';
@@ -51,7 +50,6 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
}
notificationsChanged(notifications: kollections.List<AbacusNotification>): void {
this.store?.dispatch(updateNotifications(notifications.toArray()));
return;
}
+5 -8
View File
@@ -51,14 +51,11 @@ const Profile = () => {
icon: { iconName: IconName.Gear },
href: AppRoute.Settings,
},
isConnected && {
key: 'transfers',
label: stringGetter({ key: STRING_KEYS.MANAGE_FUNDS }),
icon: { iconName: IconName.Transfer },
onClick: () => {
dispatch(openDialog({ type: DialogTypes.ManageFunds }));
},
},
// {
// key: 'tutorials',
// label: stringGetter({ key: STRING_KEYS.TUTORIALS }),
// icon: { iconName: IconName.Comment },
// },
isConnected
? {
key: 'sign-out',
File diff suppressed because it is too large Load Diff
+4
View File
@@ -304,6 +304,10 @@ Styled.FeeTable = styled(Table)`
@media ${breakpoints.notTablet} {
--tableHeader-backgroundColor: var(--color-layer-1);
thead tr {
--border-width: 0;
}
}
`;
+42 -104
View File
@@ -1,13 +1,11 @@
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { Navigate, Route, Routes } from 'react-router-dom';
import { OnboardingState } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { HistoryRoute, PortfolioRoute } from '@/constants/routes';
import { useAccountBalance, useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
import { useBreakpoints, useDocumentTitle, useStringGetter } from '@/hooks';
import { FillsTable, FillsTableColumnKey } from '@/views/tables/FillsTable';
import { FundingPaymentsTable } from '@/views/tables/FundingPaymentsTable';
@@ -16,9 +14,6 @@ import { Icon, IconName } from '@/components/Icon';
import { NavigationMenu } from '@/components/NavigationMenu';
import { WithSidebar } from '@/components/WithSidebar';
import { getOnboardingState, getSubaccount } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { PortfolioNavMobile } from './PortfolioNavMobile';
import { Overview } from './Overview';
import { Positions } from './Positions';
@@ -27,20 +22,11 @@ import { Fees } from './Fees';
import { History } from './History';
import { layoutMixins } from '@/styles/layoutMixins';
import { Button } from '@/components/Button';
import { ButtonAction } from '@/constants/buttons';
export default () => {
const dispatch = useDispatch();
const stringGetter = useStringGetter();
const { isTablet, isNotTablet } = useBreakpoints();
const onboardingState = useSelector(getOnboardingState);
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
const { nativeTokenBalance } = useAccountBalance();
const usdcBalance = freeCollateral?.current || 0;
useDocumentTitle(stringGetter({ key: STRING_KEYS.PORTFOLIO }));
const routesComponent = (
@@ -98,74 +84,46 @@ export default () => {
<WithSidebar
sidebar={
isTablet ? null : (
<Styled.SideBar>
<Styled.NavigationMenu
items={[
{
group: 'views',
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
items: [
{
value: PortfolioRoute.Overview,
slotBefore: <Styled.Icon iconName={IconName.Overview} />,
label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
href: PortfolioRoute.Overview,
},
{
value: PortfolioRoute.Positions,
slotBefore: <Styled.Icon iconName={IconName.Cube} />,
label: stringGetter({ key: STRING_KEYS.POSITIONS }),
href: PortfolioRoute.Positions,
},
{
value: PortfolioRoute.Orders,
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
label: stringGetter({ key: STRING_KEYS.ORDERS }),
href: PortfolioRoute.Orders,
},
{
value: PortfolioRoute.Fees,
slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
label: stringGetter({ key: STRING_KEYS.FEES }),
href: PortfolioRoute.Fees,
},
{
value: PortfolioRoute.History,
slotBefore: <Styled.Icon iconName={IconName.History} />,
label: stringGetter({ key: STRING_KEYS.HISTORY }),
href: PortfolioRoute.History,
},
],
},
]}
/>
{onboardingState === OnboardingState.AccountConnected && (
<Styled.Footer>
<Button
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT })}
</Button>
{usdcBalance > 0 && (
<Button
action={ButtonAction.Base}
onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
)}
{(usdcBalance > 0 || nativeTokenBalance.gt(0)) && (
<Button
action={ButtonAction.Base}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
)}
</Styled.Footer>
)}
</Styled.SideBar>
<Styled.NavigationMenu
items={[
{
group: 'views',
groupLabel: stringGetter({ key: STRING_KEYS.VIEWS }),
items: [
{
value: PortfolioRoute.Overview,
slotBefore: <Styled.Icon iconName={IconName.Overview} />,
label: stringGetter({ key: STRING_KEYS.OVERVIEW }),
href: PortfolioRoute.Overview,
},
{
value: PortfolioRoute.Positions,
slotBefore: <Styled.Icon iconName={IconName.Cube} />,
label: stringGetter({ key: STRING_KEYS.POSITIONS }),
href: PortfolioRoute.Positions,
},
{
value: PortfolioRoute.Orders,
slotBefore: <Styled.Icon iconName={IconName.OrderPending} />,
label: stringGetter({ key: STRING_KEYS.ORDERS }),
href: PortfolioRoute.Orders,
},
{
value: PortfolioRoute.Fees,
slotBefore: <Styled.Icon iconName={IconName.Calculator} />,
label: stringGetter({ key: STRING_KEYS.FEES }),
href: PortfolioRoute.Fees,
},
{
value: PortfolioRoute.History,
slotBefore: <Styled.Icon iconName={IconName.History} />,
label: stringGetter({ key: STRING_KEYS.HISTORY }),
href: PortfolioRoute.History,
},
],
},
]}
/>
)
}
>
@@ -181,26 +139,6 @@ Styled.PortfolioMobile = styled.div`
${layoutMixins.expandingColumnWithHeader}
`;
Styled.SideBar = styled.div`
${layoutMixins.flexColumn}
justify-content: space-between;
height: 100%;
`;
Styled.Footer = styled.div`
${layoutMixins.row}
flex-wrap: wrap;
padding: 1rem;
gap: 0.5rem;
> button {
flex-grow: 1;
}
`;
Styled.NavigationMenu = styled(NavigationMenu)`
padding: 0.5rem;
padding-top: 0;
+17 -8
View File
@@ -43,14 +43,23 @@ export const DYDXBalancePanel = () => {
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Small} />
) : (
<Button
slotLeft={<Icon iconName={IconName.Send} />}
size={ButtonSize.Small}
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
<>
<Styled.ReceiveButton
slotLeft={<Icon iconName={IconName.Qr} />}
size={ButtonSize.Small}
onClick={() => dispatch(openDialog({ type: DialogTypes.Receive }))}
>
{stringGetter({ key: STRING_KEYS.RECEIVE })}
</Styled.ReceiveButton>
<Button
slotLeft={<Icon iconName={IconName.Send} />}
size={ButtonSize.Small}
action={ButtonAction.Primary}
onClick={() => dispatch(openDialog({ type: DialogTypes.Transfer }))}
>
{stringGetter({ key: STRING_KEYS.TRANSFER })}
</Button>
</>
)}
</Styled.ReceiveAndTransferButtons>
</Styled.Header>
-3
View File
@@ -1,7 +1,6 @@
import { configureStore } from '@reduxjs/toolkit';
import abacusStateManager from '@/lib/abacus';
import appMiddleware from './appMiddleware';
import localizationMiddleware from './localizationMiddleware';
@@ -14,7 +13,6 @@ import { inputsSlice } from './inputs';
import { layoutSlice } from './layout';
import { localizationSlice } from './localization';
import { perpetualsSlice } from './perpetuals';
import { notificationsSlice } from './notifications';
export const commandMenuSlices = [layoutSlice, localizationSlice];
@@ -28,7 +26,6 @@ export const store = configureStore({
inputs: inputsSlice.reducer,
layout: layoutSlice.reducer,
localization: localizationSlice.reducer,
notifications: notificationsSlice.reducer,
perpetuals: perpetualsSlice.reducer,
},
+4 -9
View File
@@ -1,10 +1,5 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import {
LOCALE_DATA,
NOTIFICATIONS,
SupportedLocale,
TOOLTIPS,
} from '@dydxprotocol/v4-localization';
import { LOCALE_DATA, SupportedLocale, TOOLTIPS } from '@dydxprotocol/v4-localization';
import {
type LocaleData,
@@ -31,7 +26,6 @@ const getNewLocaleData = ({
const newLocaleData = {
...LOCALE_DATA[locale],
...NOTIFICATIONS[locale],
TOOLTIPS: TOOLTIPS[locale],
};
@@ -57,8 +51,9 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
} else if (globalThis.navigator?.language) {
const browserLanguageBaseTag = globalThis.navigator.language.split('-')[0].toLowerCase();
const locale = (SUPPORTED_BASE_TAGS_LOCALE_MAPPING[browserLanguageBaseTag] ??
SUPPORTED_BASE_TAGS_LOCALE_MAPPING[SupportedLocales.EN]) as SupportedLocales;
const locale = SUPPORTED_BASE_TAGS_LOCALE_MAPPING[
browserLanguageBaseTag
] as SupportedLocales;
if (locale) {
store.dispatch(setSelectedLocale({ locale, isAutoDetect: true }));
-24
View File
@@ -1,24 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { AbacusNotification } from '@/constants/abacus';
export interface NotificationsState {
abacusNotifications: AbacusNotification[];
}
const initialState: NotificationsState = {
abacusNotifications: [],
};
export const notificationsSlice = createSlice({
name: 'Notifications',
initialState,
reducers: {
updateNotifications: (state, action: PayloadAction<AbacusNotification[]>) => {
state.abacusNotifications = action.payload;
},
},
});
export const { updateNotifications } = notificationsSlice.actions;
-3
View File
@@ -1,3 +0,0 @@
import { RootState } from './_store';
export const getAbacusNotifications = (state: RootState) => state.notifications.abacusNotifications;
-29
View File
@@ -107,17 +107,12 @@ export const formMixins: Record<
`,
inputLabel: css`
--label-textColor: var(--color-text-1);
position: relative;
height: 100%;
width: 100%;
gap: 0;
border-radius: inherit;
@media ${breakpoints.tablet} {
gap: 0.25rem;
}
`,
inputToggleGroup: css`
@@ -135,28 +130,4 @@ export const formMixins: Record<
--button-toggle-off-textColor: var(--color-text-1);
}
`,
footer: css`
${layoutMixins.stickyFooter}
${layoutMixins.withStickyFooterBackdrop}
margin-top: auto;
`,
transfersForm: css`
${() => formMixins.inputsColumn}
--form-input-gap: 1.25rem;
--form-input-height: 3.5rem;
--form-input-height-mobile: 4rem;
--form-input-paddingY: 0.5rem;
--form-input-paddingX: 1rem;
label {
--label-textColor: var(--color-text-0);
}
@media ${breakpoints.tablet} {
--form-input-gap: 1rem;
}
`,
};
+2 -2
View File
@@ -12,8 +12,8 @@ export const headerMixins = {
--trigger-open-textColor: var(--color-text-2);
&:hover:not(:disabled) {
--trigger-backgroundColor: var(--trigger-hover-backgroundColor);
--trigger-textColor: var(--trigger-hover-textColor);
background-color: var(--trigger-hover-backgroundColor);
color: var(--trigger-hover-textColor);
}
`,
+4
View File
@@ -18,5 +18,9 @@ export const tradeViewMixins: Record<
tbody {
font: var(--font-small-book);
}
thead tr {
box-shadow: none;
}
`,
};
+5 -5
View File
@@ -82,18 +82,18 @@ const MarketsDropdownContent = ({ onRowAction }: { onRowAction?: (market: string
),
},
{
columnKey: 'priceChange24HPercent',
getCellValue: (row) => row.priceChange24HPercent,
columnKey: 'priceChange24H',
getCellValue: (row) => row.priceChange24H,
label: stringGetter({ key: STRING_KEYS._24H }),
renderCell: ({ priceChange24HPercent }) => (
renderCell: ({ priceChange24H, priceChange24HPercent }) => (
<Styled.InlineRow>
{!priceChange24HPercent ? (
{!priceChange24H ? (
<Styled.Output type={OutputType.Text} value={null} />
) : (
<Styled.PriceChangeOutput
type={OutputType.Percent}
value={priceChange24HPercent}
isNegative={MustBigNumber(priceChange24HPercent).isNegative()}
isNegative={MustBigNumber(priceChange24H).isNegative()}
/>
)}
</Styled.InlineRow>
+40 -49
View File
@@ -1,7 +1,6 @@
import styled from 'styled-components';
import { AbacusOrderStatus } from '@/constants/abacus';
import { type OrderStatus, AbacusOrderStatus } from '@/constants/abacus';
import { Icon } from '@/components/Icon';
import {
OrderCanceledIcon,
OrderFilledIcon,
@@ -10,57 +9,49 @@ import {
OrderPendingIcon,
} from '@/icons';
import { Icon } from '@/components/Icon';
import styled from 'styled-components';
type ElementProps = {
status: string;
status: OrderStatus;
totalFilled: number;
};
type StyleProps = {
className?: string;
};
export const OrderStatusIcon = ({ className, status, totalFilled }: ElementProps & StyleProps) => {
const { iconComponent, color } = {
[AbacusOrderStatus.open.rawValue]:
totalFilled > 0
? {
iconComponent: OrderPartiallyFilledIcon,
color: 'var(--color-warning)',
}
: {
iconComponent: OrderOpenIcon,
color: 'var(--color-text-2)',
},
[AbacusOrderStatus.partiallyFilled.rawValue]: {
iconComponent: OrderPartiallyFilledIcon,
color: 'var(--color-warning)',
},
[AbacusOrderStatus.filled.rawValue]: {
iconComponent: OrderFilledIcon,
color: 'var(--color-success)',
},
[AbacusOrderStatus.cancelled.rawValue]: {
iconComponent: OrderCanceledIcon,
color: 'var(--color-error)',
},
[AbacusOrderStatus.canceling.rawValue]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-error)',
},
[AbacusOrderStatus.pending.rawValue]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-text-2)',
},
[AbacusOrderStatus.untriggered.rawValue]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-text-2)',
},
}[status];
return <$Icon className={className} iconComponent={iconComponent} color={color} />;
};
export const OrderStatusIcon = ({ status, totalFilled }: ElementProps) => (
<$Icon
{...{
[AbacusOrderStatus.open.name]:
totalFilled > 0
? {
iconComponent: OrderPartiallyFilledIcon,
color: 'var(--color-warning)',
}
: {
iconComponent: OrderOpenIcon,
color: 'var(--color-text-2)',
},
[AbacusOrderStatus.filled.name]: {
iconComponent: OrderFilledIcon,
color: 'var(--color-success)',
},
[AbacusOrderStatus.cancelled.name]: {
iconComponent: OrderCanceledIcon,
color: 'var(--color-error)',
},
[AbacusOrderStatus.canceling.name]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-error)',
},
[AbacusOrderStatus.pending.name]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-text-2)',
},
[AbacusOrderStatus.untriggered.name]: {
iconComponent: OrderPendingIcon,
color: 'var(--color-text-2)',
},
}[status.name]}
/>
);
const $Icon = styled(Icon)<{ color: string }>`
color: ${({ color }) => color};
+185
View File
@@ -0,0 +1,185 @@
import { useCallback, useState, useMemo } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { Root, Trigger, Content } from '@radix-ui/react-collapsible';
import { StatusResponse } from '@0xsquid/sdk';
import { useInterval, useStringGetter } from '@/hooks';
import { STRING_KEYS } from '@/constants/localization';
import { AlertType } from '@/constants/alerts';
import { formatSeconds } from '@/lib/timeUtils';
import { AlertMessage } from '@/components/AlertMessage';
import { Output, OutputType } from '@/components/Output';
import { WithReceipt } from '@/components/WithReceipt';
import { Icon, IconName } from '@/components/Icon';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { LoadingDots } from '@/components/Loading/LoadingDots';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
type: 'withdrawal' | 'deposit';
toAmount?: number;
triggeredAt?: number;
status?: StatusResponse;
};
export const TransferStatusToast = ({
type,
toAmount,
triggeredAt = Date.now(),
status,
}: ElementProps) => {
const stringGetter = useStringGetter();
const [open, setOpen] = useState<boolean>(false);
const [secondsLeft, setSecondsLeft] = useState<number>(0);
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
const updateSecondsLeft = useCallback(() => {
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration || 0) * 1000;
setSecondsLeft(Math.floor((triggeredAt + fromChainEta + toChainEta - Date.now()) / 1000));
}, [status]);
useInterval({ callback: updateSecondsLeft });
if (!status) return <LoadingDots size={3} />;
const inProgressStatusString =
type === 'deposit'
? secondsLeft > 0
? STRING_KEYS.DEPOSIT_STATUS
: STRING_KEYS.DEPOSIT_STATUS_SHORTLY
: secondsLeft > 0
? STRING_KEYS.WITHDRAW_STATUS
: STRING_KEYS.WITHDRAW_STATUS_SHORTLY;
const statusString =
type === 'deposit'
? status?.squidTransactionStatus === 'success'
? STRING_KEYS.DEPOSIT_COMPLETE
: inProgressStatusString
: status?.squidTransactionStatus === 'success'
? STRING_KEYS.WITHDRAW_COMPLETE
: inProgressStatusString;
return (
<Styled.Root open={open} onOpenChange={setOpen}>
<WithReceipt
hideReceipt={!open}
side="bottom"
slotReceipt={
<Styled.Receipt>
<TransferStatusSteps status={status} type={type} />
</Styled.Receipt>
}
>
<Styled.BridgingStatus>
<Styled.Status>
{stringGetter({
key: statusString,
params: {
AMOUNT_USD: <Styled.InlineOutput type={OutputType.Fiat} value={toAmount} />,
ESTIMATED_DURATION: (
<Styled.InlineOutput
type={OutputType.Text}
value={formatSeconds(Math.max(secondsLeft || 0, 0))}
/>
),
},
})}
</Styled.Status>
{error && (
<AlertMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
})}
</AlertMessage>
)}
<Styled.Trigger>
<Styled.TriggerIcon>
<Icon iconName={IconName.Caret} />
</Styled.TriggerIcon>
{stringGetter({ key: open ? STRING_KEYS.HIDE_DETAILS : STRING_KEYS.VIEW_DETAILS })}
</Styled.Trigger>
</Styled.BridgingStatus>
</WithReceipt>
</Styled.Root>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Root = styled(Root)`
margin: 0.5rem -1rem -1rem -1rem;
color: var(--color-text-0);
font: var(--font-small-book);
`;
Styled.Receipt = styled.div`
padding: 0 1rem;
`;
Styled.BridgingStatus = styled.div`
${layoutMixins.flexColumn};
background-color: var(--color-layer-3);
gap: 0.5rem;
padding: 0 1rem 1rem 1rem;
border-radius: 0.5rem;
`;
Styled.Status = styled.div`
color: var(--color-text-0);
font-size: 0.875rem;
`;
Styled.InlineOutput = styled(Output)`
display: inline-block;
color: var(--color-text-1);
`;
Styled.Content = styled(Content)`
${layoutMixins.flexColumn};
gap: 0.5rem;
padding: 1rem;
`;
Styled.Step = styled.div`
${layoutMixins.row};
gap: 0.5rem;
`;
Styled.Trigger = styled(Trigger)`
display: flex;
align-items: center;
gap: 0.5em;
color: var(--color-accent);
user-select: none;
&:focus {
outline: none;
}
`;
Styled.TriggerIcon = styled.span`
width: 0.75em;
display: inline-flex;
transition: rotate 0.3s var(--ease-out-expo);
${Styled.Trigger}[data-state='open'] & {
rotate: -0.5turn;
}
`;
@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components';
import { StatusResponse } from '@0xsquid/sdk';
import { useStringGetter, useSelectedNetwork, useURLConfigs } from '@/hooks';
@@ -18,10 +18,6 @@ type ElementProps = {
type: 'withdrawal' | 'deposit';
};
type StyleProps = {
className?: string;
};
enum TransferStatusStep {
FromChain,
Bridge,
@@ -29,7 +25,7 @@ enum TransferStatusStep {
Complete,
}
export const TransferStatusSteps = ({ className, status, type }: ElementProps & StyleProps) => {
export const TransferStatusSteps = ({ status, type }: ElementProps) => {
const stringGetter = useStringGetter();
const { selectedNetwork } = useSelectedNetwork();
const { mintscan: mintscanTxUrl } = useURLConfigs();
@@ -49,12 +45,7 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
}),
step: TransferStatusStep.FromChain,
link:
type === 'deposit'
? status?.fromChain?.transactionUrl
: routeStatus?.[0]?.chainId === dydxChainId && routeStatus[0].txHash
? `${mintscanTxUrl?.replace('{tx_hash}', routeStatus[0].txHash.replace('0x', ''))}`
: undefined,
link: status?.fromChain?.transactionUrl,
},
{
label: stringGetter({ key: STRING_KEYS.BRIDGING_TOKENS }),
@@ -73,7 +64,7 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
type === 'withdrawal'
? status?.toChain?.transactionUrl
: currentStatus?.chainId === dydxChainId && currentStatus?.txHash
? `${mintscanTxUrl?.replace('{tx_hash}', currentStatus.txHash.replace('0x', ''))}`
? `${mintscanTxUrl?.replace('{tx_hash}', currentStatus.txHash)}`
: undefined,
},
];
@@ -105,7 +96,7 @@ export const TransferStatusSteps = ({ className, status, type }: ElementProps &
if (!status) return <LoadingDots size={3} />;
return (
<Styled.BridgingStatus className={className}>
<Styled.BridgingStatus>
{steps.map((step) => (
<Styled.Step key={step.step}>
<Styled.row>
+46 -2
View File
@@ -1,15 +1,23 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { isMainnet } from '@/constants/networks';
import { STRING_KEYS } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { Dialog, DialogPlacement } from '@/components/Dialog';
import { DepositDialogContent } from './DepositDialog/DepositDialogContent';
import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
setIsOpen?: (open: boolean) => void;
};
export const DepositDialog = ({ setIsOpen }: ElementProps) => {
const [showFaucet, setShowFaucet] = useState(!isMainnet);
const stringGetter = useStringGetter();
const { isMobile } = useBreakpoints();
@@ -20,9 +28,45 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
isOpen
setIsOpen={setIsOpen}
title={stringGetter({ key: STRING_KEYS.DEPOSIT })}
description={showFaucet && 'Test funds will be sent directly to your dYdX account.'}
placement={isMobile ? DialogPlacement.FullScreen : DialogPlacement.Default}
>
<DepositDialogContent onDeposit={closeDialog} />
<Styled.Content>
{isMainnet || !showFaucet ? (
<DepositForm />
) : (
<TestnetDepositForm onDeposit={closeDialog} />
)}
{!isMainnet && (
<Styled.TextToggle onClick={() => setShowFaucet(!showFaucet)}>
{showFaucet ? 'Show deposit form' : 'Show test faucet'}
</Styled.TextToggle>
)}
</Styled.Content>
</Dialog>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.TextToggle = styled.div`
${layoutMixins.stickyFooter}
color: var(--color-accent);
cursor: pointer;
margin-top: auto;
&:hover {
text-decoration: underline;
}
`;
Styled.Content = styled.div`
${layoutMixins.stickyArea0}
--stickyArea0-bottomHeight: 2rem;
--stickyArea0-bottomGap: 1rem;
--stickyArea0-totalInsetBottom: 0.5rem;
${layoutMixins.flexColumn}
gap: 1rem;
`;
@@ -1,56 +0,0 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { isMainnet } from '@/constants/networks';
import { layoutMixins } from '@/styles/layoutMixins';
import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
type ElementProps = {
onDeposit?: () => void;
};
export const DepositDialogContent = ({ onDeposit }: ElementProps) => {
const [showFaucet, setShowFaucet] = useState(!isMainnet);
return (
<Styled.Content>
{isMainnet || !showFaucet ? (
<DepositForm onDeposit={onDeposit} />
) : (
<TestnetDepositForm onDeposit={onDeposit} />
)}
{!isMainnet && (
<Styled.TextToggle onClick={() => setShowFaucet(!showFaucet)}>
{showFaucet ? 'Show deposit form' : 'Show test faucet'}
</Styled.TextToggle>
)}
</Styled.Content>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Content = styled.div`
${layoutMixins.flexColumn}
gap: 1rem;
form {
flex: 1;
}
`;
Styled.TextToggle = styled.div`
${layoutMixins.stickyFooter}
--stickyArea-bottomHeight: 0;
color: var(--color-accent);
cursor: pointer;
margin-top: auto;
&:hover {
text-decoration: underline;
}
`;
-86
View File
@@ -1,86 +0,0 @@
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { ButtonSize } from '@/constants/buttons';
import { useStringGetter } from '@/hooks';
import { Dialog, DialogPlacement } from '@/components/Dialog';
import { ToggleGroup } from '@/components/ToggleGroup';
import { TransferForm } from '@/views/forms/TransferForm';
import { WithdrawForm } from '@/views/forms/AccountManagementForms/WithdrawForm';
import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { DepositDialogContent } from './DepositDialog/DepositDialogContent';
type ElementProps = {
setIsOpen?: (open: boolean) => void;
};
export const ManageFundsDialog = ({ setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const { type } = useSelector(getTransferInputs, shallowEqual) || {};
const currentType = type?.rawValue ?? TransferType.deposit.rawValue;
const closeDialog = () => setIsOpen?.(false);
const transferTypeConfig = {
[TransferType.deposit.rawValue]: {
value: TransferType.deposit.rawValue,
label: stringGetter({ key: STRING_KEYS.DEPOSIT }),
component: <DepositDialogContent />,
},
[TransferType.withdrawal.rawValue]: {
value: TransferType.withdrawal.rawValue,
label: stringGetter({ key: STRING_KEYS.WITHDRAW }),
component: <WithdrawForm />,
},
[TransferType.transferOut.rawValue]: {
value: TransferType.transferOut.rawValue,
label: stringGetter({ key: STRING_KEYS.TRANSFER }),
component: <TransferForm onDone={closeDialog} />,
},
};
return (
<Styled.Dialog
isOpen
setIsOpen={setIsOpen}
placement={DialogPlacement.FullScreen}
title={
<Styled.ToggleGroup
items={Object.values(transferTypeConfig)}
value={currentType}
size={ButtonSize.Medium}
onValueChange={(value: string) =>
abacusStateManager.setTransferValue({
field: TransferInputField.type,
value,
})
}
/>
}
hasHeaderBorder
>
{transferTypeConfig[currentType].component}
</Styled.Dialog>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)`
--dialog-content-paddingTop: 1.5rem;
`;
Styled.ToggleGroup = styled(ToggleGroup)`
overflow-x: auto;
button {
--button-toggle-off-border: none;
--button-padding: 0 1rem;
}
`;
+11 -7
View File
@@ -5,7 +5,7 @@ import { AlertType } from '@/constants/alerts';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { useAccounts, useStringGetter } from '@/hooks';
import breakpoints from '@/styles/breakpoints';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
import { AlertMessage } from '@/components/AlertMessage';
@@ -54,14 +54,11 @@ export const MnemonicExportDialog = ({ setIsOpen }: ElementProps) => {
<Styled.WithReceipt
slotReceipt={
<Styled.CheckboxContainer>
<Checkbox
<Styled.Checkbox
checked={hasAcknowledged}
onCheckedChange={setHasAcknowledged}
id="acknowledge-secret-phase-risk"
label={stringGetter({
key: STRING_KEYS.SECRET_PHRASE_RISK_ACK,
})}
onClick={() => setHasAcknowledged(!hasAcknowledged)}
/>
<span>{stringGetter({ key: STRING_KEYS.SECRET_PHRASE_RISK_ACK })}</span>
</Styled.CheckboxContainer>
}
>
@@ -165,10 +162,17 @@ Styled.WithReceipt = styled(WithReceipt)`
`;
Styled.CheckboxContainer = styled.div`
${layoutMixins.spacedRow};
padding: 1rem;
gap: 0.75rem;
color: var(--color-text-0);
`;
Styled.Checkbox = styled(Checkbox)`
--checkbox-backgroundColor: var(--color-layer-3);
`;
Styled.AlertMessage = styled(AlertMessage)`
font: var(--font-base-book);
margin: 0;
+11 -18
View File
@@ -3,7 +3,6 @@ import styled, { AnyStyledComponent, css } from 'styled-components';
import { AnalyticsEvent } from '@/constants/analytics';
import { STRING_KEYS } from '@/constants/localization';
import { isMainnet } from '@/constants/networks';
import { EvmDerivedAccountStatus, OnboardingSteps } from '@/constants/account';
import { wallets } from '@/constants/wallets';
@@ -27,7 +26,6 @@ import { track } from '@/lib/analytics';
import { AcknowledgeTerms } from './OnboardingDialog/AcknowledgeTerms';
import { ChooseWallet } from './OnboardingDialog/ChooseWallet';
import { GenerateKeys } from './OnboardingDialog/GenerateKeys';
import { DepositForm } from '../forms/AccountManagementForms/DepositForm';
type ElementProps = {
setIsOpen?: (open: boolean) => void;
@@ -52,7 +50,7 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
disconnect();
}
setIsOpen?.(open);
};
}
return (
<Styled.Dialog
@@ -84,7 +82,10 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
description: stringGetter({ key: STRING_KEYS.SIGNATURE_CREATES_COSMOS_WALLET }),
children: (
<Styled.Content>
<GenerateKeys status={derivationStatus} setStatus={setDerivationStatus} />
<GenerateKeys
status={derivationStatus}
setStatus={setDerivationStatus}
/>
</Styled.Content>
),
width: '23rem',
@@ -100,22 +101,14 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
},
[OnboardingSteps.DepositFunds]: {
title: stringGetter({ key: STRING_KEYS.DEPOSIT }),
description: !isMainnet && 'Test funds will be sent directly to your dYdX account.',
description: 'Test funds will be sent directly to your dYdX account.',
children: (
<Styled.Content>
{isMainnet ? (
<DepositForm
onDeposit={() => {
track(AnalyticsEvent.TransferDeposit);
}}
/>
) : (
<TestnetDepositForm
onDeposit={() => {
track(AnalyticsEvent.TransferFaucet);
}}
/>
)}
<TestnetDepositForm
onDeposit={() => {
track(AnalyticsEvent.TransferFaucet);
}}
/>
</Styled.Content>
),
},
-86
View File
@@ -1,86 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import styled, { AnyStyledComponent } from 'styled-components';
import { STRING_KEYS } from '@/constants/localization';
import { NotificationType } from '@/constants/notifications';
import { useStringGetter } from '@/hooks';
import { useNotifications } from '@/hooks/useNotifications';
import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu';
import { Switch } from '@/components/Switch';
export const usePreferenceMenu = () => {
const stringGetter = useStringGetter();
// Notifications
const { notificationPreferences, setNotificationPreferences } = useNotifications();
const [enabledNotifs, setEnabledNotifs] = useState(notificationPreferences);
const toggleNotifPreference = (type: NotificationType) =>
setEnabledNotifs((prev) => ({ ...prev, [type]: !prev[type] }));
useEffect(() => {
setNotificationPreferences(enabledNotifs);
}, [enabledNotifs]);
const notificationSection = useMemo(
() => ({
group: 'Notifications',
groupLabel: stringGetter({ key: STRING_KEYS.NOTIFICATIONS }),
items: [
{
value: NotificationType.AbacusGenerated,
label: stringGetter({ key: STRING_KEYS.TRADING }),
slotAfter: (
<Switch
name={NotificationType.AbacusGenerated}
checked={enabledNotifs[NotificationType.AbacusGenerated]}
onCheckedChange={(enabled: boolean) => null}
/>
),
onSelect: () => toggleNotifPreference(NotificationType.AbacusGenerated),
},
{
value: NotificationType.SquidTransfer,
label: stringGetter({ key: STRING_KEYS.TRANSFERS }),
slotAfter: (
<Switch
name={NotificationType.SquidTransfer}
checked={enabledNotifs[NotificationType.SquidTransfer]}
onCheckedChange={(enabled: boolean) => null}
/>
),
onSelect: () => toggleNotifPreference(NotificationType.SquidTransfer),
},
],
}),
[stringGetter, enabledNotifs]
);
return [notificationSection];
};
type ElementProps = {
setIsOpen: (open: boolean) => void;
};
export const PreferencesDialog = ({ setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const preferenceItems = usePreferenceMenu();
return (
<Styled.ComboboxDialogMenu
isOpen
title={stringGetter({ key: STRING_KEYS.PREFERENCES })}
items={preferenceItems}
setIsOpen={setIsOpen}
/>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.ComboboxDialogMenu = styled(ComboboxDialogMenu)`
--dialog-content-paddingBottom: 0.5rem;
`;
+2
View File
@@ -112,6 +112,8 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Dialog = styled(Dialog)<{ currentStep: MobilePlaceOrderSteps }>`
--dialog-backgroundColor: var(--color-layer-2);
--dialog-header-height: 1rem;
--dialog-header-paddingTop: 1.5rem;
--dialog-header-paddingBottom: 1rem;
--dialog-content-paddingTop: 0;
--dialog-content-paddingBottom: 0;
--dialog-content-paddingLeft: 0;
@@ -10,7 +10,6 @@ import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NotificationStatus } from '@/constants/notifications';
import { NumberSign } from '@/constants/numbers';
import type { EvmAddress } from '@/constants/wallets';
@@ -245,7 +244,6 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
fromChainId: chainIdStr || undefined,
toAmount: summary?.usdcSize || undefined,
triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
});
abacusStateManager.clearTransferInputValues();
setFromAmount('');
@@ -353,17 +351,14 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
/>
</Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
/>
</Styled.Form>
);
};
@@ -371,12 +366,13 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
${formMixins.transfersForm}
`;
--form-input-height: 3.5rem;
Styled.Footer = styled.footer`
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
${layoutMixins.flexColumn}
gap: 1.5rem;
${layoutMixins.stickyArea1}
min-height: calc(100% - var(--stickyArea0-bottomHeight));
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
@@ -28,9 +28,7 @@ import { Output, OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccountBuyingPower, getSubaccountEquity } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
@@ -66,8 +64,6 @@ export const DepositButtonAndReceipt = ({
const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
const stringGetter = useStringGetter();
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const {
matchNetwork: switchNetwork,
isSwitchingNetwork,
@@ -214,9 +210,7 @@ export const DepositButtonAndReceipt = ({
}
slotError={slotError}
>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Base} />
) : !isMatchingNetwork ? (
{!isMatchingNetwork ? (
<Button
action={ButtonAction.Primary}
onClick={switchNetwork}
@@ -253,7 +247,8 @@ Styled.WithReceipt = styled(WithReceipt)`
Styled.CollapsibleDetails = styled.div`
${layoutMixins.column}
padding: var(--form-input-paddingY) var(--form-input-paddingX);
padding: 0.375rem 1rem 0.5rem;
gap: 0.5rem;
`;
Styled.Details = styled(Details)`
@@ -6,7 +6,7 @@ import { ButtonAction, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { useAccounts, useStringGetter, useSubaccount } from '@/hooks';
import { formMixins } from '@/styles/formMixins';
import { layoutMixins } from '@/styles/layoutMixins';
import { Button } from '@/components/Button';
@@ -71,11 +71,13 @@ export const TestnetDepositForm = ({ onDeposit, onError }: DepositFormProps) =>
},
})}
</p>
<Styled.Footer>
<Button action={ButtonAction.Primary} type={ButtonType.Submit} state={{ isLoading }}>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Button>
</Styled.Footer>
<Styled.SubmitButton
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{ isLoading }}
>
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
</Styled.SubmitButton>
</Styled.Form>
);
};
@@ -83,14 +85,10 @@ export const TestnetDepositForm = ({ onDeposit, onError }: DepositFormProps) =>
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
${formMixins.transfersForm}
${layoutMixins.column}
gap: 1rem;
`;
Styled.Footer = styled.footer`
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
button {
--button-width: 100%;
}
Styled.SubmitButton = styled(Button)`
${layoutMixins.stickyFooter}
`;
@@ -10,7 +10,6 @@ import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { NotificationStatus } from '@/constants/notifications';
import { NumberSign } from '@/constants/numbers';
import {
@@ -172,7 +171,6 @@ export const WithdrawForm = () => {
toChainId: chainIdStr || undefined,
toAmount: debouncedAmountBN.toNumber(),
triggeredAt: Date.now(),
notificationStatus: NotificationStatus.Triggered,
});
abacusStateManager.clearTransferInputValues();
setWithdrawAmount('');
@@ -371,16 +369,14 @@ export const WithdrawForm = () => {
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<Styled.Footer>
<WithdrawButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
setSlippage={onSetSlippage}
slippage={slippage}
withdrawChain={chainIdStr || undefined}
withdrawToken={toToken || undefined}
/>
</Styled.Footer>
<WithdrawButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
setSlippage={onSetSlippage}
slippage={slippage}
withdrawChain={chainIdStr || undefined}
withdrawToken={toToken || undefined}
/>
</Styled.Form>
);
};
@@ -392,12 +388,14 @@ Styled.DiffOutput = styled(DiffOutput)`
`;
Styled.Form = styled.form`
${formMixins.transfersForm}
`;
--form-input-height: 3.5rem;
Styled.Footer = styled.footer`
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
min-height: calc(100% - var(--stickyArea0-bottomHeight));
${layoutMixins.flexColumn}
gap: 1.25rem;
${layoutMixins.stickyArea1}
`;
Styled.DestinationRow = styled.div`
@@ -24,9 +24,7 @@ import { Output, OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { ToggleButton } from '@/components/ToggleButton';
import { WithReceipt } from '@/components/WithReceipt';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
@@ -59,7 +57,6 @@ export const WithdrawButtonAndReceipt = ({
const { leverage } = useSelector(getSubaccount, shallowEqual) || {};
const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const toAmount =
summary?.toAmount &&
@@ -195,20 +192,16 @@ export const WithdrawButtonAndReceipt = ({
</Styled.CollapsibleDetails>
}
>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Base} />
) : (
<Button
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{
isDisabled: !isFormValid,
isLoading: (isFormValid && !requestPayload) || isLoading,
}}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
)}
<Button
action={ButtonAction.Primary}
type={ButtonType.Submit}
state={{
isDisabled: !isFormValid,
isLoading: (isFormValid && !requestPayload) || isLoading,
}}
>
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
</Button>
</Styled.WithReceipt>
);
};
@@ -230,7 +223,8 @@ Styled.WithReceipt = styled(WithReceipt)`
Styled.CollapsibleDetails = styled.div`
${layoutMixins.column}
padding: var(--form-input-paddingY) var(--form-input-paddingX);
padding: 0.375rem 1rem 0.5rem;
gap: 0.5rem;
`;
Styled.Details = styled(Details)`
+2 -2
View File
@@ -460,10 +460,10 @@ Styled.ButtonRow = styled.div`
`;
Styled.Footer = styled.footer`
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--tradeBox-content-paddingBottom);
${layoutMixins.stickyFooter}
backdrop-filter: none;
${layoutMixins.column}
${layoutMixins.noPointerEvents}
margin-top: auto;
`;
@@ -130,38 +130,40 @@ export const AdvancedTradeOptions = () => {
</Styled.SelectMenu>
)}
{needsPostOnly && (
<Checkbox
checked={postOnly || false}
onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({
value: checked,
field: TradeInputField.postOnly,
})
}
id="post-only"
label={
<Styled.CheckboxField>
<Checkbox
checked={postOnly || false}
onClick={() =>
abacusStateManager.setTradeValue({
value: !postOnly,
field: TradeInputField.postOnly,
})
}
/>
<Styled.CheckboxLabel>
<WithTooltip tooltip="post-only" side="right">
{stringGetter({ key: STRING_KEYS.POST_ONLY })}
</WithTooltip>
}
/>
</Styled.CheckboxLabel>
</Styled.CheckboxField>
)}
{needsReduceOnly && (
<Checkbox
checked={reduceOnly || false}
onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({
value: checked,
field: TradeInputField.reduceOnly,
})
}
id="reduce-only"
label={
<Styled.CheckboxField>
<Checkbox
checked={reduceOnly || false}
onClick={() =>
abacusStateManager.setTradeValue({
value: !reduceOnly,
field: TradeInputField.reduceOnly,
})
}
/>
<Styled.CheckboxLabel>
<WithTooltip tooltip="reduce-only" side="right">
{stringGetter({ key: STRING_KEYS.REDUCE_ONLY })}
</WithTooltip>
}
/>
</Styled.CheckboxLabel>
</Styled.CheckboxField>
)}
</>
)}
@@ -172,6 +174,15 @@ export const AdvancedTradeOptions = () => {
const Styled: Record<string, AnyStyledComponent> = {};
Styled.CheckboxField = styled.div`
${layoutMixins.row}
gap: 0.5rem;
`;
Styled.CheckboxLabel = styled.div`
font-size: 0.875em;
`;
Styled.Collapsible = styled(Collapsible)`
--trigger-backgroundColor: transparent;
--trigger-open-backgroundColor: transparent;
@@ -56,8 +56,7 @@ const ToggleContainer = styled(ToggleGroup)<{ value: OrderSide }>`
position: relative;
> button {
--button-toggle-on-border: none;
--button-toggle-off-border: none;
--button-border: none;
--button-toggle-off-backgroundColor: transparent;
--button-toggle-on-backgroundColor: transparent;
--button-toggle-on-textColor: var(--toggle-color);
+17 -11
View File
@@ -300,7 +300,7 @@ export const TransferForm = ({
}}
>
<Styled.Row>
<FormInput
<Styled.FormInput
id="destination"
onInput={(e: SyntheticInputEvent) => onChangeAddress(e.target?.value)}
label={
@@ -353,8 +353,8 @@ export const TransferForm = ({
))}
</Styled.SelectMenu>
<Styled.WithDetailsReceipt side="bottom" detailItems={amountDetailItems}>
<FormInput
<WithDetailsReceipt side="bottom" detailItems={amountDetailItems}>
<Styled.FormInput
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
type={InputType.Number}
onChange={({ floatValue }: NumberFormatValues) => onChangeAmount(floatValue)}
@@ -371,7 +371,7 @@ export const TransferForm = ({
}
disabled={isLoading}
/>
</Styled.WithDetailsReceipt>
</WithDetailsReceipt>
{showNotEnoughGasWarning && (
<AlertMessage type={AlertType.Warning}>
@@ -399,12 +399,17 @@ export const TransferForm = ({
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Form = styled.form`
${formMixins.transfersForm}
${formMixins.inputsColumn}
gap: 1.25rem;
`;
Styled.FormInput = styled(FormInput)`
--form-input-height: 3.5rem;
`;
Styled.Footer = styled.footer`
${formMixins.footer}
--stickyFooterBackdrop-outsetY: var(--dialog-content-paddingBottom);
${layoutMixins.stickyFooter}
margin-top: auto;
`;
Styled.Row = styled.div`
@@ -414,6 +419,7 @@ Styled.Row = styled.div`
Styled.SelectMenu = styled(SelectMenu)`
${formMixins.inputSelectMenu}
--form-input-height: 3.5rem;
`;
Styled.SelectItem = styled(SelectItem)`
@@ -424,10 +430,6 @@ Styled.NetworkSelectMenu = styled(Styled.SelectMenu)`
pointer-events: none;
`;
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.InlineRow = styled.span`
${layoutMixins.inlineRow}
height: 100%;
@@ -442,6 +444,10 @@ Styled.DestinationInputLabel = styled.span`
svg {
color: var(--color-positive);
path {
stroke-width: 2;
}
}
`;
@@ -1,5 +1,4 @@
import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
@@ -13,9 +12,9 @@ import { DiffOutput } from '@/components/DiffOutput';
import { Output, OutputType } from '@/components/Output';
import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
@@ -103,7 +102,7 @@ export const TransferButtonAndReceipt = ({
].filter(isTruthy);
return (
<Styled.WithDetailsReceipt detailItems={transferDetailItems}>
<WithDetailsReceipt detailItems={transferDetailItems}>
{!canAccountTrade ? (
<OnboardingTriggerButton size={ButtonSize.Base} />
) : (
@@ -115,16 +114,6 @@ export const TransferButtonAndReceipt = ({
{stringGetter({ key: STRING_KEYS.CONFIRM_TRANSFER })}
</Button>
)}
</Styled.WithDetailsReceipt>
</WithDetailsReceipt>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
dl {
padding: var(--form-input-paddingY) var(--form-input-paddingX);
}
`;
+31 -23
View File
@@ -7,7 +7,6 @@ import { OnboardingState } from '@/constants/account';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS, TOOLTIP_STRING_KEYS } from '@/constants/localization';
import { isMainnet } from '@/constants/networks';
import { DydxChainAsset, wallets } from '@/constants/wallets';
import {
@@ -22,7 +21,6 @@ import {
import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
import { AssetIcon } from '@/components/AssetIcon';
import { CopyButton } from '@/components/CopyButton';
import { DropdownMenu } from '@/components/DropdownMenu';
import { Output, OutputType } from '@/components/Output';
import { Icon, IconName } from '@/components/Icon';
@@ -40,7 +38,6 @@ import { layoutMixins } from '@/styles/layoutMixins';
import { headerMixins } from '@/styles/headerMixins';
import { MustBigNumber } from '@/lib/numbers';
export const AccountMenu = () => {
const stringGetter = useStringGetter();
const { mintscanBase } = useURLConfigs();
@@ -90,8 +87,13 @@ export const AccountMenu = () => {
</WithTooltip>
<Styled.Address>{truncateAddress(dydxAddress)}</Styled.Address>
</Styled.Column>
<Styled.CopyButton buttonType="icon" value={dydxAddress} shape={ButtonShape.Square} />
<Styled.IconButton
<IconButton
action={ButtonAction.Base}
iconName={IconName.Copy}
onClick={() => dydxAddress && navigator.clipboard.writeText(dydxAddress)}
shape={ButtonShape.Square}
/>
<IconButton
action={ButtonAction.Base}
href={`${mintscanBase}/account/${dydxAddress}`}
iconName={IconName.LinkOut}
@@ -119,7 +121,7 @@ export const AccountMenu = () => {
key: STRING_KEYS.ASSET_BALANCE,
params: { ASSET: chainTokenLabel },
})}
<AssetIcon symbol={chainTokenLabel} />
{/* <AssetIcon symbol="DYDX" /> */}
</Styled.label>
<Styled.BalanceOutput type={OutputType.Asset} value={nativeTokenBalance} />
</div>
@@ -167,9 +169,22 @@ export const AccountMenu = () => {
onSelect: onRecoverKeys,
separator: true,
},
dydxAddress && {
value: 'CopyAddress',
icon: <Icon iconName={IconName.Copy} />,
label: stringGetter({ key: STRING_KEYS.COPY_ADDRESS }),
onSelect: async () => await navigator.clipboard.writeText(dydxAddress),
},
// {
// value: 'ViewInExplorer',
// icon: <Icon iconName={IconName.Etherscan} />,
// label: stringGetter({ key: STRING_KEYS.OPEN_IN_ETHERSCAN }),
// onSelect: () => globalThis.open(`${mintscanBase}/address/${address}`),
// separator: true,
// },
...(onboardingState === OnboardingState.AccountConnected && hdKey
? [
!isMainnet && {
{
value: 'MobileQrSignIn',
icon: <Icon iconName={IconName.Qr} />,
label: stringGetter({ key: STRING_KEYS.TITLE_SIGN_INTO_MOBILE }),
@@ -177,19 +192,17 @@ export const AccountMenu = () => {
},
{
value: 'MnemonicExport',
icon: <Icon iconName={IconName.ExportKeys} />,
icon: (
<span>
<Icon iconName={IconName.ExportKeys} />
</span>
),
label: <span>{stringGetter({ key: STRING_KEYS.EXPORT_SECRET_PHRASE })}</span>,
highlightColor: 'negative',
onSelect: () => dispatch(openDialog({ type: DialogTypes.MnemonicExport })),
},
].filter(isTruthy)
]
: []),
{
value: 'Preferences',
icon: <Icon iconName={IconName.Gear} />,
label: stringGetter({ key: STRING_KEYS.PREFERENCES }),
onSelect: () => dispatch(openDialog({ type: DialogTypes.Preferences })),
},
{
value: 'Disconnect',
icon: <Icon iconName={IconName.BoxClose} />,
@@ -320,12 +333,13 @@ Styled.label = styled.div`
Styled.Balances = styled.div`
${layoutMixins.flexColumn}
gap: 2px;
> div {
${layoutMixins.spacedRow}
box-shadow: 0 0 0 1px var(--color-border);
gap: 0.5rem;
padding: 0.625rem 1rem;
padding: 0.5rem 1rem;
background-color: var(--color-layer-4);
@@ -372,10 +386,4 @@ Styled.ConnectToChain = styled(Styled.Column)`
Styled.IconButton = styled(IconButton)`
--button-padding: 0 0.25rem;
--button-border: solid var(--border-width) var(--color-layer-6);
`;
Styled.CopyButton = styled(CopyButton)`
--button-padding: 0 0.25rem;
--button-border: solid var(--border-width) var(--color-layer-6);
`;
+60 -37
View File
@@ -3,17 +3,18 @@ import styled from 'styled-components';
import { layoutMixins } from '@/styles/layoutMixins';
import { groupBy } from 'lodash';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { type Notification, NotificationStatus } from '@/constants/notifications';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { useBreakpoints } from '@/hooks';
import { useNotifications } from '@/hooks/useNotifications';
import { CloseIcon } from '@/icons';
import { Button } from '@/components/Button';
import { ButtonAction, ButtonSize } from '@/constants/buttons';
import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu';
import { DialogPlacement } from '@/components/Dialog';
import { Notification as NotificationCard } from '@/components/Notification';
import { Output, OutputType } from '@/components/Output';
import { IconButton } from '@/components/IconButton';
import { Toolbar } from '@/components/Toolbar';
type ElementProps = {
@@ -26,14 +27,13 @@ export const NotificationsMenu = ({
placement = DialogPlacement.Sidebar,
}: ElementProps) => {
const { isTablet } = useBreakpoints();
const stringGetter = useStringGetter();
const {
notifications,
getDisplayData,
getKey,
markSeen,
markCleared,
markAllCleared,
onNotificationAction,
@@ -65,12 +65,14 @@ export const NotificationsMenu = ({
const items: Parameters<typeof ComboboxDialogMenu>[0]['items'] = useMemo(
() =>
(Object.entries(notificationsByStatus) as unknown as [NotificationStatus, Notification[]][])
.filter(([status]) => status < NotificationStatus.Cleared)
// .filter(([status]) => status !== NotificationStatus.Cleared)
.map(([status, notifications]) => ({
group: status,
groupLabel: {
[NotificationStatus.Triggered]: stringGetter({ key: STRING_KEYS.NEW }),
[NotificationStatus.Triggered]: 'New',
// [NotificationStatus.Updated]: 'Updates',
[NotificationStatus.Seen]: 'Seen',
[NotificationStatus.Cleared]: 'Archived',
}[status as number],
items: notifications
@@ -81,40 +83,59 @@ export const NotificationsMenu = ({
)
.map((notification) => ({
notification,
key: getKey?.(notification),
displayData: getDisplayData?.(notification),
key: getKey(notification),
displayData: getDisplayData(notification),
}))
.map(({ notification, key, displayData }) => ({
value: key,
label: displayData.title,
description: displayData.body,
slotCustomContent: displayData.renderCustomBody?.({ notification }) ?? (
<NotificationCard
isToast={false}
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotDescription={displayData.body}
notification={notification}
/>
label: displayData.title ?? '',
description: displayData.customMenuContent || displayData.description,
slotBefore: !displayData.customMenuContent && displayData.icon,
slotAfter: !displayData.customMenuContent && (
<>
<$Output
type={OutputType.RelativeTime}
value={
notification.timestamps[NotificationStatus.Updated] ||
notification.timestamps[NotificationStatus.Triggered]
}
/>
{notification.status < NotificationStatus.Seen ? <$UnreadIndicator /> : null}
{notification.status < NotificationStatus.Cleared ? (
<$IconButton
iconComponent={CloseIcon}
onClick={(e) => {
e.stopPropagation();
/*if (notification.status < NotificationStatus.Seen) {
markSeen(notification);
} else*/ if (notification.status < NotificationStatus.Cleared) {
markCleared(notification);
}
}}
/>
) : null}
</>
),
disabled: notification.status === NotificationStatus.Cleared,
onSelect: () => {
onNotificationAction?.(notification);
markSeen?.(notification);
onNotificationAction(notification);
markSeen(notification);
},
})),
}))
.filter(({ items }) => items.length),
[notificationsByStatus, getDisplayData, onNotificationAction, markSeen, stringGetter]
[notificationsByStatus, getDisplayData, onNotificationAction, markSeen]
);
return (
<$ComboboxDialogMenu
withItemBorders
<ComboboxDialogMenu
isOpen={isMenuOpen || placement === DialogPlacement.Inline}
setIsOpen={setIsMenuOpen}
items={items}
title={stringGetter({ key: STRING_KEYS.NOTIFICATIONS })}
title="Notifications"
slotTrigger={
<$TriggerContainer>
{slotTrigger}
@@ -129,11 +150,11 @@ export const NotificationsMenu = ({
state={{ isLoading: isEnablingPush }}
onClick={enablePush}
>
{stringGetter({ key: STRING_KEYS.ENABLE_PUSH_NOTIFICATIONS })}
Enable Push Notifications
</Button>
) : (
<Button size={ButtonSize.Small} action={ButtonAction.Secondary} onClick={disablePush}>
{stringGetter({ key: STRING_KEYS.DISABLE_PUSH_NOTIFICATIONS })}
Disable Push Notifications
</Button>
)}
@@ -147,7 +168,7 @@ export const NotificationsMenu = ({
),
}}
>
{stringGetter({ key: STRING_KEYS.CLEAR_ALL })}
Clear All
</Button>
</$FooterToolbar>
}
@@ -157,14 +178,6 @@ export const NotificationsMenu = ({
);
};
const $ComboboxDialogMenu = styled(ComboboxDialogMenu)`
--comboboxDialogMenu-item-padding: 0;
[cmdk-list] > [cmdk-list-sizer] > * {
box-shadow: none;
}
`;
const $UnreadIndicator = styled.div`
width: 0.5rem;
height: 0.5rem;
@@ -185,6 +198,16 @@ const $TriggerUnreadIndicator = styled($UnreadIndicator)`
top: -0.325rem;
`;
const $Output = styled(Output)`
color: var(--color-text-0);
`;
const $IconButton = styled(IconButton)`
--button-border: none;
--button-textColor: var(--color-text-0);
--button-hover-textColor: var(--color-text-1);
`;
const $FooterToolbar = styled(Toolbar)`
display: flex;
flex-wrap: wrap;
@@ -1,127 +0,0 @@
import styled, { type AnyStyledComponent } from 'styled-components';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import { shallowEqual, useSelector } from 'react-redux';
import {
AbacusOrderStatus,
AbacusOrderType,
KotlinIrEnumValues,
ORDER_STATUS_STRINGS,
TRADE_TYPES,
} from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
import { TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade';
import { useStringGetter } from '@/hooks';
import { layoutMixins } from '@/styles/layoutMixins';
import { AssetIcon } from '@/components/AssetIcon';
import { Details } from '@/components/Details';
import { Notification, NotificationProps } from '@/components/Notification';
import { OrderSideTag } from '@/components/OrderSideTag';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { Output, OutputType } from '@/components/Output';
import { getMarketData } from '@/state/perpetualsSelectors';
type ElementProps = {
data: {
AMOUNT: string;
ASSET: string;
AVERAGE_PRICE: string;
FILLED_AMOUNT: string;
MARKET: string;
ORDER_STATUS: string;
ORDER_TYPE: string;
ORDER_TYPE_TEXT: string;
PRICE: string;
SIDE: string;
};
};
export type TradeNotificationProps = NotificationProps & ElementProps;
export const TradeNotification = ({ isToast, data, notification }: TradeNotificationProps) => {
const stringGetter = useStringGetter();
const { AVERAGE_PRICE, FILLED_AMOUNT, MARKET, ORDER_TYPE, ORDER_STATUS, SIDE } = data;
const marketData = useSelector(getMarketData(MARKET), shallowEqual);
const { assetId } = marketData ?? {};
const orderType = ORDER_TYPE as KotlinIrEnumValues<typeof AbacusOrderType>;
const tradeType = TRADE_TYPES[orderType];
const titleKey = tradeType && TRADE_TYPE_STRINGS[tradeType]?.tradeTypeKey;
const orderStatus = ORDER_STATUS as KotlinIrEnumValues<typeof AbacusOrderStatus>;
return (
<Notification
isToast={isToast}
notification={notification}
slotIcon={<AssetIcon symbol={assetId} />}
slotTitle={titleKey && stringGetter({ key: titleKey })}
slotTitleRight={
<Styled.OrderStatus>
{stringGetter({ key: ORDER_STATUS_STRINGS[orderStatus] })}
<Styled.OrderStatusIcon status={orderStatus} totalFilled={parseFloat(FILLED_AMOUNT)} />
</Styled.OrderStatus>
}
slotCustomContent={
<Styled.Details
items={[
{
key: 'size',
label: (
<Styled.Label>
{stringGetter({ key: STRING_KEYS.SIZE })}
<OrderSideTag
orderSide={SIDE === STRING_KEYS.BUY ? OrderSide.BUY : OrderSide.SELL}
/>
</Styled.Label>
),
value: (
<Output type={OutputType.Asset} value={FILLED_AMOUNT} tag={marketData?.assetId} />
),
},
{
key: 'price',
label: stringGetter({ key: STRING_KEYS.PRICE }),
value:
ORDER_TYPE === TradeTypes.MARKET ? (
<span>{stringGetter({ key: STRING_KEYS.MARKET_ORDER_SHORT })}</span>
) : (
<Output
type={OutputType.Fiat}
value={AVERAGE_PRICE}
fractionDigits={marketData?.configs?.displayTickSizeDecimals}
/>
),
},
]}
/>
}
/>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Label = styled.span`
${layoutMixins.row}
gap: 0.5ch;
`;
Styled.OrderStatus = styled(Styled.Label)`
color: var(--color-text-0);
font: var(--font-small-book);
`;
Styled.OrderStatusIcon = styled(OrderStatusIcon)`
width: 0.9375rem;
height: 0.9375rem;
`;
Styled.Details = styled(Details)`
--details-item-height: 1.5rem;
dd {
color: var(--color-text-2);
}
`;
@@ -1,225 +0,0 @@
import { useCallback, useState, useMemo, MouseEvent } from 'react';
import styled, { css, type AnyStyledComponent } from 'styled-components';
import { useInterval, useStringGetter } from '@/hooks';
import { AlertType } from '@/constants/alerts';
import { STRING_KEYS } from '@/constants/localization';
import { TransferNotifcation } from '@/constants/notifications';
import { formatSeconds } from '@/lib/timeUtils';
import { AlertMessage } from '@/components/AlertMessage';
import { Collapsible } from '@/components/Collapsible';
import { Icon, IconName } from '@/components/Icon';
import { LoadingDots } from '@/components/Loading/LoadingDots';
import { Notification, NotificationProps } from '@/components/Notification';
import { Output, OutputType } from '@/components/Output';
import { WithReceipt } from '@/components/WithReceipt';
import { layoutMixins } from '@/styles/layoutMixins';
import { TransferStatusSteps } from './TransferStatusSteps';
type ElementProps = {
type: 'withdrawal' | 'deposit';
transfer: TransferNotifcation;
triggeredAt?: number;
};
export const TransferStatusNotification = ({
isToast,
notification,
slotIcon,
slotTitle,
slotDescription,
transfer,
type,
triggeredAt = Date.now(),
}: ElementProps & NotificationProps) => {
const stringGetter = useStringGetter();
const [open, setOpen] = useState<boolean>(false);
const [secondsLeft, setSecondsLeft] = useState<number>(0);
const { fromChainId, status, txHash, toAmount } = transfer;
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
const updateSecondsLeft = useCallback(() => {
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration || 0) * 1000;
setSecondsLeft(Math.floor((triggeredAt + fromChainEta + toChainEta - Date.now()) / 1000));
}, [status]);
useInterval({ callback: updateSecondsLeft });
const inProgressStatusString =
type === 'deposit'
? secondsLeft > 0
? STRING_KEYS.DEPOSIT_STATUS
: STRING_KEYS.DEPOSIT_STATUS_SHORTLY
: secondsLeft > 0
? STRING_KEYS.WITHDRAW_STATUS
: STRING_KEYS.WITHDRAW_STATUS_SHORTLY;
const statusString =
type === 'deposit'
? status?.squidTransactionStatus === 'success'
? STRING_KEYS.DEPOSIT_COMPLETE
: inProgressStatusString
: status?.squidTransactionStatus === 'success'
? STRING_KEYS.WITHDRAW_COMPLETE
: inProgressStatusString;
const content = (
<>
<Styled.Status>
{stringGetter({
key: statusString,
params: {
AMOUNT_USD: <Styled.InlineOutput type={OutputType.Fiat} value={toAmount} />,
ESTIMATED_DURATION: (
<Styled.InlineOutput
type={OutputType.Text}
value={formatSeconds(Math.max(secondsLeft || 0, 0))}
/>
),
},
})}
</Styled.Status>
{error && (
<AlertMessage type={AlertType.Error}>
{stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
})}
</AlertMessage>
)}
</>
);
const transferNotif = (
<Notification
isToast={isToast}
notification={notification}
slotIcon={isToast && slotIcon}
slotTitle={slotTitle}
slotCustomContent={
!status ? (
<LoadingDots size={3} />
) : (
<Styled.BridgingStatus>
{content}
{!isToast && status?.squidTransactionStatus !== 'success' && (
<Styled.TransferStatusSteps status={status} type={type} />
)}
</Styled.BridgingStatus>
)
}
slotAction={
isToast &&
status &&
!error && (
<Styled.Trigger
isOpen={open}
onClick={(e: MouseEvent) => {
e.stopPropagation();
setOpen(!open);
}}
>
<Icon iconName={IconName.Caret} />{' '}
{stringGetter({
key: open ? STRING_KEYS.HIDE_DETAILS : STRING_KEYS.VIEW_DETAILS,
})}
</Styled.Trigger>
)
}
withClose={false}
/>
);
return isToast ? (
<WithReceipt
hideReceipt={!open}
side="bottom"
slotReceipt={
<Collapsible open={open} onOpenChange={setOpen} label="" withTrigger={false}>
<Styled.Receipt>
<TransferStatusSteps status={status} type={type} />
</Styled.Receipt>
</Collapsible>
}
>
{transferNotif}
</WithReceipt>
) : (
transferNotif
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.BridgingStatus = styled.div`
${layoutMixins.flexColumn};
gap: 0.5rem;
`;
Styled.Status = styled.div<{ withMarginBottom?: boolean }>`
color: var(--color-text-0);
font-size: 0.875rem;
${({ withMarginBottom }) =>
withMarginBottom &&
css`
margin-bottom: 0.5rem;
`}
`;
Styled.InlineOutput = styled(Output)`
display: inline-block;
color: var(--color-text-1);
`;
Styled.Step = styled.div`
${layoutMixins.row};
gap: 0.5rem;
`;
Styled.TransferStatusSteps = styled(TransferStatusSteps)`
padding: 0.5rem 0 0;
`;
Styled.Trigger = styled.button<{ isOpen?: boolean }>`
display: flex;
align-items: center;
gap: 0.5em;
color: var(--color-accent);
user-select: none;
cursor: pointer;
&:focus-visible {
outline: none;
text-decoration: underline;
}
svg {
transition: rotate 0.3s var(--ease-out-expo);
}
${({ isOpen }) =>
isOpen &&
css`
& > svg {
rotate: -0.5turn;
}
`}
`;
Styled.Receipt = styled.div`
padding: 0 1rem;
`;
+4 -4
View File
@@ -96,19 +96,19 @@ export const MarketsTable = ({ className }: { className?: string }) => {
),
},
{
columnKey: 'priceChange24HPercent',
getCellValue: (row) => row.priceChange24HPercent,
columnKey: 'priceChange24H',
getCellValue: (row) => row.priceChange24H,
label: stringGetter({ key: STRING_KEYS.CHANGE_24H }),
renderCell: ({ priceChange24H, priceChange24HPercent, tickSizeDecimals }) => (
<TableCell stacked>
<Styled.InlineRow>
{!priceChange24HPercent ? (
{!priceChange24H ? (
<Output type={OutputType.Text} value={null} />
) : (
<Styled.Output
type={OutputType.Percent}
value={MustBigNumber(priceChange24HPercent).abs()}
isNegative={MustBigNumber(priceChange24HPercent).isNegative()}
isNegative={MustBigNumber(priceChange24H).isNegative()}
/>
)}
</Styled.InlineRow>
+2 -2
View File
@@ -81,10 +81,10 @@ const getTransferHistoryTableColumnDef = ({
),
renderCell: ({ fromAddress, toAddress }) => (
<TableCell stacked>
<CopyButton buttonType="text" value={fromAddress ?? undefined}>
<CopyButton shownAsText value={fromAddress ?? undefined}>
{fromAddress ? truncateAddress(fromAddress) : '-'}
</CopyButton>{' '}
<CopyButton buttonType="text" value={toAddress ?? undefined}>
<CopyButton shownAsText value={toAddress ?? undefined}>
{toAddress ? truncateAddress(toAddress) : '-'}
</CopyButton>
</TableCell>