@xswap-link/sdk 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.global.js +99 -93
  3. package/dist/index.js +2061 -1846
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1901 -1689
  6. package/dist/index.mjs.map +1 -1
  7. package/localTheme.ts +1 -1
  8. package/package.json +2 -1
  9. package/postcss.config.js +2 -0
  10. package/src/assets/icons/ChainlinkMarkIcon.tsx +4 -5
  11. package/src/assets/icons/CheckCircleThinIcon.tsx +22 -0
  12. package/src/assets/icons/ChevronDownThinIcon.tsx +39 -0
  13. package/src/assets/icons/ChevronDownWideThinIcon.tsx +18 -0
  14. package/src/assets/icons/ChevronRightThinIcon.tsx +23 -0
  15. package/src/assets/icons/ChevronsUpDownThinIcon.tsx +21 -0
  16. package/src/assets/icons/CloseIcon.tsx +3 -2
  17. package/src/assets/icons/ErrorCircleThinIcon.tsx +22 -0
  18. package/src/assets/icons/InfoThinIcon.tsx +33 -0
  19. package/src/assets/icons/LinkExternalThinIcon.tsx +19 -0
  20. package/src/assets/icons/LoadingRingIcon.tsx +22 -0
  21. package/src/assets/icons/ProgressRingThinIcon.tsx +29 -0
  22. package/src/assets/icons/RedirectThinIcon.tsx +15 -0
  23. package/src/assets/icons/SearchThinIcon.tsx +15 -0
  24. package/src/assets/icons/SignThinIcon.tsx +15 -0
  25. package/src/assets/icons/WalletCheckThinIcon.tsx +24 -0
  26. package/src/assets/icons/index.ts +14 -0
  27. package/src/components/PoweredBy/index.tsx +1 -1
  28. package/src/components/Swap/Header/index.tsx +5 -5
  29. package/src/components/Swap/HistoryView/ActivityCard/index.tsx +110 -115
  30. package/src/components/Swap/HistoryView/index.tsx +10 -7
  31. package/src/components/Swap/ReorderButton/ReorderButton.tsx +7 -4
  32. package/src/components/Swap/SettingsView/Delivery/index.tsx +20 -18
  33. package/src/components/Swap/SettingsView/InfiniteApproval/index.tsx +20 -18
  34. package/src/components/Swap/SettingsView/Slippage/index.tsx +57 -49
  35. package/src/components/Swap/SwapView/ConfirmationView/TokenTiles/index.tsx +72 -0
  36. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Header/index.tsx +13 -25
  37. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Steps/index.tsx +65 -113
  38. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +19 -23
  39. package/src/components/Swap/SwapView/ConfirmationView/TxResult/index.tsx +47 -69
  40. package/src/components/Swap/SwapView/FeesPanel/Fees/index.tsx +1 -1
  41. package/src/components/Swap/SwapView/FeesPanel/index.tsx +6 -4
  42. package/src/components/Swap/SwapView/SwapButton/index.tsx +0 -29
  43. package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +11 -2
  44. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/ChainPicker/index.tsx +4 -4
  45. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx +62 -51
  46. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +1 -1
  47. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +94 -43
  48. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +27 -5
  49. package/src/components/Swap/SwapView/SwapPanel/UsdValue/index.tsx +2 -2
  50. package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +23 -18
  51. package/src/components/Swap/SwapView/SwapPanel/index.tsx +17 -5
  52. package/src/components/Swap/SwapView/WalletPicker/index.tsx +131 -139
  53. package/src/components/Swap/SwapView/index.tsx +11 -7
  54. package/src/components/Swap/WalletPickerView/index.tsx +65 -0
  55. package/src/components/Swap/index.tsx +23 -6
  56. package/src/components/ToggleButton/index.tsx +6 -6
  57. package/src/components/TokenLogo/index.tsx +1 -1
  58. package/src/components/TxModal/index.tsx +13 -7
  59. package/src/components/global.css +103 -1
  60. package/src/config/fonts.ts +15 -2
  61. package/src/constants/index.ts +4 -0
  62. package/src/context/SwapProvider.tsx +106 -19
  63. package/src/context/TxUIWrapper.tsx +0 -1
  64. package/src/utils/validation.ts +9 -4
  65. package/tailwind.config.js +3 -9
  66. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/ArrowIcon.tsx +0 -13
  67. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/SwapPanel/index.tsx +0 -72
  68. package/src/components/Swap/SwapView/ConfirmationView/TxResult/TokenItem/index.tsx +0 -25
