@stableflow/pay-widgets 0.2.0-beta-90dffc3d2f-20261012 → 0.2.0

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.
@@ -9,11 +9,29 @@ import { useSyncExternalStore } from "react";
9
9
  var TOKEN_BALANCE_POLL_MS = 6e4;
10
10
  var ALL_CHAIN_FILTER = "all";
11
11
  var DEFAULT_STABLE_SYMBOLS = ["USDT", "USDC"];
12
- var POPULAR_SECTION_TITLE = "Popular on your network";
13
12
  var TOKEN_SELECT_PREFS_STORAGE_KEY = "stableflow-pay:token-select-prefs:v1";
14
- var TOKEN_SELECT_PREFS_VERSION = 2;
13
+ var TOKEN_SELECT_PREFS_VERSION = 3;
15
14
  var MAX_RECENT_BLOCKCHAINS = 16;
16
- var MAX_RECENT_ASSETS = 3;
15
+ var MAX_RECENTLY_USED = 5;
16
+ var MAX_RECENT_SEARCHES = 5;
17
+ var MAX_RECENT_HISTORY = 50;
18
+ var YOUR_TOKENS_TITLE = "Your Tokens";
19
+ var RECENT_SEARCHES_TITLE = "Recent Searches";
20
+ var TRENDS_TOKEN_TITLE = "Trends Token";
21
+ var ALL_CHAIN_PREVIEW_BLOCKCHAINS = ["eth", "near", "sol", "tron"];
22
+ var DESKTOP_MEDIA_QUERY = "(min-width: 768px)";
23
+ var TRENDS_TOKENS = [
24
+ { blockchain: "tron", symbol: "USDT" },
25
+ { blockchain: "eth", symbol: "USDT" },
26
+ { blockchain: "eth", symbol: "USDC" },
27
+ { blockchain: "sol", symbol: "USDC" },
28
+ { blockchain: "sol", symbol: "USDT" },
29
+ { blockchain: "pol", symbol: "USDC" },
30
+ { blockchain: "bsc", symbol: "USDT" },
31
+ { blockchain: "arb", symbol: "USDT" },
32
+ { blockchain: "arb", symbol: "USDC" },
33
+ { blockchain: "near", symbol: "USDT" }
34
+ ];
17
35
  var CHAIN_KIND_LABELS = {
18
36
  evm: "EVM",
19
37
  near: "Near",
@@ -247,7 +265,6 @@ function PayWidgetsProvider(props) {
247
265
  const {
248
266
  tokens,
249
267
  loading = false,
250
- popularTokens = [],
251
268
  wallet,
252
269
  readBalances,
253
270
  getBalance,
@@ -278,7 +295,6 @@ function PayWidgetsProvider(props) {
278
295
  tokens,
279
296
  loading,
280
297
  wallet,
281
- popularTokens,
282
298
  stableSymbols,
283
299
  chainKindLabels,
284
300
  onCopyAddress,
@@ -297,17 +313,32 @@ function usePayWidgets() {
297
313
  }
298
314
 
299
315
  // src/token-select/TokenSelectDialog.tsx
300
- import { useEffect as useEffect3, useLayoutEffect, useMemo, useRef as useRef2, useState as useState2, useSyncExternalStore as useSyncExternalStore3 } from "react";
316
+ import { useEffect as useEffect5, useMemo, useRef as useRef3, useState as useState4, useSyncExternalStore as useSyncExternalStore3 } from "react";
301
317
  import { Dialog } from "@stableflow/pay-ui/dialog";
302
- import { SearchInput } from "@stableflow/pay-ui/search-input";
318
+ import { SearchInput as SearchInput2 } from "@stableflow/pay-ui/search-input";
303
319
 
304
- // src/token-select/chain-pane.tsx
305
- import { IconChevron } from "@stableflow/pay-ui/icons/chevron";
320
+ // src/token-select/chain-select.tsx
321
+ import { useEffect as useEffect3, useRef, useState as useState2 } from "react";
322
+ import { IconCheck } from "@stableflow/pay-ui/icons/outline/check";
323
+ import { IconChevron } from "@stableflow/pay-ui/icons/outline/chevron";
324
+ import { Drawer } from "@stableflow/pay-ui/drawer";
325
+ import { SearchInput } from "@stableflow/pay-ui/search-input";
306
326
  import { Tooltip } from "@stableflow/pay-ui/tooltip";
307
327
 
308
- // src/token-select/chain-wallet-status.tsx
309
- import { IconCopy } from "@stableflow/pay-ui/icons/copy";
310
- import { IconLogout } from "@stableflow/pay-ui/icons/logout";
328
+ // src/token-select/chain-stack.tsx
329
+ import { jsx as jsx2 } from "react/jsx-runtime";
330
+ function ChainStack(props) {
331
+ const { chains, logoClassName } = props;
332
+ return /* @__PURE__ */ jsx2("span", { className: "grid grid-cols-2 gap-0.5", children: chains.slice(0, 4).map((chain) => /* @__PURE__ */ jsx2(
333
+ "img",
334
+ {
335
+ src: chain.logo,
336
+ alt: "",
337
+ className: logoClassName
338
+ },
339
+ chain.blockchain
340
+ )) });
341
+ }
311
342
 
312
343
  // src/token-select/format.ts
313
344
  import { clsx } from "clsx";
@@ -317,12 +348,6 @@ var thinScrollbar = "[&::-webkit-scrollbar]:w-[4px] [&::-webkit-scrollbar-thumb]
317
348
  function cx(...inputs) {
318
349
  return twMerge(clsx(inputs));
319
350
  }
320
- function formatAddress(address, prefix = 4, suffix = 5) {
321
- if (!address) return "";
322
- if (!address.startsWith("0x") && address.length <= 32) return address;
323
- if (address.length <= prefix + suffix) return address;
324
- return `${address.slice(0, prefix)}...${address.slice(-suffix)}`;
325
- }
326
351
  function chainKindLabel(kind, labels) {
327
352
  return labels?.[kind] ?? CHAIN_KIND_LABELS[kind] ?? kind;
328
353
  }
@@ -346,73 +371,21 @@ function formatDisplayAmount(value, options = {}) {
346
371
  return `${prefix}${negative ? "-" : ""}${body}`;
347
372
  }
348
373
 
349
- // src/token-select/chain-wallet-status.tsx
350
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
351
- function stop(event) {
352
- event.preventDefault();
353
- event.stopPropagation();
354
- }
355
- function ChainWalletStatus({ kind }) {
356
- const { wallet, onCopyAddress } = usePayWidgets();
357
- const account = wallet.accounts[kind];
358
- const address = account?.address?.trim() || "";
359
- const hideDisconnect = account?.canDisconnect === false;
360
- if (!address) {
361
- return /* @__PURE__ */ jsx2(
362
- "button",
363
- {
364
- type: "button",
365
- onClick: (event) => {
366
- stop(event);
367
- void wallet.connect(kind);
368
- },
369
- className: cx("shrink-0 cursor-pointer border-0 bg-transparent p-0 text-xs font-medium text-[#3F8AFB] hover:underline", fontSans),
370
- children: account?.connecting ? "Connecting\u2026" : "Connect"
371
- }
372
- );
373
- }
374
- return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1", onClick: stop, children: [
375
- /* @__PURE__ */ jsx2(
376
- "button",
377
- {
378
- type: "button",
379
- onClick: async (event) => {
380
- stop(event);
381
- if (!onCopyAddress) return;
382
- onCopyAddress(address);
383
- },
384
- className: cx("truncate border-0 bg-transparent p-0 text-xs text-[#606060] hover:text-black", fontSans),
385
- children: formatAddress(address)
386
- }
387
- ),
388
- /* @__PURE__ */ jsx2(
389
- "button",
390
- {
391
- type: "button",
392
- "aria-label": "Copy address",
393
- onClick: (event) => {
394
- stop(event);
395
- if (!onCopyAddress) return;
396
- onCopyAddress(address);
397
- },
398
- className: "inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-[#606060] hover:text-black",
399
- children: /* @__PURE__ */ jsx2(IconCopy, { className: "size-3" })
400
- }
401
- ),
402
- hideDisconnect ? null : /* @__PURE__ */ jsx2(
403
- "button",
404
- {
405
- type: "button",
406
- "aria-label": "Disconnect",
407
- onClick: (event) => {
408
- stop(event);
409
- void wallet.disconnect(kind);
410
- },
411
- className: "inline-flex shrink-0 cursor-pointer border-0 bg-transparent p-0 text-[#ff5656]",
412
- children: /* @__PURE__ */ jsx2(IconLogout, { className: "size-3" })
413
- }
414
- )
415
- ] });
374
+ // src/token-select/use-desktop.ts
375
+ import { useEffect as useEffect2, useState } from "react";
376
+ function useDesktop() {
377
+ const [matches, setMatches] = useState(() => {
378
+ if (typeof window === "undefined") return true;
379
+ return window.matchMedia(DESKTOP_MEDIA_QUERY).matches;
380
+ });
381
+ useEffect2(() => {
382
+ const media = window.matchMedia(DESKTOP_MEDIA_QUERY);
383
+ const onChange = () => setMatches(media.matches);
384
+ onChange();
385
+ media.addEventListener("change", onChange);
386
+ return () => media.removeEventListener("change", onChange);
387
+ }, []);
388
+ return matches;
416
389
  }
417
390
 
418
391
  // src/token-select/utils.ts
@@ -439,6 +412,10 @@ function tokenMatchesSearch(token, query) {
439
412
  if (token.providerSymbol.toLowerCase().includes(q)) return true;
440
413
  return Boolean(token.contractAddress?.toLowerCase().includes(q));
441
414
  }
415
+ function matchesChainFilter(token, chainFilter) {
416
+ if (chainFilter === ALL_CHAIN_FILTER) return true;
417
+ return token.blockchain === chainFilter;
418
+ }
442
419
  function formattedBalancePositive(formatted) {
443
420
  if (formatted == null || formatted === "") return false;
444
421
  const amount = Number(formatted);
@@ -458,29 +435,6 @@ function chainBalanceUsd(blockchain, tokens, getBalanceUsd) {
458
435
  }
459
436
  return total;
460
437
  }
461
- function parsePopularTokens(raw) {
462
- if (!raw?.trim()) return [];
463
- const refs = [];
464
- for (const part of raw.split(",")) {
465
- const trimmed = part.trim();
466
- const colon = trimmed.indexOf(":");
467
- if (colon <= 0 || colon >= trimmed.length - 1) continue;
468
- const blockchain = trimmed.slice(0, colon).trim().toLowerCase();
469
- const symbol = trimmed.slice(colon + 1).trim();
470
- if (!blockchain || !symbol || symbol.includes(":")) continue;
471
- refs.push({ blockchain, symbol: symbol.toUpperCase() });
472
- }
473
- return refs;
474
- }
475
- function popularTokensForWallets(tokens, configured, isChainConnected) {
476
- const result = [];
477
- for (const ref of configured) {
478
- const token = tokens.find((item) => item.blockchain === ref.blockchain && item.symbol.toUpperCase() === ref.symbol);
479
- if (!token || !isChainConnected(token.chain.chainKind)) continue;
480
- result.push(token);
481
- }
482
- return result;
483
- }
484
438
  function sortChainsAlpha(chains) {
485
439
  return chains.slice().sort((left, right) => left.chainName.localeCompare(right.chainName));
486
440
  }
@@ -540,6 +494,9 @@ function sortTokensByBalance(tokens, getBalanceUsd, isLoading) {
540
494
  return compareTokensBySymbol(left, right);
541
495
  });
542
496
  }
497
+ function sortTokensBySymbol(tokens) {
498
+ return tokens.slice().sort(compareTokensBySymbol);
499
+ }
543
500
  function positiveBalanceTokens(tokens, getBalanceUsd) {
544
501
  return sortTokensByBalance(
545
502
  tokens.filter((token) => getBalanceUsd(token) > 0),
@@ -588,48 +545,54 @@ function sortTokensForChain(tokens, options) {
588
545
  return compareTokensByPriority(left, right, stableSymbols);
589
546
  });
590
547
  }
591
- var EVM_SEARCH_GROUP = "evm";
592
- var EVM_SEARCH_GROUP_NAME = "EVM-based";
593
- function groupSearchResults(tokens, query, options) {
594
- const matched = tokens.filter((token) => tokenMatchesSearch(token, query));
595
- const groups = /* @__PURE__ */ new Map();
596
- for (const token of matched) {
597
- const key = token.chain.chainKind === EVM_SEARCH_GROUP ? EVM_SEARCH_GROUP : token.blockchain;
598
- const list = groups.get(key) ?? [];
599
- list.push(token);
600
- groups.set(key, list);
548
+ function trendTokensIn(tokens, options) {
549
+ const trends = options.trends ?? TRENDS_TOKENS;
550
+ const inScope = tokens.filter((token) => matchesChainFilter(token, options.chainFilter));
551
+ const used = /* @__PURE__ */ new Set();
552
+ const head = [];
553
+ for (const ref of trends) {
554
+ if (options.chainFilter !== ALL_CHAIN_FILTER && ref.blockchain.toLowerCase() !== options.chainFilter.toLowerCase()) continue;
555
+ const match = inScope.find((token) => !used.has(token) && token.blockchain.toLowerCase() === ref.blockchain.toLowerCase() && token.symbol.toLowerCase() === ref.symbol.toLowerCase());
556
+ if (!match) continue;
557
+ used.add(match);
558
+ head.push(match);
601
559
  }
602
- const stableSymbols = options.stableSymbols ?? DEFAULT_STABLE_SYMBOLS;
603
- const built = Array.from(groups.entries()).map(([key, groupTokens]) => {
604
- const chain = groupTokens[0].chain;
605
- const folded = key === EVM_SEARCH_GROUP;
606
- const sorted = options.role === "payer" ? sortTokensForChain(groupTokens, {
607
- withBalances: options.hasOwner(chain.chainKind),
608
- usdOf: options.usdOf,
609
- loadingOf: options.loadingOf,
610
- stableSymbols
611
- }) : groupTokens.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols));
612
- return {
613
- blockchain: folded ? EVM_SEARCH_GROUP : key,
614
- chainName: folded ? EVM_SEARCH_GROUP_NAME : chain.chainName,
615
- chainKind: chain.chainKind,
616
- tokens: sorted,
617
- usd: sorted.reduce((total, token) => {
618
- const usd = options.usdOf(token);
619
- return usd > 0 ? total + usd : total;
620
- }, 0)
621
- };
622
- });
623
- built.sort((left, right) => {
624
- if (options.role === "payer" && right.usd !== left.usd) return right.usd - left.usd;
625
- return left.chainName.localeCompare(right.chainName);
626
- });
627
- return built.map(({ blockchain, chainName, chainKind, tokens: groupTokens }) => ({
628
- blockchain,
629
- chainName,
630
- chainKind,
631
- tokens: groupTokens
632
- }));
560
+ return [...head, ...options.sortRest(inScope.filter((token) => !used.has(token)))];
561
+ }
562
+ function allChainPreview(chains) {
563
+ if (chains.length <= 4) return chains.slice();
564
+ const byCode = new Map(chains.map((chain) => [chain.blockchain.toLowerCase(), chain]));
565
+ const used = /* @__PURE__ */ new Set();
566
+ const picked = [];
567
+ const take = (chain) => {
568
+ if (!chain || picked.length >= 4) return;
569
+ const code = chain.blockchain.toLowerCase();
570
+ if (used.has(code)) return;
571
+ used.add(code);
572
+ picked.push(chain);
573
+ };
574
+ const eth = byCode.get(ALL_CHAIN_PREVIEW_BLOCKCHAINS[0]);
575
+ if (eth) take(eth);
576
+ else take(chains.find((chain) => chain.chainKind === "evm"));
577
+ take(byCode.get("near"));
578
+ take(byCode.get("sol"));
579
+ take(byCode.get("tron"));
580
+ const fill = chains.filter((chain) => chain.chainKind === "evm" && !used.has(chain.blockchain.toLowerCase())).slice().sort((left, right) => left.chainName.localeCompare(right.chainName));
581
+ for (const chain of fill) take(chain);
582
+ return picked;
583
+ }
584
+ function chainMenuOrder(chains, tokens, options) {
585
+ const dotted = chains.filter((chain) => options.dotted.has(chain.blockchain));
586
+ const rest = chains.filter((chain) => !options.dotted.has(chain.blockchain));
587
+ return [
588
+ ...sortChainsForSidebar(dotted, [...tokens], options),
589
+ ...sortChainsAlpha(rest)
590
+ ];
591
+ }
592
+ function searchTokens(tokens, query, options) {
593
+ const matched = tokens.filter((token) => tokenMatchesSearch(token, query));
594
+ if (options.role === "payer") return sortTokensByBalance(matched, options.usdOf, options.loadingOf);
595
+ return sortTokensBySymbol(matched);
633
596
  }