@@ -1,4 +1,4 @@
1
- import { ChevronDownIcon, SearchIcon } from "@src/assets/icons";
1
+ import { ChevronDownThinIcon, SearchThinIcon } from "@src/assets/icons";
2
2
  import { useSwapContext } from "@src/context";
3
3
  import { SwapPanelType } from "@src/components/Swap/SwapView";
4
4
  import { Chain } from "@src/models";
@@ -56,14 +56,15 @@ export const NetworkFilter = ({ type, selectedChain, onSelect }: Props) => {
56
56
  return () => document.removeEventListener("mousedown", onPointerDown);
57
57
  }, []);
58
58
 
59
- const previewChains = chains.slice(0, 3);
59
+ // 2x2 icon cluster shown when no network is picked
60
+ const clusterChains = chains.slice(0, 4);
60
61
 
61
62
  return (
62
63
  <div className="relative" ref={containerRef}>
63
64
  <button
64
65
  type="button"
65
66
  onClick={() => setOpen((state) => !state)}
66
- className="flex items-center gap-2 h-12 px-3 py-2 rounded-lg bg-t_bg_tertiary bg-opacity-[0.04] hover:bg-opacity-10 transition-colors"
67
+ className="flex items-center gap-2.5 h-12 px-3 py-2 rounded-lg bg-t_bg_tertiary bg-opacity-[0.04] hover:bg-opacity-10 transition-colors"
67
68
  >
68
69
  {selectedChain ? (
69
70
  <img
@@ -72,13 +73,13 @@ export const NetworkFilter = ({ type, selectedChain, onSelect }: Props) => {
72
73
  className="w-5 h-5 rounded-full"
73
74
  />
74
75
  ) : (
75
- <div className="flex -space-x-1.5 shrink-0">
76
- {previewChains.map((chain) => (
76
+ <div className="grid grid-cols-2 gap-0.5 shrink-0">
77
+ {clusterChains.map((chain) => (
77
78
  <img
78
79
  key={chain.chainId}
79
80
  src={chain.image}
80
81
  alt={chain.displayName}
81
- className="w-5 h-5 rounded-full border border-solid border-t_bg_primary bg-t_bg_primary"
82
+ className="w-[11px] h-[11px] rounded-full"
82
83
  />
83
84
  ))}
84
85
  </div>
@@ -89,67 +90,77 @@ export const NetworkFilter = ({ type, selectedChain, onSelect }: Props) => {
89
90
  : `${chains.length} Networks`}
90
91
  </p>
91
92
  <div
92
- className={`flex items-center justify-center w-6 h-6 fill-t_text_primary opacity-50 transition-transform [&_svg]:w-3 [&_svg]:h-3 ${
93
+ className={`flex items-center justify-center w-6 h-6 text-t_text_primary opacity-50 transition-transform ${
93
94
  open ? "rotate-180" : ""
94
95
  }`}
95
96
  >
96
- <ChevronDownIcon />
97
+ <ChevronDownThinIcon />
97
98
  </div>
98
99
  </button>
99
100
 
100
101
  {open && (
101
- <div className="absolute right-0 top-[calc(100%+8px)] z-20 flex flex-col w-[220px] max-h-[280px] rounded-xl bg-t_bg_primary border border-solid border-t_text_primary border-opacity-10 backdrop-blur-[24px] overflow-hidden shadow-lg">
102
- <div className="flex items-center gap-2 px-4 py-3 border-b border-solid border-t_text_primary border-opacity-10">
103
- <div className="fill-t_text_primary opacity-50 [&_svg]:w-3.5 [&_svg]:h-3.5 flex items-center">
104
- <SearchIcon />
102
+ <div className="absolute -right-2 top-[calc(100%+8px)] z-20 flex flex-col w-[296px] py-5 px-2.5 rounded-xl network-dropdown-surface border border-solid border-t_text_primary border-opacity-5 overflow-hidden">
103
+ <div className="flex items-center gap-2 px-2.5 pb-4">
104
+ <div className="text-t_text_primary opacity-60 [&_svg]:w-4 [&_svg]:h-4 flex items-center">
105
+ <SearchThinIcon />
105
106
  </div>
106
107
  <input
107
108
  value={searchValue}
108
109
  onChange={(e) => setSearchValue(e.target.value)}
109
110
  placeholder={`Browse ${chains.length} Chains...`}
110
- className="bg-transparent border-none outline-none w-full text-sm text-t_text_primary placeholder:text-t_text_primary placeholder:text-opacity-40"
111
+ className="bg-transparent border-none outline-none w-full text-sm leading-6 text-t_text_primary placeholder:text-t_text_primary placeholder:text-opacity-40"
111
112
  />
112
113
  </div>
113
- <div className="h-[200px]">
114
+ <div className="h-px w-full bg-t_bg_tertiary bg-opacity-5 mb-2" />
115
+ {/* The design's list mask is a hard cut 6px into the sixth row —
116
+ that sliver is the hint that the list scrolls. Rows are 40px
117
+ pills around 24px of content, so the cut lands at 214px. */}
118
+ <div className="h-[214px]">
114
119
  <Virtuoso
115
- style={{ height: "200px" }}
120
+ style={{ height: "214px" }}
116
121
  data={[undefined, ...filteredChains] as (Chain | undefined)[]}
117
- itemContent={(_, chain) => (
118
- <button
119
- type="button"
120
- onClick={() => {
121
- onSelect(chain);
122
- setOpen(false);
123
- }}
124
- className={`flex items-center gap-2 w-full px-4 py-2 text-left hover:bg-t_bg_tertiary hover:bg-opacity-10 transition-colors ${
125
- (chain?.chainId || "") === (selectedChain?.chainId || "")
126
- ? "bg-t_bg_tertiary bg-opacity-10"
127
- : ""
128
- }`}
129
- >
130
- {chain ? (
131
- <img
132
- src={chain.image}
133
- alt={chain.displayName}
134
- className="w-5 h-5 rounded-full"
135
- />
136
- ) : (
137
- <div className="flex -space-x-1 shrink-0">
138
- {previewChains.map((previewChain) => (
139
- <img
140
- key={previewChain.chainId}
141
- src={previewChain.image}
142
- alt={previewChain.displayName}
143
- className="w-4 h-4 rounded-full border border-solid border-t_bg_primary bg-t_bg_primary"
144
- />
145
- ))}
146
- </div>
147
- )}
148
- <p className="text-sm leading-6 text-t_text_primary truncate">
149
- {chain ? chain.displayName : "All Networks"}
150
- </p>
151
- </button>
152
- )}
122
+ itemContent={(_, chain) => {
123
+ const isSelected =
124
+ (chain?.chainId || "") === (selectedChain?.chainId || "");
125
+ return (
126
+ <button
127
+ type="button"
128
+ onClick={() => {
129
+ onSelect(chain);
130
+ setOpen(false);
131
+ }}
132
+ className={`network-dropdown-row flex items-center gap-2.5 w-full h-10 px-2.5 rounded-lg text-left ${
133
+ isSelected ? "network-dropdown-row-active" : ""
134
+ }`}
135
+ >
136
+ {chain ? (
137
+ <img
138
+ src={chain.image}
139
+ alt={chain.displayName}
140
+ className="w-6 h-6 rounded-full"
141
+ />
142
+ ) : (
143
+ <div className="grid grid-cols-2 gap-0.5 shrink-0">
144
+ {clusterChains.map((clusterChain) => (
145
+ <img
146
+ key={clusterChain.chainId}
147
+ src={clusterChain.image}
148
+ alt={clusterChain.displayName}
149
+ className="w-[11px] h-[11px] rounded-full"
150
+ />
151
+ ))}
152
+ </div>
153
+ )}
154
+ <p
155
+ className={`text-sm leading-[18px] tracking-[0.01em] text-t_text_primary truncate ${
156
+ isSelected ? "" : "text-opacity-80"
157
+ }`}
158
+ >
159
+ {chain ? chain.displayName : "All Networks"}
160
+ </p>
161
+ </button>
162
+ );
163
+ }}
153
164
  />
154
165
  </div>
155
166
  </div>
@@ -139,7 +139,7 @@ export const TokenItem = ({
139
139
  )
140
140
  )}
141
141
  {resetWarning ? (
142
- <div className="flex items-center justify-end gap-1.5 text-[#D77F02]">
142
+ <div className="flex items-center justify-end gap-1.5 text-[#F46F28]">
143
143
  <WarningIcon className="w-3.5 h-3.5 shrink-0" />
144
144
  <span className="text-xs leading-[18px] tracking-[0.01em] whitespace-nowrap">
145
145
  <span className="sm:hidden">
@@ -1,6 +1,6 @@
1
1
  import { faCheck } from "@fortawesome/free-solid-svg-icons";
2
2
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
- import { CloseIcon, CopyIcon, SearchIcon } from "@src/assets/icons";
3
+ import { CloseIcon, CopyIcon, SearchThinIcon } from "@src/assets/icons";
4
4
  import {
5
5
  Button,
6
6
  Spinner,
@@ -20,7 +20,7 @@ import {
20
20
  } from "@src/utils";
21
21
  import { BigNumber, ethers } from "ethers";
22
22
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
23
- import { GroupedVirtuoso } from "react-virtuoso";
23
+ import { Virtuoso } from "react-virtuoso";
24
24
  import { NetworkFilter } from "./NetworkFilter";
25
25
  import { TokenItem } from "./TokenItem";
26
26
  import { useAccount } from "wagmi";
@@ -55,7 +55,11 @@ export const TokenPicker = ({
55
55
  const [loadingCustomTokenData, setLoadingCustomTokenData] = useState(false);
56
56
  const [customTokenData, setCustomTokenData] =
57
57
  useState<ImportedTokenData | null>(null);
58
- const [networkFilter, setNetworkFilter] = useState<Chain | undefined>(chain);
58
+ // Always opens on "All Networks" — the cross-chain token list is the
59
+ // default view even when a chain is already set on the swap panel.
60
+ const [networkFilter, setNetworkFilter] = useState<Chain | undefined>(
61
+ undefined,
62
+ );
59
63
  const currentRouteRequestNonce = useRef<number>(0);
60
64
  const tokenSearchRef = useRef<HTMLInputElement | null>(null);
61
65
 
@@ -295,8 +299,7 @@ export const TokenPicker = ({
295
299
 
296
300
  useEffect(() => {
297
301
  setSearchValue("");
298
- setNetworkFilter(chain);
299
- // eslint-disable-next-line react-hooks/exhaustive-deps
302
+ setNetworkFilter(undefined);
300
303
  }, [isOpen]);
301
304
 
302
305
  const isMatch = (value, searchValue) =>
@@ -422,11 +425,50 @@ export const TokenPicker = ({
422
425
  : [{ label: "RECOMMENDED.", tokens: recommendedTokens }];
423
426
  }, [ownedTokens, recommendedTokens]);
424
427
 
425
- const allTokens = useMemo(
426
- () => tokenGroups.flatMap((group) => group.tokens),
428
+ // The current group's label renders OUTSIDE the scroll container (pinned
429
+ // above it, transparent bg), so rows physically clip on the label's bottom
430
+ // edge — the scroller's top edge. Labels of the following groups stay
431
+ // in-flow and take over the pinned slot once they reach that line.
432
+ const listRows = useMemo(
433
+ () =>
434
+ tokenGroups.flatMap((group, groupIndex) => {
435
+ const rows: (
436
+ | { kind: "label"; label: string; groupLabel: string }
437
+ | { kind: "empty"; groupLabel: string }
438
+ | { kind: "token"; token: TokenOption; groupLabel: string }
439
+ )[] =
440
+ groupIndex === 0
441
+ ? []
442
+ : [
443
+ {
444
+ kind: "label",
445
+ label: group.label,
446
+ groupLabel: group.label,
447
+ },
448
+ ];
449
+ if (group.tokens.length) {
450
+ rows.push(
451
+ ...group.tokens.map((token) => ({
452
+ kind: "token" as const,
453
+ token,
454
+ groupLabel: group.label,
455
+ })),
456
+ );
457
+ } else {
458
+ rows.push({ kind: "empty", groupLabel: group.label });
459
+ }
460
+ return rows;
461
+ }),
427
462
  [tokenGroups],
428
463
  );
429
464
 
465
+ // Index of the topmost (partially) visible row, reported by Virtuoso.
466
+ const [topRowIndex, setTopRowIndex] = useState(0);
467
+ const pinnedGroupLabel =
468
+ listRows[Math.min(topRowIndex, listRows.length - 1)]?.groupLabel ??
469
+ tokenGroups[0]?.label ??
470
+ "";
471
+
430
472
  useEffect(() => {
431
473
  if (isOpen && tokenSearchRef.current) {
432
474
  setTimeout(() => {
@@ -438,7 +480,7 @@ export const TokenPicker = ({
438
480
  return showImportWarning ? (
439
481
  <>
440
482
  <div className="flex justify-between items-center mb-6">
441
- <div className="text-2xl leading-8 tracking-[0.01em]">
483
+ <div className="text-2xl font-medium leading-8 tracking-[0.01em]">
442
484
  Import token
443
485
  </div>
444
486
  <button
@@ -528,7 +570,7 @@ export const TokenPicker = ({
528
570
  ) : (
529
571
  <>
530
572
  <div className="flex justify-between items-center mb-6">
531
- <div className="text-2xl leading-8 tracking-[0.01em] text-t_text_primary">
573
+ <div className="text-2xl font-medium leading-8 tracking-[0.01em] text-t_text_primary">
532
574
  Select Token
533
575
  </div>
534
576
  <button
@@ -545,8 +587,8 @@ export const TokenPicker = ({
545
587
 
546
588
  <div className="flex items-center justify-between gap-2 h-14 pl-4 pr-1 rounded-xl bg-t_bg_tertiary bg-opacity-[0.03] border border-solid border-t_text_primary border-opacity-5 mb-6">
547
589
  <div className="flex items-center gap-2 flex-1 min-w-0">
548
- <div className="fill-t_text_primary opacity-50 [&_svg]:w-4 [&_svg]:h-4 flex items-center shrink-0">
549
- <SearchIcon />
590
+ <div className="text-t_text_primary opacity-60 flex items-center shrink-0">
591
+ <SearchThinIcon />
550
592
  </div>
551
593
  <input
552
594
  ref={tokenSearchRef}
@@ -584,7 +626,7 @@ export const TokenPicker = ({
584
626
  <div className="text-sm font-medium text-ellipsis">
585
627
  {customTokenData.name}
586
628
  </div>
587
- <div className="text-xs text-t_text_primary !text-opacity-50 font-medium">
629
+ <div className="text-xs text-t_text_primary !text-opacity-50">
588
630
  {importChain?.displayName}
589
631
  </div>
590
632
  </div>
@@ -603,45 +645,54 @@ export const TokenPicker = ({
603
645
  </div>
604
646
  </div>
605
647
  ) : (
606
- <div className="flex flex-col h-full">
607
- <GroupedVirtuoso
608
- style={{ maxHeight: "700px", height: "100%" }}
609
- groupCounts={tokenGroups.map((group) => group.tokens.length)}
610
- groupContent={(index) => (
611
- // Sticky section label: a near-opaque, blurred, widget-glass-colored
612
- // band rows dissolve under it while scrolling instead
613
- // of showing through, and the band still picks up a hint
614
- // of the page glow like the glass around it.
615
- <div className="backdrop-blur-xl bg-gradient-to-r from-t_bg_secondary/40 to-t_bg_secondary/40 bg-t_bg_primary bg-opacity-75">
616
- <p className="pb-3 text-[10px] font-medium leading-4 tracking-[0.015em] text-t_text_primary text-opacity-30 text-left">
617
- {tokenGroups[index]?.label}
618
- </p>
619
- {!tokenGroups[index]?.tokens.length && (
620
- <div className="text-center py-1 pb-3 text-sm text-t_text_primary text-opacity-50">
621
- No token found
622
- </div>
623
- )}
624
- </div>
625
- )}
626
- itemContent={(index) => {
627
- const token = allTokens[index];
628
- return (
629
- token && (
648
+ <div className="flex flex-col h-full min-h-0">
649
+ {/* Pinned label of the group at the top of the list; sits
650
+ above the scroller, so rows clip on its bottom edge. */}
651
+ <p className="pb-3 text-[10px] leading-4 tracking-[0.015em] text-t_text_primary text-opacity-30 text-left shrink-0">
652
+ {pinnedGroupLabel}
653
+ </p>
654
+ <div className="relative flex flex-col flex-1 min-h-0">
655
+ <Virtuoso
656
+ style={{ maxHeight: "700px", height: "100%" }}
657
+ totalCount={listRows.length}
658
+ rangeChanged={({ startIndex }) => setTopRowIndex(startIndex)}
659
+ itemContent={(index) => {
660
+ const row = listRows[index];
661
+ if (!row) {
662
+ return null;
663
+ }
664
+ if (row.kind === "label") {
665
+ return (
666
+ <p className="pt-2 pb-3 text-[10px] leading-4 tracking-[0.015em] text-t_text_primary text-opacity-30 text-left">
667
+ {row.label}
668
+ </p>
669
+ );
670
+ }
671
+ if (row.kind === "empty") {
672
+ return (
673
+ <div className="text-center py-1 pb-3 text-sm text-t_text_primary text-opacity-50">
674
+ No token found
675
+ </div>
676
+ );
677
+ }
678
+ return (
630
679
  <TokenItem
631
680
  panelType={type}
632
- token={token}
681
+ token={row.token}
633
682
  selectedChainId={chain?.chainId}
634
683
  selectedTokenAddress={selectedTokenAddress}
635
- resetWarning={getResetWarning(token)}
684
+ resetWarning={getResetWarning(row.token)}
636
685
  onClick={() => {
637
- onSelect(token);
686
+ onSelect(row.token);
638
687
  setModalOpen(false);
639
688
  }}
640
689
  />
641
- )
642
- );
643
- }}
644
- />
690
+ );
691
+ }}
692
+ />
693
+ {/* Scroll fade — rows dissolve towards the widget bottom */}
694
+ <div className="pointer-events-none absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-t_bg_secondary/65 to-t_bg_secondary/0" />
695
+ </div>
645
696
  </div>
646
697
  )}
647
698
  </>
@@ -1,5 +1,5 @@
1
- import { ChevronDownIcon } from "@src/assets/icons";
2
- import { TokenLogoWithChain } from "@src/components";
1
+ import { ChevronDownThinIcon } from "@src/assets/icons";
2
+ import { Skeleton, TokenLogoWithChain } from "@src/components";
3
3
  import { useSwapContext } from "@src/context";
4
4
  import { Chain, Token } from "@src/models";
5
5
  import { useMemo } from "react";
@@ -19,6 +19,7 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
19
19
  srcTokenLocked,
20
20
  dstChainLocked,
21
21
  setActivePicker,
22
+ isAsyncDataLoaded,
22
23
  } = useSwapContext();
23
24
 
24
25
  const integratorLockedDestinationToken = useMemo(
@@ -43,6 +44,25 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
43
44
  integratorLockedDestinationToken ||
44
45
  integratorLockedSourceToken;
45
46
 
47
+ if (!isAsyncDataLoaded) {
48
+ // Mirror the loaded layout: symbol bar + token circle with the small
49
+ // chain badge, so nothing shifts when the real content arrives.
50
+ return (
51
+ <div
52
+ className={`flex items-center shrink-0 min-h-11 ${className || ""}`}
53
+ aria-hidden
54
+ >
55
+ <div className="flex items-center gap-3">
56
+ <Skeleton className="w-12 h-4" />
57
+ <div className="relative flex items-end pr-1 pb-1">
58
+ <Skeleton className="w-10 h-10 rounded-full" />
59
+ <Skeleton className="absolute bottom-0 right-0 w-[18px] h-[18px] rounded-full border border-solid border-t_bg_secondary" />
60
+ </div>
61
+ </div>
62
+ </div>
63
+ );
64
+ }
65
+
46
66
  return (
47
67
  <button
48
68
  type="button"
@@ -50,7 +70,7 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
50
70
  onClick={() => {
51
71
  setActivePicker(type);
52
72
  }}
53
- className={`flex items-center gap-1 shrink-0 rounded-lg ${
73
+ className={`flex items-center shrink-0 min-h-11 rounded-lg ${
54
74
  locked ? "" : "hover:opacity-80 transition-opacity"
55
75
  } ${className || ""}`}
56
76
  >
@@ -86,8 +106,10 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
86
106
  )}
87
107
  </div>
88
108
  {!locked && (
89
- <div className="flex items-center justify-center w-6 h-6 fill-t_text_primary opacity-50 [&_svg]:w-3 [&_svg]:h-3">
90
- <ChevronDownIcon />
109
+ // The 24px glyph has 8px of built-in whitespace on each side; pull the
110
+ // right side back so the chevron sits flush with the panel edge.
111
+ <div className="flex items-center justify-center w-6 h-6 ml-1 -mr-2 text-t_text_primary opacity-50">
112
+ <ChevronDownThinIcon />
91
113
  </div>
92
114
  )}
93
115
  </button>
@@ -16,11 +16,11 @@ export const UsdValue = ({ type }: Props) => {
16
16
  );
17
17
 
18
18
  if (type === "destination" && isFetchingRoute) {
19
- return <Skeleton className="w-[80px] h-[16px]" />;
19
+ return <Skeleton className="w-[80px] h-[16px] my-1" />;
20
20
  }
21
21
 
22
22
  return (
23
- <p className="text-[10px] leading-4 tracking-[0.01em] text-t_text_primary text-opacity-50 whitespace-nowrap">
23
+ <p className="py-1 text-[10px] leading-4 tracking-[0.01em] text-t_text_primary text-opacity-50 whitespace-nowrap">
24
24
  {valueUsd ? `${valueUsd} USD` : ""}
25
25
  </p>
26
26
  );
@@ -1,4 +1,5 @@
1
- import { ChevronDownIcon } from "@src/assets/icons";
1
+ import { ChevronDownThinSmallIcon } from "@src/assets/icons";
2
+ import { Skeleton } from "@src/components";
2
3
  import { useWallet } from "@src/context/WalletProvider";
3
4
  import { useSwapContext } from "@src/context";
4
5
  import { SwapPanelType } from "../..";
@@ -21,25 +22,28 @@ export const WalletPanel = ({ chain, className }: Props) => {
21
22
  solana,
22
23
  sui,
23
24
  } = useWallet();
24
- const { integrationConfig, hasOnConnectWalletCallback, onConnectWallet } =
25
- useSwapContext();
25
+ const {
26
+ integrationConfig,
27
+ hasOnConnectWalletCallback,
28
+ onConnectWallet,
29
+ isAsyncDataLoaded,
30
+ } = useSwapContext();
26
31
 
27
32
  const openEvmWalletModal = useCallback(() => {
28
33
  // Mirror SwapButton's contract: when the integrator supplied their own
29
34
  // connect handler and did not opt into the built-in picker, defer to it
30
- // (e.g. the host dapp's RainbowKit modal) instead of opening the SDK picker.
35
+ // in every state — the host decides whether to show its connect modal or
36
+ // its account modal (e.g. RainbowKit) for an already-connected wallet.
31
37
  if (!integrationConfig.defaultWalletPicker && hasOnConnectWalletCallback) {
32
38
  onConnectWallet();
33
39
  return;
34
40
  }
35
41
  setOpenEVMWalletModal(true);
36
- evm.connectWallet();
37
42
  }, [
38
43
  integrationConfig.defaultWalletPicker,
39
44
  hasOnConnectWalletCallback,
40
45
  onConnectWallet,
41
46
  setOpenEVMWalletModal,
42
- evm,
43
47
  ]);
44
48
 
45
49
  const openWalletModal = useCallback(() => {
@@ -50,17 +54,10 @@ export const WalletPanel = ({ chain, className }: Props) => {
50
54
  openEvmWalletModal();
51
55
  } else if (chain.ecosystem === "solana") {
52
56
  setOpenSolanaWalletModal(true);
53
- solana.connectWallet();
54
57
  } else if (chain.ecosystem === "sui") {
55
58
  setOpenSuiWalletModal(true);
56
59
  }
57
- }, [
58
- chain,
59
- openEvmWalletModal,
60
- solana,
61
- setOpenSolanaWalletModal,
62
- setOpenSuiWalletModal,
63
- ]);
60
+ }, [chain, openEvmWalletModal, setOpenSolanaWalletModal, setOpenSuiWalletModal]);
64
61
 
65
62
  const address = useMemo(() => {
66
63
  if (!chain) {
@@ -76,23 +73,31 @@ export const WalletPanel = ({ chain, className }: Props) => {
76
73
  return "";
77
74
  }, [chain, evm, solana, sui]);
78
75
 
76
+ if (!isAsyncDataLoaded) {
77
+ return <Skeleton className="w-16 h-6" />;
78
+ }
79
+
80
+ // Reserve the button's height before the chain loads so the card
81
+ // doesn't grow when the wallet button appears.
79
82
  if (!chain) {
80
- return null;
83
+ return <div className="h-6" aria-hidden />;
81
84
  }
82
85
 
83
86
  return (
84
87
  <button
85
88
  type="button"
86
89
  onClick={openWalletModal}
87
- className={`flex items-center justify-center gap-0.5 px-2 py-1 rounded bg-t_bg_tertiary bg-opacity-5 hover:bg-opacity-10 transition-colors ${
90
+ className={`flex items-center justify-center px-2 py-1 rounded bg-t_bg_tertiary bg-opacity-5 hover:bg-opacity-10 transition-colors ${
88
91
  className || ""
89
92
  }`}
90
93
  >
91
94
  <p className="text-[10px] leading-4 tracking-[0.01em] text-t_text_primary whitespace-nowrap">
92
95
  {address ? shortAddress(address) : "Connect"}
93
96
  </p>
94
- <div className="flex items-center justify-center w-4 h-4 fill-t_text_primary opacity-50 [&_svg]:w-2.5 [&_svg]:h-2.5">
95
- <ChevronDownIcon />
97
+ {/* The 16px glyph has 6px of built-in whitespace on each side; pull the
98
+ right side back so the button's visual padding stays symmetric. */}
99
+ <div className="flex items-center justify-center w-4 h-4 -mr-1.5 text-t_text_primary opacity-50">
100
+ <ChevronDownThinSmallIcon />
96
101
  </div>
97
102
  </button>
98
103
  );
@@ -1,5 +1,5 @@
1
1
  import { useSwapContext } from "@src/context";
2
- import { useMemo } from "react";
2
+ import { MouseEvent, useMemo } from "react";
3
3
  import { SwapPanelType } from "../index";
4
4
  import { AmountPanel } from "./AmountPanel";
5
5
  import { Balance } from "./AmountPanel/Balance";
@@ -23,15 +23,27 @@ export const SwapPanel = ({ type }: Props) => {
23
23
  [type, srcChain, dstChain],
24
24
  );
25
25
 
26
+ // The whole source card acts as the amount input's hit area; clicks on the
27
+ // nested buttons (wallet, MAX, token picker) must keep their own actions.
28
+ const focusAmountInput = (event: MouseEvent<HTMLDivElement>) => {
29
+ if (type !== "source" || (event.target as HTMLElement).closest("button")) {
30
+ return;
31
+ }
32
+ event.currentTarget
33
+ .querySelector<HTMLInputElement>("#swap-amount-input")
34
+ ?.focus();
35
+ };
36
+
26
37
  return (
27
38
  <div
28
- className={`flex flex-col gap-2 px-4 sm:px-6 py-4 rounded-2xl bg-t_bg_secondary bg-opacity-60 ${
39
+ onClick={focusAmountInput}
40
+ className={`flex flex-col gap-2 px-4 sm:px-6 py-4 rounded-2xl backdrop-blur-[12px] ${
29
41
  type === "source"
30
- ? "bg-gradient-to-r from-t_bg_tertiary/5 to-t_bg_tertiary/5 border border-solid border-t_text_primary border-opacity-25"
42
+ ? "bg-gradient-to-r from-t_bg_tertiary/5 to-t_bg_tertiary/5 swap-panel-gradient-border cursor-text"
31
43
  : "bg-gradient-to-r from-t_bg_tertiary/[0.03] to-t_bg_tertiary/[0.03]"
32
44
  }`}
33
45
  >
34
- <div className="flex gap-2 items-center w-full">
46
+ <div className="flex gap-2 items-center w-full min-h-6">
35
47
  <p className="text-[10px] leading-4 tracking-[0.01em] text-t_text_primary text-opacity-50">
36
48
  {type === "source" ? "Source" : "Destination"}
37
49
  </p>
@@ -41,7 +53,7 @@ export const SwapPanel = ({ type }: Props) => {
41
53
  <AmountPanel type={type} />
42
54
  <TokenPanel chain={chain} token={token} type={type} />
43
55
  </div>
44
- <div className="flex items-center justify-between w-full gap-4">
56
+ <div className="flex items-center justify-between w-full gap-4 min-h-6">
45
57
  <UsdValue type={type} />
46
58
  <Balance type={type} />
47
59
  </div>