634
597
  function recentTokensInOrder(tokens, recentAssetIds) {
635
598
  const byId = new Map(tokens.map((token) => [token.assetId, token]));
@@ -641,220 +604,260 @@ function recentTokensInOrder(tokens, recentAssetIds) {
641
604
  return result;
642
605
  }
643
606
 
644
- // src/token-select/chain-pane.tsx
645
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
646
- function lockReason(lockChainKind, labels) {
647
- return `Recipient address is on ${chainKindLabel(lockChainKind, labels)}; edit the recipient to change chain`;
648
- }
649
- function ChainPane(props) {
607
+ // src/token-select/chain-select.tsx
608
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
609
+ var SEARCH_FIELD_CLASS = "h-[42px] rounded-[12px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30";
610
+ var SEARCH_ICON_CLASS = "left-4 size-[19px] text-black";
611
+ function ChainSelect(props) {
650
612
  const {
651
613
  chains,
652
- showWalletStatus = true,
653
- onSelectChain,
614
+ previewChains,
615
+ dotted,
616
+ value,
617
+ onChange,
654
618
  lockChainKind = null,
655
619
  disabledBlockchains = null,
656
- disabledReason,
657
- scrollRef
620
+ disabledReason
658
621
  } = props;
659
622
  const { chainKindLabels } = usePayWidgets();
660
- function wrapDisabled(disabled, reason, content) {
661
- if (!disabled || !reason) return content;
662
- return /* @__PURE__ */ jsx3(Tooltip, { side: "right", triggerClassName: "min-w-0 flex-1", content: reason, children: content });
623
+ const isDesktop = useDesktop();
624
+ const [open, setOpen] = useState2(false);
625
+ const [query, setQuery] = useState2("");
626
+ const rootRef = useRef(null);
627
+ const selected = value === ALL_CHAIN_FILTER ? void 0 : chains.find((chain) => chain.blockchain === value);
628
+ useEffect3(() => {
629
+ if (!open) return;
630
+ setQuery("");
631
+ }, [open]);
632
+ useEffect3(() => {
633
+ if (!open || !isDesktop) return;
634
+ const onPointerDown = (event) => {
635
+ const target = event.target;
636
+ if (!(target instanceof Node) || rootRef.current?.contains(target)) return;
637
+ setOpen(false);
638
+ };
639
+ const onKeyDown = (event) => {
640
+ if (event.key !== "Escape") return;
641
+ event.stopPropagation();
642
+ event.preventDefault();
643
+ setOpen(false);
644
+ };
645
+ document.addEventListener("pointerdown", onPointerDown);
646
+ document.addEventListener("keydown", onKeyDown, true);
647
+ return () => {
648
+ document.removeEventListener("pointerdown", onPointerDown);
649
+ document.removeEventListener("keydown", onKeyDown, true);
650
+ };
651
+ }, [open, isDesktop]);
652
+ function reasonFor(chain) {
653
+ if (isBlockchainDisabled(chain.blockchain, disabledBlockchains)) return disabledReason;
654
+ if (isChainKindLocked(chain.chainKind, lockChainKind) && lockChainKind) {
655
+ return `Recipient address is on ${chainKindLabel(lockChainKind, chainKindLabels)}; edit the recipient to change chain`;
656
+ }
657
+ return void 0;
663
658
  }
664
- return /* @__PURE__ */ jsx3("div", { className: "flex h-full min-h-0 flex-col", children: /* @__PURE__ */ jsx3("div", { ref: scrollRef, className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs2("div", { className: "px-2.5 pt-[17px] pb-3", children: [
665
- /* @__PURE__ */ jsx3("p", { className: cx("px-3.5 text-sm font-medium text-[#aaa]", fontSans), children: "All Networks" }),
666
- /* @__PURE__ */ jsx3("div", { className: "mt-1", children: chains.map((chain) => {
667
- const locked = isChainKindLocked(chain.chainKind, lockChainKind);
668
- const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
669
- const disabled = locked || chainDisabled;
670
- const reason = chainDisabled ? disabledReason : locked && lockChainKind ? lockReason(lockChainKind, chainKindLabels) : void 0;
671
- return /* @__PURE__ */ jsxs2(
672
- "div",
673
- {
674
- className: cx(
675
- "flex h-[66px] w-full items-center gap-4 rounded-[12px] px-3.5",
676
- disabled ? "opacity-40" : "hover:bg-[#f6f6f6]"
677
- ),
678
- children: [
679
- wrapDisabled(
680
- disabled,
681
- reason,
682
- /* @__PURE__ */ jsxs2(
683
- "button",
684
- {
685
- type: "button",
686
- disabled,
687
- onClick: () => {
688
- if (disabled) return;
689
- onSelectChain(chain.blockchain);
690
- },
691
- className: cx(
692
- "flex min-w-0 flex-1 items-center gap-3 border-0 bg-transparent text-left",
693
- disabled ? "cursor-not-allowed" : "cursor-pointer"
694
- ),
695
- children: [
696
- /* @__PURE__ */ jsx3("img", { src: chain.logo, alt: "", className: "size-8 shrink-0 object-cover" }),
697
- /* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black", fontSans), children: chain.chainName })
698
- ]
699
- }
700
- )
701
- ),
702
- showWalletStatus ? /* @__PURE__ */ jsx3(ChainWalletStatus, { kind: chain.chainKind }) : /* @__PURE__ */ jsx3("span", { className: "min-w-0 flex-1" }),
703
- /* @__PURE__ */ jsx3(
704
- "button",
705
- {
706
- type: "button",
707
- "aria-label": `Open ${chain.chainName}`,
708
- disabled,
709
- onClick: () => {
710
- if (disabled) return;
711
- onSelectChain(chain.blockchain);
712
- },
713
- className: cx(
714
- "inline-flex shrink-0 border-0 bg-transparent p-0 text-[#606060]",
715
- disabled ? "cursor-not-allowed" : "cursor-pointer"
716
- ),
717
- children: /* @__PURE__ */ jsx3(IconChevron, {})
718
- }
719
- )
720
- ]
721
- },
722
- chain.blockchain
723
- );
724
- }) })
725
- ] }) }) });
726
- }
727
-
728
- // src/token-select/network-rail.tsx
729
- import { IconAllNetworks } from "@stableflow/pay-ui/icons/all-networks";
730
- import { IconWallet } from "@stableflow/pay-ui/icons/wallet";
731
- import { Tooltip as Tooltip2 } from "@stableflow/pay-ui/tooltip";
732
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
733
- function chipClass(selected, disabled) {
734
- return cx(
735
- "relative flex size-[50px] shrink-0 items-center justify-center rounded-[12px] border border-solid",
736
- selected ? "border-[#06f] bg-[rgba(0,102,255,0.1)]" : "border-transparent bg-transparent",
737
- disabled ? "cursor-not-allowed opacity-40" : "cursor-pointer hover:bg-black/5"
659
+ const trimmed = query.trim().toLowerCase();
660
+ const showAll = !trimmed || "all".includes(trimmed);
661
+ const visible = chains.filter((chain) => {
662
+ if (!trimmed) return true;
663
+ return chain.chainName.toLowerCase().includes(trimmed) || chain.blockchain.toLowerCase().includes(trimmed);
664
+ });
665
+ const empty = !showAll && visible.length === 0;
666
+ function select(blockchain) {
667
+ onChange(blockchain);
668
+ setOpen(false);
669
+ }
670
+ const menu = /* @__PURE__ */ jsx3(
671
+ ChainMenu,
672
+ {
673
+ showAll,
674
+ chains: visible,
675
+ previewChains,
676
+ dotted,
677
+ value,
678
+ query,
679
+ onQueryChange: setQuery,
680
+ empty,
681
+ isDesktop,
682
+ reasonFor,
683
+ onSelect: select
684
+ }
738
685
  );
686
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: "relative shrink-0", children: [
687
+ /* @__PURE__ */ jsxs(
688
+ "button",
689
+ {
690
+ type: "button",
691
+ "aria-haspopup": "listbox",
692
+ "aria-expanded": open,
693
+ onClick: () => setOpen((current) => !current),
694
+ className: "flex h-[50px] w-[70px] cursor-pointer items-center justify-between pl-[9px] pr-[12px] gap-1.5 rounded-[12px] border border-[#e0e0e0] bg-[#fdfdfd] shadow-[0_0_40px_0_rgba(0,0,0,0.06)]",
695
+ children: [
696
+ selected ? /* @__PURE__ */ jsx3("img", { src: selected.logo, alt: "", className: "size-7 rounded-[8px] object-cover" }) : /* @__PURE__ */ jsx3(ChainStack, { chains: previewChains, logoClassName: "size-[12px] rounded-[2px] object-cover" }),
697
+ /* @__PURE__ */ jsx3(IconChevron, { className: cx("size-2.5 text-black duration-150", open ? "-rotate-90" : "rotate-90") })
698
+ ]
699
+ }
700
+ ),
701
+ open && isDesktop ? /* @__PURE__ */ jsx3(
702
+ "div",
703
+ {
704
+ role: "listbox",
705
+ "aria-label": "Select network",
706
+ onKeyDown: (event) => {
707
+ if (event.key !== "Escape") return;
708
+ event.stopPropagation();
709
+ event.preventDefault();
710
+ setOpen(false);
711
+ },
712
+ className: "absolute top-[59px] right-0 z-20 flex max-h-[483px] w-[302px] flex-col rounded-[12px] border border-[#e0e0e0] bg-[#fdfdfd] p-4 shadow-[0_0_40px_0_rgba(0,0,0,0.06)]",
713
+ children: menu
714
+ }
715
+ ) : null,
716
+ !isDesktop ? /* @__PURE__ */ jsx3(
717
+ Drawer,
718
+ {
719
+ open,
720
+ onClose: () => setOpen(false),
721
+ title: "Select Network",
722
+ titleClassName: cx("text-lg font-semibold text-black", fontSans),
723
+ cardClassName: "max-h-[80vh]",
724
+ contentClassName: "overflow-hidden",
725
+ children: /* @__PURE__ */ jsx3("div", { className: "flex h-[min(483px,60vh)] min-h-0 flex-col", children: menu })
726
+ }
727
+ ) : null
728
+ ] });
739
729
  }
740
- function NetworkRail(props) {
730
+ function ChainMenu(props) {
741
731
  const {
742
- role,
743
- walletConnected,
744
- selection,
732
+ showAll,
745
733
  chains,
746
- fundedBlockchains,
747
- lockChainKind = null,
748
- disabledBlockchains = null,
749
- disabledReason,
750
- onSelectWallet,
751
- onSelectAll,
752
- onSelectNetworks,
753
- onSelectChain,
754
- className
734
+ previewChains,
735
+ dotted,
736
+ value,
737
+ query,
738
+ onQueryChange,
739
+ empty,
740
+ isDesktop,
741
+ reasonFor,
742
+ onSelect
755
743
  } = props;
756
- const { chainKindLabels } = usePayWidgets();
757
- const payer = role === "payer";
758
- const networksSelected = selection.kind === "networks";
759
- const allSelected = selection.kind === "all";
760
- const walletSelected = selection.kind === "wallet";
761
- function wrapDisabled(disabled, reason, content) {
762
- if (!disabled || !reason) return content;
763
- return /* @__PURE__ */ jsx4(Tooltip2, { side: "right", content: reason, children: content });
764
- }
765
- function chainButton(chain) {
766
- const locked = isChainKindLocked(chain.chainKind, lockChainKind);
767
- const chainDisabled = isBlockchainDisabled(chain.blockchain, disabledBlockchains);
768
- const disabled = locked || chainDisabled;
769
- const reason = chainDisabled ? disabledReason : locked ? `Recipient address is on ${chainKindLabel(lockChainKind, chainKindLabels)}; edit the recipient to change chain` : void 0;
770
- const selected = selection.kind === "chain" && selection.blockchain === chain.blockchain;
771
- return /* @__PURE__ */ jsx4("span", { children: wrapDisabled(
772
- disabled,
773
- reason,
774
- /* @__PURE__ */ jsxs3(
775
- "button",
744
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
745
+ /* @__PURE__ */ jsx3(
746
+ SearchInput,
747
+ {
748
+ value: query,
749
+ onChange: onQueryChange,
750
+ placeholder: "search network",
751
+ iconClassName: SEARCH_ICON_CLASS,
752
+ inputClassName: cx(SEARCH_FIELD_CLASS, "pl-[43px]")
753
+ }
754
+ ),
755
+ /* @__PURE__ */ jsxs("div", { className: cx("mt-2.5 min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: [
756
+ showAll ? /* @__PURE__ */ jsx3(
757
+ ChainOption,
776
758
  {
777
- type: "button",
778
- "aria-label": chain.chainName,
779
- disabled,
780
- onClick: () => {
781
- if (disabled) return;
782
- onSelectChain(chain.blockchain);
783
- },
784
- className: chipClass(selected, disabled),
785
- children: [
786
- /* @__PURE__ */ jsx4("img", { src: chain.logo, alt: "", className: "size-8 object-cover" }),
787
- fundedBlockchains.has(chain.blockchain) ? /* @__PURE__ */ jsx4("span", { className: "absolute top-1 right-1 size-2.5 rounded-full border border-white bg-[#06f]" }) : null
788
- ]
759
+ selected: value === ALL_CHAIN_FILTER,
760
+ onSelect: () => onSelect(ALL_CHAIN_FILTER),
761
+ label: "All",
762
+ children: /* @__PURE__ */ jsx3(ChainStack, { chains: previewChains, logoClassName: "size-[15px] rounded-[4px] object-cover" })
789
763
  }
790
- )
791
- ) }, chain.blockchain);
792
- }
793
- return /* @__PURE__ */ jsxs3("div", { className: cx("box-border flex h-full w-[76px] shrink-0 flex-col items-center border-r border-[#e3e3e3] pt-3", className), children: [
794
- payer && walletConnected ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "Wallet", onClick: onSelectWallet, className: chipClass(walletSelected, false), children: /* @__PURE__ */ jsx4(IconWallet, { className: walletSelected ? "size-5 text-[#06f]" : "size-5 text-black" }) }) : null,
795
- payer && !walletConnected ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "All networks", onClick: onSelectNetworks, className: chipClass(networksSelected, false), children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-5 text-black" }) }) : null,
796
- !payer ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "All", onClick: onSelectAll, className: chipClass(allSelected, false), children: /* @__PURE__ */ jsx4("span", { className: cx("text-sm font-medium text-black", fontSans), children: "All" }) }) : null,
797
- payer && walletConnected ? /* @__PURE__ */ jsxs3("div", { className: cx("mt-3.5 flex min-h-0 w-full flex-1 flex-col items-center gap-3.5 overflow-y-auto", thinScrollbar), children: [
798
- chains.map((chain) => chainButton(chain)),
799
- /* @__PURE__ */ jsx4("div", { className: "h-px w-8 shrink-0 bg-[#e3e3e3]" }),
800
- /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "All networks", onClick: onSelectNetworks, className: chipClass(networksSelected, false), children: /* @__PURE__ */ jsx4(IconAllNetworks, { className: "size-5 text-black" }) }),
801
- /* @__PURE__ */ jsx4("div", { className: "pb-3" })
802
- ] }) : null,
803
- !payer ? /* @__PURE__ */ jsxs3("div", { className: cx("mt-3.5 flex min-h-0 w-full flex-1 flex-col items-center gap-3.5 overflow-y-auto", thinScrollbar), children: [
804
- chains.map((chain) => chainButton(chain)),
805
- /* @__PURE__ */ jsx4("div", { className: "pb-3" })
806
- ] }) : null
764
+ ) : null,
765
+ chains.map((chain) => {
766
+ const reason = reasonFor(chain);
767
+ const disabled = Boolean(reason);
768
+ return /* @__PURE__ */ jsx3(
769
+ ChainOption,
770
+ {
771
+ selected: value === chain.blockchain,
772
+ disabled,
773
+ reason,
774
+ showReason: !isDesktop,
775
+ dotted: dotted.has(chain.blockchain),
776
+ onSelect: () => onSelect(chain.blockchain),
777
+ label: chain.chainName,
778
+ children: /* @__PURE__ */ jsx3("img", { src: chain.logo, alt: "", className: "size-8 rounded-[8px] object-cover" })
779
+ },
780
+ chain.blockchain
781
+ );
782
+ }),
783
+ empty ? /* @__PURE__ */ jsx3("p", { className: cx("px-[13px] py-4 text-[13px] text-[#606060]", fontSans), children: "No networks found" }) : null
784
+ ] })
807
785
  ] });
808
786
  }
809
-
810
- // src/token-select/pane-transition.tsx
811
- import { AnimatePresence, motion, useReducedMotion } from "motion/react";
812
- import { jsx as jsx5 } from "react/jsx-runtime";
813
- var EASE = [0.32, 0.72, 0, 1];
814
- var variants = {
815
- enter: (direction) => ({ x: direction > 0 ? 24 : -24, opacity: 0 }),
816
- center: { x: 0, opacity: 1 },
817
- exit: (direction) => ({ x: direction > 0 ? -24 : 24, opacity: 0 })
818
- };
819
- function PaneTransition(props) {
820
- const reduce = useReducedMotion();
821
- if (reduce) {
822
- return /* @__PURE__ */ jsx5("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: props.children });
823
- }
824
- return /* @__PURE__ */ jsx5("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx5(AnimatePresence, { initial: false, custom: props.direction, children: /* @__PURE__ */ jsx5(
825
- motion.div,
787
+ function ChainOption(props) {
788
+ const { selected, disabled = false, reason, showReason = false, dotted = false, onSelect, label, children } = props;
789
+ const button = /* @__PURE__ */ jsxs(
790
+ "button",
826
791
  {
827
- custom: props.direction,
828
- variants,
829
- initial: "enter",
830
- animate: "center",
831
- exit: "exit",
832
- transition: { duration: 0.22, ease: EASE },
833
- className: "absolute inset-0 flex min-h-0 flex-col",
834
- children: props.children
835
- },
836
- props.paneKey
837
- ) }) });
792
+ type: "button",
793
+ "aria-disabled": disabled || void 0,
794
+ onClick: () => {
795
+ if (disabled) return;
796
+ onSelect();
797
+ },
798
+ className: cx(
799
+ "flex w-full cursor-pointer items-center gap-3 rounded-[6px] border-0 bg-transparent px-1.5 py-1.5 text-left",
800
+ selected && "bg-[#f6f6f6]",
801
+ disabled && "cursor-not-allowed"
802
+ ),
803
+ children: [
804
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
805
+ /* @__PURE__ */ jsxs("span", { className: cx("flex min-w-0 items-center gap-3", disabled && "opacity-40"), children: [
806
+ /* @__PURE__ */ jsx3("span", { className: "flex size-8 shrink-0 items-center justify-center", children }),
807
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
808
+ /* @__PURE__ */ jsx3("span", { className: cx("truncate text-sm font-medium text-black", fontSans), children: label }),
809
+ dotted ? /* @__PURE__ */ jsx3("span", { className: "size-2 shrink-0 rounded-full bg-[#6284f5]" }) : null
810
+ ] })
811
+ ] }),
812
+ disabled && showReason && reason ? /* @__PURE__ */ jsx3("span", { className: cx("mt-0.5 block pl-11 text-xs font-normal text-[#606060]", fontSans), children: reason }) : null
813
+ ] }),
814
+ selected ? /* @__PURE__ */ jsx3(IconCheck, { className: "mr-1 size-2.5 shrink-0 text-black" }) : null
815
+ ]
816
+ }
817
+ );
818
+ if (disabled && reason && !showReason) {
819
+ return /* @__PURE__ */ jsx3(Tooltip, { content: reason, triggerClassName: "flex w-full", children: button });
820
+ }
821
+ return button;
838
822
  }
839
823
 
840
824
  // src/token-select/prefs.ts
841
825
  import { useSyncExternalStore as useSyncExternalStore2 } from "react";
842
- var EMPTY_PREFS = { recentAssetIds: [], recentBlockchains: [] };
843
- function rememberRecentAsset(recent, assetId) {
826
+ var EMPTY_PREFS = {
827
+ recentAssetIds: [],
828
+ recentSearchAssetIds: [],
829
+ recentBlockchains: []
830
+ };
831
+ function rememberId(recent, assetId, max) {
844
832
  const id = assetId.trim();
845
833
  if (!id) return recent;
846
- return [id, ...recent.filter((item) => item !== id)].slice(0, MAX_RECENT_ASSETS);
834
+ return [id, ...recent.filter((item) => item !== id)].slice(0, max);
835
+ }
836
+ function rememberRecentAsset(recent, assetId) {
837
+ return rememberId(recent, assetId, MAX_RECENT_HISTORY);
838
+ }
839
+ function rememberRecentSearch(recent, assetId) {
840
+ return rememberId(recent, assetId, MAX_RECENT_HISTORY);
841
+ }
842
+ function withoutAssetIds(recent, assetIds) {
843
+ const drop = new Set(assetIds);
844
+ return recent.filter((id) => !drop.has(id));
847
845
  }
848
846
  function rememberRecentBlockchain(recent, blockchain) {
849
847
  const code = blockchain.trim();
850
848
  if (!code) return recent;
851
849
  return [code, ...recent.filter((item) => item !== code)].slice(0, MAX_RECENT_BLOCKCHAINS);
852
850
  }
851
+ function cleanIds(value, max) {
852
+ if (!Array.isArray(value)) return [];
853
+ return value.filter((id) => typeof id === "string" && id.trim()).slice(0, max);
854
+ }
853
855
  function migrateTokenSelectPrefs(persisted, version) {
854
856
  const state = persisted ?? {};
855
- const recentBlockchains = version >= 1 ? Array.isArray(state.recentBlockchains) ? state.recentBlockchains : [] : typeof state.lastBlockchain === "string" && state.lastBlockchain ? [state.lastBlockchain] : [];
856
- const recentAssetIds = version >= TOKEN_SELECT_PREFS_VERSION && Array.isArray(state.recentAssetIds) ? state.recentAssetIds.filter((id) => typeof id === "string" && id.trim()).slice(0, MAX_RECENT_ASSETS) : typeof state.lastAssetId === "string" && state.lastAssetId ? [state.lastAssetId] : [];
857
- return { recentAssetIds, recentBlockchains };
857
+ const recentBlockchains = version >= 1 ? cleanIds(state.recentBlockchains, MAX_RECENT_BLOCKCHAINS) : typeof state.lastBlockchain === "string" && state.lastBlockchain ? [state.lastBlockchain] : [];
858
+ const recentAssetIds = version >= 2 && Array.isArray(state.recentAssetIds) ? cleanIds(state.recentAssetIds, MAX_RECENT_HISTORY) : typeof state.lastAssetId === "string" && state.lastAssetId ? [state.lastAssetId] : [];
859
+ const recentSearchAssetIds = version >= TOKEN_SELECT_PREFS_VERSION ? cleanIds(state.recentSearchAssetIds, MAX_RECENT_HISTORY) : [];
860
+ return { recentAssetIds, recentSearchAssetIds, recentBlockchains };
858
861
  }
859
862
  function readStorage() {
860
863
  if (typeof localStorage === "undefined") return EMPTY_PREFS;
@@ -892,10 +895,23 @@ function getTokenSelectPrefs() {
892
895
  }
893
896
  function setLastToken(assetId, blockchain) {
894
897
  emit2({
898
+ ...prefs,
895
899
  recentAssetIds: rememberRecentAsset(prefs.recentAssetIds, assetId),
896
900
  recentBlockchains: rememberRecentBlockchain(prefs.recentBlockchains, blockchain)
897
901
  });
898
902
  }
903
+ function setRecentSearch(assetId) {
904
+ emit2({
905
+ ...prefs,
906
+ recentSearchAssetIds: rememberRecentSearch(prefs.recentSearchAssetIds, assetId)
907
+ });
908
+ }
909
+ function removeRecentSearches(assetIds) {
910
+ emit2({
911
+ ...prefs,
912
+ recentSearchAssetIds: withoutAssetIds(prefs.recentSearchAssetIds, assetIds)
913
+ });
914
+ }
899
915
  function setLastBlockchain(blockchain) {
900
916
  emit2({
901
917
  ...prefs,
@@ -906,26 +922,25 @@ function useTokenSelectPrefs() {
906
922
  return useSyncExternalStore2(subscribe2, getTokenSelectPrefs, getTokenSelectPrefs);
907
923
  }
908
924
 
909
- // src/token-select/token-pane.tsx
910
- import { IconChevron as IconChevron2 } from "@stableflow/pay-ui/icons/chevron";
925
+ // src/token-select/recent-tiles.tsx
911
926
  import { Skeleton } from "@stableflow/pay-ui/skeleton";
912
927
 
913
928
  // src/token-select/lazy-image.tsx
914
- import { useEffect as useEffect2, useRef, useState } from "react";
929
+ import { useEffect as useEffect4, useRef as useRef2, useState as useState3 } from "react";
915
930
  import { useInView } from "motion/react";
916
- import { jsx as jsx6 } from "react/jsx-runtime";
931
+ import { jsx as jsx4 } from "react/jsx-runtime";
917
932
  function LazyImage(props) {
918
933
  const { src, fallbackSrc, alt = "", className } = props;
919
- const ref = useRef(null);
934
+ const ref = useRef2(null);
920
935
  const inView = useInView(ref, { once: true });
921
- const [failedSrc, setFailedSrc] = useState(null);
922
- useEffect2(() => {
936
+ const [failedSrc, setFailedSrc] = useState3(null);
937
+ useEffect4(() => {
923
938
  setFailedSrc(null);
924
939
  }, [src]);
925
940
  const requested = src.trim();
926
941
  const useFallback = requested.length === 0 || failedSrc === requested;
927
942
  const shown = useFallback ? fallbackSrc : requested;
928
- return /* @__PURE__ */ jsx6("span", { ref, className: "block size-full", children: inView ? /* @__PURE__ */ jsx6(
943
+ return /* @__PURE__ */ jsx4("span", { ref, className: "block size-full", children: inView ? /* @__PURE__ */ jsx4(
929
944
  "img",
930
945
  {
931
946
  src: shown,
@@ -981,37 +996,94 @@ function tokenLogoUrl(symbol) {
981
996
  return logoPath(`/stableflow/tokens/${file}.png`);
982
997
  }
983
998
 
999
+ // src/token-select/recent-tiles.tsx
1000
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
1001
+ function RecentTiles(props) {
1002
+ const { tokens, onSelect, isTokenDisabled } = props;
1003
+ if (tokens.length === 0) return null;
1004
+ return /* @__PURE__ */ jsx5("div", { className: "grid grid-cols-5 gap-2 px-5 md:gap-3", children: tokens.map((token) => {
1005
+ const disabled = Boolean(isTokenDisabled?.(token));
1006
+ return /* @__PURE__ */ jsxs2(
1007
+ "button",
1008
+ {
1009
+ type: "button",
1010
+ disabled,
1011
+ onClick: () => onSelect(token),
1012
+ className: cx(
1013
+ "flex h-[72px] cursor-pointer flex-col items-center justify-center gap-1 rounded-[12px] border-0 bg-[#f6f6f6] px-1 hover:bg-[#ededed]",
1014
+ disabled && "cursor-not-allowed opacity-40 hover:bg-[#f6f6f6]"
1015
+ ),
1016
+ children: [
1017
+ /* @__PURE__ */ jsxs2("span", { className: "relative size-[26px] shrink-0", children: [
1018
+ /* @__PURE__ */ jsx5(
1019
+ LazyImage,
1020
+ {
1021
+ src: token.logo,
1022
+ fallbackSrc: DEFAULT_TOKEN_LOGO,
1023
+ className: "size-[26px] rounded-[12px] object-cover"
1024
+ }
1025
+ ),
1026
+ /* @__PURE__ */ jsx5(
1027
+ "img",
1028
+ {
1029
+ src: token.chain.logo,
1030
+ alt: "",
1031
+ className: "absolute -right-1 -bottom-1 size-3 rounded-[4px] border border-white object-cover"
1032
+ }
1033
+ )
1034
+ ] }),
1035
+ /* @__PURE__ */ jsx5("span", { className: cx("max-w-full truncate text-sm font-semibold text-black", fontSans), children: token.symbol })
1036
+ ]
1037
+ },
1038
+ token.assetId
1039
+ );
1040
+ }) });
1041
+ }
1042
+ function RecentTileSkeletons() {
1043
+ return /* @__PURE__ */ jsx5("div", { className: "grid grid-cols-5 gap-2 px-5 md:gap-3", "aria-hidden": true, children: Array.from({ length: 5 }, (_, index) => /* @__PURE__ */ jsx5(Skeleton, { className: "h-[72px] rounded-[12px]" }, index)) });
1044
+ }
1045
+
984
1046
  // src/token-select/token-pane.tsx
985
- import { Fragment, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1047
+ import { IconDatabase } from "@stableflow/pay-ui/icons/solid/database";
1048
+ import { IconDuration } from "@stableflow/pay-ui/icons/solid/duration";
1049
+ import { IconTrends } from "@stableflow/pay-ui/icons/outline/trends";
1050
+ import { Skeleton as Skeleton2 } from "@stableflow/pay-ui/skeleton";
1051
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
986
1052
  function TokenPane(props) {
987
1053
  const {
988
1054
  sections,
989
1055
  selectedAssetId,
990
- loading,
991
1056
  showBalances = false,
992
- showWalletStatus = true,
993
- header = null,
994
1057
  getBalance,
995
1058
  isBalanceLoading,
996
1059
  onSelectToken,
997
1060
  isTokenDisabled,
998
- rowClassName
1061
+ rowClassName,
1062
+ emptyLabel = null
999
1063
  } = props;
1000
- const visibleSections = sections.filter((section) => section.title || section.tokens.length > 0);
1001
- const hasTokens = sections.some((section) => section.tokens.length > 0);
1002
- return /* @__PURE__ */ jsx7("div", { className: "flex h-full min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx7("div", { className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs4("div", { className: "px-2.5 pt-[17px] pb-3", children: [
1003
- header ? /* @__PURE__ */ jsx7(ChainListHeader, { header, showWalletStatus }) : null,
1004
- visibleSections.map((section) => /* @__PURE__ */ jsxs4("section", { className: "mb-2", children: [
1005
- section.title ? /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-2 px-3.5", children: [
1006
- /* @__PURE__ */ jsx7("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: section.title }),
1007
- showWalletStatus && section.walletKind ? /* @__PURE__ */ jsx7(ChainWalletStatus, { kind: section.walletKind }) : null
1064
+ return /* @__PURE__ */ jsx6("div", { className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs3("div", { className: "px-3 pt-3 pb-3", children: [
1065
+ sections.map((section) => /* @__PURE__ */ jsxs3("section", { className: "mb-2", children: [
1066
+ section.title ? /* @__PURE__ */ jsxs3("div", { className: "mb-1 flex items-center justify-between gap-2 px-[13px]", children: [
1067
+ /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 items-center gap-2 text-[#aaa]", children: [
1068
+ /* @__PURE__ */ jsx6(SectionIcon, { icon: section.icon }),
1069
+ /* @__PURE__ */ jsx6("span", { className: cx("truncate text-sm font-medium text-[#aaa]", fontSans), children: section.title })
1070
+ ] }),
1071
+ section.action ? /* @__PURE__ */ jsx6(
1072
+ "button",
1073
+ {
1074
+ type: "button",
1075
+ onClick: section.action.onClick,
1076
+ className: cx("cursor-pointer border-0 bg-transparent p-0 text-sm font-medium text-[#aaa]", fontSans),
1077
+ children: section.action.label
1078
+ }
1079
+ ) : null
1008
1080
  ] }) : null,
1009
- section.tokens.map((token) => /* @__PURE__ */ jsx7(
1081
+ section.skeletonRows ? /* @__PURE__ */ jsx6(TokenListSkeleton, { rows: section.skeletonRows }) : null,
1082
+ section.tokens.map((token) => /* @__PURE__ */ jsx6(
1010
1083
  TokenRow,
1011
1084
  {
1012
1085
  token,
1013
1086
  selected: token.assetId === selectedAssetId,
1014
- recent: token.assetId === section.badgeAssetId,
1015
1087
  showBalances,
1016
1088
  formatted: showBalances ? getBalance(token) : null,
1017
1089
  loadingBalance: showBalances && isBalanceLoading(token),
@@ -1022,57 +1094,43 @@ function TokenPane(props) {
1022
1094
  `${section.id}-${token.assetId}`
1023
1095
  ))
1024
1096
  ] }, section.id)),
1025
- loading && !hasTokens ? /* @__PURE__ */ jsx7(TokenListSkeleton, {}) : null,
1026
- !loading && !hasTokens ? /* @__PURE__ */ jsx7("p", { className: cx("px-3.5 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
1027
- ] }) }) });
1028
- }
1029
- function ChainListHeader(props) {
1030
- const { header, showWalletStatus } = props;
1031
- const walletKind = header.walletKind;
1032
- return /* @__PURE__ */ jsxs4("div", { className: "mb-1 flex items-center justify-between gap-3 px-3.5", children: [
1033
- header.kind === "back" ? /* @__PURE__ */ jsxs4(
1034
- "button",
1035
- {
1036
- type: "button",
1037
- onClick: header.onBack,
1038
- className: cx("flex min-w-0 cursor-pointer items-center gap-3 border-0 bg-transparent p-0 text-sm font-medium text-[#aaa]", fontSans),
1039
- children: [
1040
- /* @__PURE__ */ jsx7(IconChevron2, { className: "rotate-180 text-[#aaa]" }),
1041
- /* @__PURE__ */ jsx7("span", { className: "truncate", children: header.title })
1042
- ]
1043
- }
1044
- ) : /* @__PURE__ */ jsx7("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: header.title }),
1045
- showWalletStatus && walletKind ? /* @__PURE__ */ jsx7(ChainWalletStatus, { kind: walletKind }) : null
1046
- ] });
1047
- }
1048
- function TokenListSkeleton() {
1049
- return /* @__PURE__ */ jsx7("div", { "aria-hidden": true, children: Array.from({ length: 6 }, (_, index) => /* @__PURE__ */ jsxs4("div", { className: "flex h-[66px] items-center gap-2.5 px-3.5", children: [
1050
- /* @__PURE__ */ jsx7(Skeleton, { className: "size-8 shrink-0 rounded-full" }),
1051
- /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
1052
- /* @__PURE__ */ jsx7(Skeleton, { className: "h-3.5 w-24" }),
1053
- /* @__PURE__ */ jsx7(Skeleton, { className: "h-3 w-16" })
1097
+ emptyLabel ? /* @__PURE__ */ jsx6("p", { className: cx("px-[13px] py-4 text-[13px] text-[#606060]", fontSans), children: emptyLabel }) : null
1098
+ ] }) });
1099
+ }
1100
+ function SectionIcon(props) {
1101
+ const className = "size-3.5 text-[#aaa]";
1102
+ if (props.icon === "database") return /* @__PURE__ */ jsx6(IconDatabase, { className });
1103
+ if (props.icon === "duration") return /* @__PURE__ */ jsx6(IconDuration, { className });
1104
+ return /* @__PURE__ */ jsx6(IconTrends, { className });
1105
+ }
1106
+ function TokenListSkeleton(props) {
1107
+ return /* @__PURE__ */ jsx6("div", { "aria-hidden": true, children: Array.from({ length: props.rows }, (_, index) => /* @__PURE__ */ jsxs3("div", { className: "flex h-[66px] items-center gap-2.5 px-[13px]", children: [
1108
+ /* @__PURE__ */ jsx6(Skeleton2, { className: "size-8 shrink-0 rounded-full" }),
1109
+ /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
1110
+ /* @__PURE__ */ jsx6(Skeleton2, { className: "h-3.5 w-24" }),
1111
+ /* @__PURE__ */ jsx6(Skeleton2, { className: "h-3 w-16" })
1054
1112
  ] })
1055
1113
  ] }, index)) });
1056
1114
  }
1057
1115
  function TokenRow(props) {
1058
- const { token, selected, recent, showBalances, formatted, loadingBalance, disabled, onSelect, className } = props;
1116
+ const { token, selected, showBalances, formatted, loadingBalance, disabled, onSelect, className } = props;
1059
1117
  const usd = showBalances && !loadingBalance && formatted != null ? tokenBalanceUsd(token, formatted) : -1;
1060
- return /* @__PURE__ */ jsxs4(
1118
+ return /* @__PURE__ */ jsxs3(
1061
1119
  "button",
1062
1120
  {
1063
1121
  type: "button",
1064
1122
  disabled,
1065
1123
  onClick: onSelect,
1066
1124
  className: cx(
1067
- "flex h-[66px] w-full cursor-pointer items-center justify-between gap-2 rounded-[12px] border-0 bg-transparent px-3.5 text-left hover:bg-[#f6f6f6]",
1125
+ "flex h-[66px] w-full cursor-pointer items-center justify-between gap-2 rounded-[12px] border-0 bg-transparent px-[13px] text-left hover:bg-[#f6f6f6]",
1068
1126
  selected && "bg-[#f6f6f6]",
1069
1127
  disabled && "cursor-not-allowed opacity-40 hover:bg-transparent",
1070
1128
  className
1071
1129
  ),
1072
1130
  children: [
1073
- /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-2.5", children: [
1074
- /* @__PURE__ */ jsxs4("span", { className: "relative size-8 shrink-0", children: [
1075
- /* @__PURE__ */ jsx7(
1131
+ /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 items-center gap-2.5", children: [
1132
+ /* @__PURE__ */ jsxs3("span", { className: "relative size-8 shrink-0", children: [
1133
+ /* @__PURE__ */ jsx6(
1076
1134
  LazyImage,
1077
1135
  {
1078
1136
  src: token.logo,
@@ -1080,7 +1138,7 @@ function TokenRow(props) {
1080
1138
  className: "size-8 rounded-[12px] object-cover"
1081
1139
  }
1082
1140
  ),
1083
- /* @__PURE__ */ jsx7(
1141
+ /* @__PURE__ */ jsx6(
1084
1142
  "img",
1085
1143
  {
1086
1144
  src: token.chain.logo,
@@ -1089,69 +1147,58 @@ function TokenRow(props) {
1089
1147
  }
1090
1148
  )
1091
1149
  ] }),
1092
- /* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
1093
- /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 items-center gap-1.5", children: [
1094
- /* @__PURE__ */ jsx7("span", { className: cx("truncate text-sm font-semibold text-black", fontSans), children: token.symbol }),
1095
- recent ? /* @__PURE__ */ jsx7("span", { className: cx("shrink-0 rounded-[9px] bg-[#06f]/10 px-1.5 text-[10px] font-medium leading-[18px] text-[#06f]", fontSans), children: "Recently Used" }) : null
1096
- ] }),
1097
- /* @__PURE__ */ jsx7("span", { className: cx("mt-0.5 block truncate text-xs text-[#606060]", fontSans), children: token.chain.chainName })
1150
+ /* @__PURE__ */ jsxs3("span", { className: "min-w-0", children: [
1151
+ /* @__PURE__ */ jsx6("span", { className: cx("block truncate text-sm font-semibold text-black", fontSans), children: token.symbol }),
1152
+ /* @__PURE__ */ jsx6("span", { className: cx("mt-0.5 block truncate text-xs text-[#606060]", fontSans), children: token.chain.chainName })
1098
1153
  ] })
1099
1154
  ] }),
1100
- showBalances ? /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-right", children: loadingBalance ? /* @__PURE__ */ jsx7(
1155
+ showBalances ? /* @__PURE__ */ jsx6("span", { className: "shrink-0 text-right", children: loadingBalance ? /* @__PURE__ */ jsx6(
1101
1156
  "span",
1102
1157
  {
1103
1158
  className: "inline-block size-3.5 animate-spin rounded-full border-2 border-[#606060] border-r-transparent",
1104
1159
  "aria-label": "Loading balance"
1105
1160
  }
1106
- ) : formatted != null ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1107
- /* @__PURE__ */ jsx7("span", { className: cx("block text-sm font-medium text-black", fontSans), children: formatDisplayAmount(formatted, { prefix: "", maxDecimals: 4 }) }),
1108
- usd >= 0 ? /* @__PURE__ */ jsx7("span", { className: cx("block text-xs text-[#606060]", fontSans), children: formatDisplayAmount(usd, { prefix: "$", showDust: true }) }) : null
1109
- ] }) : /* @__PURE__ */ jsx7("span", { className: cx("text-sm font-medium text-black", fontSans), children: "-" }) }) : null
1161
+ ) : formatted != null ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
1162
+ /* @__PURE__ */ jsx6("span", { className: cx("block text-sm font-medium text-black", fontSans), children: formatDisplayAmount(formatted, { prefix: "", maxDecimals: 4 }) }),
1163
+ usd >= 0 ? /* @__PURE__ */ jsx6("span", { className: cx("block text-xs text-[#606060]", fontSans), children: formatDisplayAmount(usd, { prefix: "$", showDust: true }) }) : null
1164
+ ] }) : /* @__PURE__ */ jsx6("span", { className: cx("text-sm font-medium text-black", fontSans), children: "-" }) }) : null
1110
1165
  ]
1111
1166
  }
1112
1167
  );
1113
1168
  }
1114
1169
 
1115
1170
  // src/token-select/search-results.tsx
1116
- import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1171
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1117
1172
  function SearchResults(props) {
1118
1173
  const {
1119
- groups,
1174
+ tokens,
1120
1175
  selectedAssetId,
1121
1176
  loading,
1122
1177
  showBalances = false,
1123
- showWalletStatus = true,
1124
1178
  getBalance,
1125
1179
  isBalanceLoading,
1126
1180
  onSelectToken,
1127
1181
  isTokenDisabled,
1128
1182
  rowClassName
1129
1183
  } = props;
1130
- const hasTokens = groups.some((group) => group.tokens.length > 0);
1131
- return /* @__PURE__ */ jsx8("div", { className: "flex h-full min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx8("div", { className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs5("div", { className: "px-2.5 pt-[17px] pb-3", children: [
1132
- groups.map((group) => /* @__PURE__ */ jsxs5("section", { className: "mb-2", children: [
1133
- /* @__PURE__ */ jsxs5("div", { className: "mb-1 flex items-center justify-between gap-2 px-3.5", children: [
1134
- /* @__PURE__ */ jsx8("p", { className: cx("min-w-0 truncate text-sm font-medium text-[#aaa]", fontSans), children: group.chainName }),
1135
- showWalletStatus ? /* @__PURE__ */ jsx8(ChainWalletStatus, { kind: group.chainKind }) : null
1136
- ] }),
1137
- group.tokens.map((token) => /* @__PURE__ */ jsx8(
1138
- TokenRow,
1139
- {
1140
- token,
1141
- selected: token.assetId === selectedAssetId,
1142
- recent: false,
1143
- showBalances,
1144
- formatted: showBalances ? getBalance(token) : null,
1145
- loadingBalance: showBalances && isBalanceLoading(token),
1146
- disabled: Boolean(isTokenDisabled?.(token)),
1147
- onSelect: () => onSelectToken(token),
1148
- className: rowClassName
1149
- },
1150
- token.assetId
1151
- ))
1152
- ] }, group.blockchain)),
1153
- !loading && !hasTokens ? /* @__PURE__ */ jsx8("p", { className: cx("px-3.5 py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
1154
- ] }) }) });
1184
+ return /* @__PURE__ */ jsx7("div", { className: cx("min-h-0 flex-1 overflow-y-auto", thinScrollbar), children: /* @__PURE__ */ jsxs4("div", { className: "px-3 pt-3 pb-3", children: [
1185
+ loading && tokens.length === 0 ? /* @__PURE__ */ jsx7(TokenListSkeleton, { rows: 6 }) : null,
1186
+ tokens.map((token) => /* @__PURE__ */ jsx7(
1187
+ TokenRow,
1188
+ {
1189
+ token,
1190
+ selected: token.assetId === selectedAssetId,
1191
+ showBalances,
1192
+ formatted: showBalances ? getBalance(token) : null,
1193
+ loadingBalance: showBalances && isBalanceLoading(token),
1194
+ disabled: Boolean(isTokenDisabled?.(token)),
1195
+ onSelect: () => onSelectToken(token),
1196
+ className: rowClassName
1197
+ },
1198
+ token.assetId
1199
+ )),
1200
+ !loading && tokens.length === 0 ? /* @__PURE__ */ jsx7("p", { className: cx("px-[13px] py-4 text-[13px] text-[#606060]", fontSans), children: "No tokens found" }) : null
1201
+ ] }) });
1155
1202
  }
1156
1203
 
1157
1204
  // src/token-select/types.ts
@@ -1161,8 +1208,9 @@ function ownerKey2(owners) {
1161
1208
  }
1162
1209
 
1163
1210
  // src/token-select/TokenSelectDialog.tsx
1164
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1165
- var SEARCH_INPUT_CLASS = "h-[36px] rounded-[18px] border-[#e3e3e3] bg-[#f6f6f6] text-sm placeholder:text-black/30";
1211
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1212
+ var TOKEN_SEARCH_CLASS = "h-[50px] rounded-[12px] border-[#e3e3e3] bg-[#f6f6f6] pl-[43px] text-sm placeholder:text-black/30";
1213
+ var TOKEN_SEARCH_ICON_CLASS = "left-4 size-[19px] text-black";
1166
1214
  function ownerForToken(owners, token) {
1167
1215
  return owners[token.chain.chainKind] ?? null;
1168
1216
  }
@@ -1173,15 +1221,6 @@ function isNativeToken(token) {
1173
1221
  const addr = String(token.contractAddress || "").trim().toLowerCase();
1174
1222
  return !addr || addr === "native" || addr === "0x0000000000000000000000000000000000000000";
1175
1223
  }
1176
- function railSelection(view) {
1177
- if (view.kind === "wallet") return { kind: "wallet" };
1178
- if (view.kind === "all") return { kind: "all" };
1179
- if (view.kind === "rail-chain") return { kind: "chain", blockchain: view.blockchain };
1180
- return { kind: "networks" };
1181
- }
1182
- function chainByCode(chains, blockchain) {
1183
- return chains.find((chain) => chain.blockchain === blockchain);
1184
- }
1185
1224
  function TokenSelectDialog(props) {
1186
1225
  const {
1187
1226
  open,
@@ -1202,14 +1241,12 @@ function TokenSelectDialog(props) {
1202
1241
  closeClassName,
1203
1242
  contentClassName,
1204
1243
  searchClassName,
1205
- railClassName,
1206
1244
  rowClassName
1207
1245
  } = props;
1208
1246
  const payer = role === "payer";
1209
1247
  const showBalances = payer;
1210
1248
  const {
1211
1249
  wallet,
1212
- popularTokens: popularRefs,
1213
1250
  tokens,
1214
1251
  loading,
1215
1252
  stableSymbols,
@@ -1231,47 +1268,26 @@ function TokenSelectDialog(props) {
1231
1268
  () => showBalances ? { ...connectedOwners, ...balanceOwners } : {},
1232
1269
  [showBalances, connectedOwners, balanceOwners]
1233
1270
  );
1234
- const walletConnected = hasAnyOwner(connectedOwners);
1235
- const [search, setSearch] = useState2("");
1236
- const [view, setView] = useState2(payer ? { kind: "networks" } : { kind: "all" });
1237
- const [slideDirection, setSlideDirection] = useState2(1);
1238
- const networksScrollRef = useRef2(null);
1239
- const savedNetworksScroll = useRef2(0);
1240
- const walletConnectedRef = useRef2(walletConnected);
1241
- const payerRef = useRef2(payer);
1242
- const lockRef = useRef2(lockChainKind);
1243
- const tokensRef = useRef2(tokens);
1244
- walletConnectedRef.current = walletConnected;
1245
- payerRef.current = payer;
1271
+ const [search, setSearch] = useState4("");
1272
+ const [chainFilter, setChainFilter] = useState4(ALL_CHAIN_FILTER);
1273
+ const userPickedChain = useRef3(false);
1274
+ const lockRef = useRef3(lockChainKind);
1275
+ const tokensRef = useRef3(tokens);
1246
1276
  lockRef.current = lockChainKind;
1247
1277
  tokensRef.current = tokens;
1248
- useEffect3(() => {
1278
+ useEffect5(() => {
1249
1279
  if (!open) return;
1280
+ userPickedChain.current = false;
1250
1281
  setSearch("");
1251
- const lock = lockRef.current;
1252
- if (lock) {
1253
- const filter = initialChainFilter(
1254
- lock,
1255
- getTokenSelectPrefs().recentBlockchains,
1256
- chainsFromTokens(tokensRef.current)
1257
- );
1258
- if (filter !== ALL_CHAIN_FILTER) {
1259
- setView({ kind: "rail-chain", blockchain: filter });
1260
- return;
1261
- }
1262
- return;
1263
- }
1264
- if (!payerRef.current) {
1265
- setView({ kind: "all" });
1266
- return;
1267
- }
1268
- setView(walletConnectedRef.current ? { kind: "wallet" } : { kind: "networks" });
1282
+ setChainFilter(initialChainFilter(
1283
+ lockRef.current,
1284
+ getTokenSelectPrefs().recentBlockchains,
1285
+ chainsFromTokens(tokensRef.current)
1286
+ ));
1269
1287
  }, [open]);
1270
- useEffect3(() => {
1271
- if (!open || !lockChainKind) return;
1272
- const filter = initialChainFilter(lockChainKind, getTokenSelectPrefs().recentBlockchains, chainsFromTokens(tokens));
1273
- if (filter === ALL_CHAIN_FILTER) return;
1274
- setView((current) => current.kind === "rail-chain" ? current : { kind: "rail-chain", blockchain: filter });
1288
+ useEffect5(() => {
1289
+ if (!open || !lockChainKind || userPickedChain.current) return;
1290
+ setChainFilter(initialChainFilter(lockChainKind, getTokenSelectPrefs().recentBlockchains, chainsFromTokens(tokens)));
1275
1291
  }, [open, lockChainKind, tokens]);
1276
1292
  const allowed = useMemo(() => {
1277
1293
  if (!allowedBlockchains || allowedBlockchains.length === 0) return null;
@@ -1288,11 +1304,11 @@ function TokenSelectDialog(props) {
1288
1304
  }, [tokens, allowed, excludeNative, payer]);
1289
1305
  const ownersKey = ownerKey2(owners);
1290
1306
  const tokenKey = useMemo(() => scopedTokens.map((token) => token.assetId).join("|"), [scopedTokens]);
1291
- const ownersRef = useRef2(owners);
1292
- const scopedRef = useRef2(scopedTokens);
1307
+ const ownersRef = useRef3(owners);
1308
+ const scopedRef = useRef3(scopedTokens);
1293
1309
  ownersRef.current = owners;
1294
1310
  scopedRef.current = scopedTokens;
1295
- useEffect3(() => {
1311
+ useEffect5(() => {
1296
1312
  if (balanceMode !== "async" || !showBalances || !open || !hasAnyOwner(ownersRef.current) || !tokenKey) return;
1297
1313
  void ensurePayTokenBalances(ownersRef.current, scopedRef.current);
1298
1314
  const id = window.setInterval(() => {
@@ -1321,6 +1337,10 @@ function TokenSelectDialog(props) {
1321
1337
  return !entry || entry.status === "loading";
1322
1338
  }
1323
1339
  const availableChains = useMemo(() => chainsFromTokens(scopedTokens), [scopedTokens]);
1340
+ const rangeTokens = useMemo(
1341
+ () => scopedTokens.filter((token) => matchesChainFilter(token, chainFilter)),
1342
+ [scopedTokens, chainFilter]
1343
+ );
1324
1344
  const fundedBlockchains = useMemo(() => {
1325
1345
  const funded = /* @__PURE__ */ new Set();
1326
1346
  if (!showBalances) return funded;
@@ -1331,253 +1351,208 @@ function TokenSelectDialog(props) {
1331
1351
  }
1332
1352
  return funded;
1333
1353
  }, [availableChains, owners, scopedTokens, showBalances, balanceEntries, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1334
- const railChains = useMemo(() => {
1335
- if (!payer) return sortChainsAlpha(availableChains);
1336
- const connected = payerRailChains(availableChains, owners, fundedBlockchains);
1337
- return sortChainsForSidebar(connected, scopedTokens, {
1338
- showBalances,
1339
- getBalanceUsd: (token) => tokenBalanceUsd(token, balanceOf(token))
1340
- });
1341
- }, [payer, availableChains, owners, fundedBlockchains, scopedTokens, showBalances, balanceEntries, balanceMode, syncGetBalance]);
1342
- const networkChains = useMemo(() => sortChainsAlpha(availableChains), [availableChains]);
1343
- useEffect3(() => {
1344
- if (!open || !payer || lockChainKind) return;
1345
- setView((current) => {
1346
- if (current.kind === "wallet" && !walletConnected) return { kind: "networks" };
1347
- if (current.kind === "rail-chain" && !railChains.some((chain) => chain.blockchain === current.blockchain)) {
1348
- return walletConnected ? { kind: "wallet" } : { kind: "networks" };
1349
- }
1350
- return current;
1351
- });
1352
- }, [open, payer, lockChainKind, walletConnected, railChains]);
1353
- const popularTokens = useMemo(
1354
- () => popularTokensForWallets(scopedTokens, popularRefs, (kind) => Boolean(connectedOwners[kind])),
1355
- [scopedTokens, popularRefs, connectedOwners]
1356
- );
1357
- const newestAssetId = showBalances ? prefs2.recentAssetIds[0] ?? null : null;
1354
+ const dottedChains = useMemo(() => {
1355
+ if (!payer) return [];
1356
+ return payerRailChains(availableChains, owners, fundedBlockchains);
1357
+ }, [payer, availableChains, owners, fundedBlockchains]);
1358
+ const dottedSet = useMemo(() => new Set(dottedChains.map((chain) => chain.blockchain)), [dottedChains]);
1359
+ const menuChains = useMemo(() => chainMenuOrder(availableChains, scopedTokens, {
1360
+ dotted: dottedSet,
1361
+ showBalances,
1362
+ getBalanceUsd: (token) => tokenBalanceUsd(token, balanceOf(token))
1363
+ }), [availableChains, scopedTokens, dottedSet, showBalances, balanceEntries, owners, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1364
+ const previewChains = useMemo(() => allChainPreview(availableChains), [availableChains]);
1358
1365
  const searching = search.trim().length > 0;
1359
- const sections = useMemo(() => {
1360
- if (view.kind === "wallet") {
1361
- const next = [];
1362
- const recent = recentTokensInOrder(scopedTokens, prefs2.recentAssetIds);
1363
- if (recent.length > 0) {
1364
- next.push({
1365
- id: "recent",
1366
- title: "Recently Used",
1367
- tokens: recent,
1368
- badgeAssetId: newestAssetId
1369
- });
1370
- }
1371
- const yours = positiveBalanceTokens(scopedTokens, usdOf);
1372
- if (yours.length > 0) {
1373
- next.push({ id: "yours", title: "Your Tokens", tokens: yours });
1374
- }
1375
- if (popularTokens.length > 0) {
1376
- next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popularTokens });
1377
- }
1378
- return next;
1379
- }
1380
- if (view.kind === "all") {
1381
- const next = [];
1382
- if (popularTokens.length > 0) {
1383
- next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popularTokens });
1384
- }
1385
- const sorted = scopedTokens.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols));
1386
- next.push({ id: "all", tokens: sorted });
1387
- return next;
1388
- }
1389
- if (view.kind === "rail-chain" || view.kind === "network-chain") {
1390
- const matched = scopedTokens.filter((token) => token.blockchain === view.blockchain);
1391
- if (!payer) {
1392
- const next = [];
1393
- const popular = popularTokens.filter((token) => token.blockchain === view.blockchain);
1394
- if (popular.length > 0) {
1395
- next.push({ id: "popular", title: POPULAR_SECTION_TITLE, tokens: popular });
1396
- }
1397
- next.push({
1398
- id: view.blockchain,
1399
- tokens: matched.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols))
1400
- });
1401
- return next;
1366
+ const rangeWalletConnected = rangeTokens.some((token) => Boolean(owners[token.chain.chainKind]));
1367
+ const recentTokens = useMemo(
1368
+ () => recentTokensInOrder(rangeTokens, prefs2.recentAssetIds).slice(0, MAX_RECENTLY_USED),
1369
+ [rangeTokens, prefs2.recentAssetIds]
1370
+ );
1371
+ const recentSearches = useMemo(
1372
+ () => recentTokensInOrder(rangeTokens, prefs2.recentSearchAssetIds).slice(0, MAX_RECENT_SEARCHES),
1373
+ [rangeTokens, prefs2.recentSearchAssetIds]
1374
+ );
1375
+ const yours = useMemo(() => {
1376
+ if (!payer || !rangeWalletConnected) return [];
1377
+ return positiveBalanceTokens(rangeTokens, usdOf);
1378
+ }, [payer, rangeWalletConnected, rangeTokens, owners, balanceEntries, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1379
+ const trends = useMemo(() => trendTokensIn(rangeTokens, {
1380
+ chainFilter,
1381
+ sortRest: (rest) => {
1382
+ if (chainFilter === ALL_CHAIN_FILTER) {
1383
+ return rest.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols));
1402
1384
  }
1403
- const chain = chainByCode(availableChains, view.blockchain);
1404
- return [{
1405
- id: view.blockchain,
1406
- tokens: sortTokensForChain(matched, {
1407
- withBalances: Boolean(chain && owners[chain.chainKind]),
1408
- usdOf,
1409
- loadingOf,
1410
- stableSymbols
1411
- }),
1412
- badgeAssetId: newestAssetId
1413
- }];
1385
+ const chain = availableChains.find((item) => item.blockchain === chainFilter);
1386
+ return sortTokensForChain(rest, {
1387
+ withBalances: Boolean(chain && owners[chain.chainKind]),
1388
+ usdOf,
1389
+ loadingOf,
1390
+ stableSymbols
1391
+ });
1414
1392
  }
1415
- return [];
1416
- }, [
1417
- view,
1418
- payer,
1419
- scopedTokens,
1420
- prefs2.recentAssetIds,
1421
- newestAssetId,
1422
- balanceEntries,
1423
- owners,
1424
- popularTokens,
1425
- availableChains,
1426
- stableSymbols,
1427
- balanceMode,
1428
- syncGetBalance,
1429
- syncIsBalanceLoading
1430
- ]);
1431
- const searchGroups = useMemo(() => {
1393
+ }), [rangeTokens, chainFilter, availableChains, owners, stableSymbols, balanceEntries, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1394
+ const receiverTokens = useMemo(
1395
+ () => rangeTokens.slice().sort((left, right) => compareTokensByPriority(left, right, stableSymbols)),
1396
+ [rangeTokens, stableSymbols]
1397
+ );
1398
+ const results = useMemo(() => {
1432
1399
  if (!searching) return [];
1433
- return groupSearchResults(scopedTokens, search, {
1400
+ return searchTokens(rangeTokens, search, {
1434
1401
  role: payer ? "payer" : "receiver",
1435
1402
  usdOf,
1436
- loadingOf,
1437
- hasOwner: (kind) => Boolean(owners[kind]),
1438
- stableSymbols
1403
+ loadingOf
1439
1404
  });
1440
- }, [searching, scopedTokens, search, payer, owners, stableSymbols, balanceEntries, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1441
- function handleSelectChain(blockchain, fromNetworks) {
1442
- setLastBlockchain(blockchain);
1443
- if (fromNetworks) {
1444
- savedNetworksScroll.current = networksScrollRef.current?.scrollTop ?? 0;
1445
- setSlideDirection(1);
1446
- setView({ kind: "network-chain", blockchain });
1447
- return;
1448
- }
1449
- setView({ kind: "rail-chain", blockchain });
1450
- }
1451
- function handleBackToNetworks() {
1452
- setSlideDirection(-1);
1453
- setView({ kind: "networks" });
1454
- }
1455
- useLayoutEffect(() => {
1456
- if (view.kind !== "networks" || slideDirection !== -1) return;
1457
- const node = networksScrollRef.current;
1458
- if (!node) return;
1459
- node.scrollTop = savedNetworksScroll.current;
1460
- }, [view, slideDirection]);
1461
- function handleSelectToken(token) {
1405
+ }, [searching, rangeTokens, search, payer, owners, balanceEntries, balanceMode, syncGetBalance, syncIsBalanceLoading]);
1406
+ function handleSelectToken(token, fromSearch) {
1462
1407
  if (isBlockchainDisabled(token.blockchain, disabledBlockchains)) return;
1463
1408
  if (isChainKindLocked(token.chain.chainKind, lockChainKind)) return;
1464
1409
  if (payer) setLastToken(token.assetId, token.blockchain);
1465
1410
  else setLastBlockchain(token.blockchain);
1411
+ if (fromSearch && search.trim()) setRecentSearch(token.assetId);
1466
1412
  onSelect({ token });
1467
1413
  onClose();
1468
1414
  }
1469
- const activeChain = view.kind === "rail-chain" || view.kind === "network-chain" ? chainByCode(availableChains, view.blockchain) : void 0;
1470
- const chainHeader = view.kind === "network-chain" ? { kind: "back", title: activeChain?.chainName ?? view.blockchain, onBack: handleBackToNetworks, walletKind: activeChain?.chainKind } : view.kind === "rail-chain" ? { kind: "chain", title: activeChain?.chainName ?? view.blockchain, walletKind: activeChain?.chainKind } : null;
1471
- const awaitingBalances = showBalances && sections.every((section) => section.tokens.length === 0) && scopedTokens.some((token) => loadingOf(token));
1415
+ function clearVisibleSearches() {
1416
+ if (chainFilter === ALL_CHAIN_FILTER) {
1417
+ removeRecentSearches(prefs2.recentSearchAssetIds);
1418
+ return;
1419
+ }
1420
+ const byId = new Map(scopedTokens.map((token) => [token.assetId, token]));
1421
+ removeRecentSearches(prefs2.recentSearchAssetIds.filter((assetId) => byId.get(assetId)?.blockchain === chainFilter));
1422
+ }
1472
1423
  const tokenDisabled = (token) => isBlockchainDisabled(token.blockchain, disabledBlockchains) || isChainKindLocked(token.chain.chainKind, lockChainKind);
1473
- const sliding = view.kind === "networks" || view.kind === "network-chain";
1474
- const tokenPane = /* @__PURE__ */ jsx9(
1475
- TokenPane,
1476
- {
1477
- sections,
1478
- header: chainHeader,
1479
- selectedAssetId,
1480
- loading: loading || awaitingBalances,
1481
- showBalances,
1482
- showWalletStatus: payer,
1483
- getBalance: balanceOf,
1484
- isBalanceLoading: loadingOf,
1485
- onSelectToken: handleSelectToken,
1486
- isTokenDisabled: tokenDisabled,
1487
- rowClassName
1424
+ const sections = useMemo(() => {
1425
+ if (loading) {
1426
+ const next2 = [];
1427
+ if (payer && rangeWalletConnected) {
1428
+ next2.push({ id: "yours", title: YOUR_TOKENS_TITLE, icon: "database", tokens: [], skeletonRows: 2 });
1429
+ }
1430
+ if (payer && recentSearches.length > 0) {
1431
+ next2.push({
1432
+ id: "searches",
1433
+ title: RECENT_SEARCHES_TITLE,
1434
+ icon: "duration",
1435
+ tokens: [],
1436
+ skeletonRows: 2,
1437
+ action: { label: "Clear", onClick: clearVisibleSearches }
1438
+ });
1439
+ }
1440
+ if (payer) {
1441
+ next2.push({ id: "trends", title: TRENDS_TOKEN_TITLE, icon: "trends", tokens: [], skeletonRows: 6 });
1442
+ }
1443
+ return next2;
1488
1444
  }
1489
- );
1490
- return /* @__PURE__ */ jsx9(
1445
+ if (!payer) return [];
1446
+ const next = [];
1447
+ if (yours.length > 0) {
1448
+ next.push({ id: "yours", title: YOUR_TOKENS_TITLE, icon: "database", tokens: yours });
1449
+ }
1450
+ if (recentSearches.length > 0) {
1451
+ next.push({
1452
+ id: "searches",
1453
+ title: RECENT_SEARCHES_TITLE,
1454
+ icon: "duration",
1455
+ tokens: recentSearches,
1456
+ action: { label: "Clear", onClick: clearVisibleSearches }
1457
+ });
1458
+ }
1459
+ if (trends.length > 0) {
1460
+ next.push({ id: "trends", title: TRENDS_TOKEN_TITLE, icon: "trends", tokens: trends });
1461
+ }
1462
+ return next;
1463
+ }, [loading, payer, rangeWalletConnected, recentSearches, yours, trends, chainFilter, prefs2.recentSearchAssetIds, scopedTokens]);
1464
+ return /* @__PURE__ */ jsx8(
1491
1465
  Dialog,
1492
1466
  {
1493
1467
  open,
1494
1468
  onClose,
1495
1469
  title,
1496
- titleClassName: cx(
1497
- "min-w-0 flex-1 truncate text-base font-medium text-[#606060] md:flex-none",
1498
- fontSans,
1499
- titleClassName
1500
- ),
1501
- headerClassName: cx("h-14 border-b border-[#e3e3e3] px-[22px] pr-4", headerClassName),
1502
- closeClassName: cx("md:ml-0", closeClassName),
1470
+ titleClassName: cx("text-[18px] font-semibold text-black", fontSans, titleClassName),
1471
+ headerClassName: cx("border-0 px-6 pt-[25px] pb-0", headerClassName),
1472
+ closeClassName,
1503
1473
  contentClassName: cx("overflow-hidden", contentClassName),
1504
- headerAction: /* @__PURE__ */ jsx9(
1505
- SearchInput,
1506
- {
1507
- value: search,
1508
- onChange: setSearch,
1509
- placeholder: "search or paste address",
1510
- className: "ml-auto hidden w-[215px] shrink-0 md:block",
1511
- inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
1512
- }
1513
- ),
1514
1474
  cardClassName: cx(
1515
- "w-full max-h-[90vh] gap-0 rounded-[12px] border border-[#e0e0e0] bg-[#fdfdfd] p-0 shadow-[0_0_40px_0_rgba(0,0,0,0.06)] md:w-[474px] md:rounded-[12px] md:p-0",
1475
+ "w-full max-h-[90vh] gap-3 rounded-[12px] border border-[#e0e0e0] bg-[#fdfdfd] p-0 shadow-[0_0_40px_0_rgba(0,0,0,0.06)] md:w-[448px] md:rounded-[12px] md:p-0",
1516
1476
  className
1517
1477
  ),
1518
- children: /* @__PURE__ */ jsxs6("div", { className: "flex h-[min(520px,62vh)] min-h-0 flex-col overflow-hidden md:h-[min(555px,70vh)]", children: [
1519
- /* @__PURE__ */ jsx9(
1520
- SearchInput,
1521
- {
1522
- value: search,
1523
- onChange: setSearch,
1524
- placeholder: "search or paste address",
1525
- className: "mb-3 shrink-0 px-[22px] md:hidden",
1526
- inputClassName: cx(SEARCH_INPUT_CLASS, searchClassName)
1527
- }
1528
- ),
1529
- /* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 flex-1", children: [
1530
- /* @__PURE__ */ jsx9(
1531
- NetworkRail,
1478
+ children: /* @__PURE__ */ jsxs5("div", { className: "flex h-[min(620px,70vh)] min-h-0 flex-col gap-2.5", children: [
1479
+ /* @__PURE__ */ jsxs5("div", { className: "flex shrink-0 items-center gap-3.5 px-5", children: [
1480
+ /* @__PURE__ */ jsx8(
1481
+ SearchInput2,
1532
1482
  {
1533
- role: payer ? "payer" : "receiver",
1534
- walletConnected,
1535
- selection: railSelection(view),
1536
- chains: railChains,
1537
- fundedBlockchains,
1483
+ value: search,
1484
+ onChange: setSearch,
1485
+ placeholder: "search or paste address",
1486
+ className: "min-w-0 flex-1",
1487
+ iconClassName: TOKEN_SEARCH_ICON_CLASS,
1488
+ inputClassName: cx(TOKEN_SEARCH_CLASS, searchClassName)
1489
+ }
1490
+ ),
1491
+ /* @__PURE__ */ jsx8(
1492
+ ChainSelect,
1493
+ {
1494
+ chains: menuChains,
1495
+ previewChains,
1496
+ dotted: dottedSet,
1497
+ value: chainFilter,
1498
+ onChange: (blockchain) => {
1499
+ userPickedChain.current = true;
1500
+ setChainFilter(blockchain);
1501
+ },
1538
1502
  lockChainKind,
1539
1503
  disabledBlockchains,
1540
- disabledReason,
1541
- onSelectWallet: () => setView({ kind: "wallet" }),
1542
- onSelectAll: () => setView({ kind: "all" }),
1543
- onSelectNetworks: () => setView({ kind: "networks" }),
1544
- onSelectChain: (blockchain) => handleSelectChain(blockchain, false),
1545
- className: railClassName
1504
+ disabledReason
1546
1505
  }
1547
- ),
1548
- searching ? /* @__PURE__ */ jsx9(
1506
+ )
1507
+ ] }),
1508
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-h-0 flex-1 flex-col gap-2.5", children: [
1509
+ payer && loading && recentTokens.length > 0 ? /* @__PURE__ */ jsx8(RecentTileSkeletons, {}) : null,
1510
+ payer && !loading ? /* @__PURE__ */ jsx8(RecentTiles, { tokens: recentTokens, onSelect: (token) => handleSelectToken(token, false), isTokenDisabled: tokenDisabled }) : null,
1511
+ searching ? /* @__PURE__ */ jsx8(
1549
1512
  SearchResults,
1550
1513
  {
1551
- groups: searchGroups,
1514
+ tokens: results,
1552
1515
  selectedAssetId,
1553
1516
  loading,
1554
1517
  showBalances,
1555
- showWalletStatus: payer,
1556
1518
  getBalance: balanceOf,
1557
1519
  isBalanceLoading: loadingOf,
1558
- onSelectToken: handleSelectToken,
1520
+ onSelectToken: (token) => handleSelectToken(token, true),
1559
1521
  isTokenDisabled: tokenDisabled,
1560
1522
  rowClassName
1561
1523
  }
1562
- ) : sliding ? /* @__PURE__ */ jsx9(
1563
- PaneTransition,
1524
+ ) : payer ? /* @__PURE__ */ jsx8(
1525
+ TokenPane,
1526
+ {
1527
+ sections,
1528
+ selectedAssetId,
1529
+ showBalances,
1530
+ getBalance: balanceOf,
1531
+ isBalanceLoading: loadingOf,
1532
+ onSelectToken: (token) => handleSelectToken(token, false),
1533
+ isTokenDisabled: tokenDisabled,
1534
+ rowClassName,
1535
+ emptyLabel: recentTokens.length === 0 && sections.every((section) => section.tokens.length === 0 && !section.skeletonRows) ? "No tokens found" : null
1536
+ }
1537
+ ) : loading ? /* @__PURE__ */ jsx8("div", { className: "min-h-0 flex-1 overflow-y-auto px-3", children: /* @__PURE__ */ jsx8(TokenListSkeleton, { rows: 6 }) }) : /* @__PURE__ */ jsx8(
1538
+ TokenPane,
1564
1539
  {
1565
- paneKey: view.kind === "network-chain" ? `network-chain:${view.blockchain}` : "networks",
1566
- direction: slideDirection,
1567
- children: view.kind === "networks" ? /* @__PURE__ */ jsx9(
1568
- ChainPane,
1569
- {
1570
- chains: networkChains,
1571
- showWalletStatus: payer,
1572
- onSelectChain: (blockchain) => handleSelectChain(blockchain, true),
1573
- lockChainKind,
1574
- disabledBlockchains,
1575
- disabledReason,
1576
- scrollRef: networksScrollRef
1577
- }
1578
- ) : tokenPane
1540
+ sections: [{
1541
+ id: "all",
1542
+ title: "",
1543
+ icon: "trends",
1544
+ tokens: receiverTokens
1545
+ }],
1546
+ selectedAssetId,
1547
+ showBalances: false,
1548
+ getBalance: balanceOf,
1549
+ isBalanceLoading: loadingOf,
1550
+ onSelectToken: (token) => handleSelectToken(token, false),
1551
+ isTokenDisabled: tokenDisabled,
1552
+ rowClassName,
1553
+ emptyLabel: receiverTokens.length === 0 ? "No tokens found" : null
1579
1554
  }
1580
- ) : /* @__PURE__ */ jsx9("div", { className: "relative min-h-0 flex-1", children: tokenPane })
1555
+ )
1581
1556
  ] })
1582
1557
  ] })
1583
1558
  }
@@ -1684,10 +1659,10 @@ function normalizePayConfig(source, options) {
1684
1659
  explorer: chain.explorer
1685
1660
  });
1686
1661
  }
1687
- const chainByCode2 = new Map(chains.map((chain) => [chain.blockchain, chain]));
1662
+ const chainByCode = new Map(chains.map((chain) => [chain.blockchain, chain]));
1688
1663
  const tokens = [];
1689
1664
  for (const token of source.tokens) {
1690
- const chain = chainByCode2.get(token.network);
1665
+ const chain = chainByCode.get(token.network);
1691
1666
  if (!chain) continue;
1692
1667
  const symbol = normalizeSymbol(token.symbol);
1693
1668
  if (!symbol) continue;
@@ -1720,7 +1695,6 @@ export {
1720
1695
  mapPayConfigSource,
1721
1696
  normalizePayConfig,
1722
1697
  ownerKey2 as ownerKey,
1723
- parsePopularTokens,
1724
1698
  refreshPayTokenBalance,
1725
1699
  tokenAssetId,
1726
1700
  usePayTokenBalance