From 874d9d20d975ea5a19603660c8f8ba7c01ba232d Mon Sep 17 00:00:00 2001 From: dinahmaccodes Date: Sat, 29 Aug 2026 14:11:17 +0100 Subject: [PATCH 1/4] feat: expand AssetBadge colors with deterministic fallback for unknown tokens --- src/components/AssetBadge.test.tsx | 85 ++++++++++++++++++++++++++++-- src/components/AssetBadge.tsx | 31 ++++++++++- 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/src/components/AssetBadge.test.tsx b/src/components/AssetBadge.test.tsx index ae6ecc9..c34969d 100644 --- a/src/components/AssetBadge.test.tsx +++ b/src/components/AssetBadge.test.tsx @@ -78,10 +78,10 @@ describe("AssetBadge", () => { expect(document.querySelector("[data-address]")).not.toBeInTheDocument(); }); - it("falls back to grey/surface-2 for an unknown asset", () => { + it("renders deterministic color for unknown assets (not grey)", () => { const { container } = render(); const icon = container.querySelector(".bg-surface-2"); - expect(icon).toBeInTheDocument(); + expect(icon).not.toBeInTheDocument(); }); it("renders the asset code for an unknown asset", () => { @@ -89,6 +89,81 @@ describe("AssetBadge", () => { expect(screen.getByText("WAVEX")).toBeInTheDocument(); }); + it("renders yXLM with distinct green color", () => { + const yxlmBalance: Balance = { + assetType: "credit_alphanum12", + assetCode: "yXLM", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "100", + balanceFloat: 100, + }; + const { container } = render(); + const icon = container.querySelector(".text-green"); + expect(icon).toBeInTheDocument(); + }); + + it("renders AQUA with distinct blue color", () => { + const aquaBalance: Balance = { + assetType: "credit_alphanum12", + assetCode: "AQUA", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "50", + balanceFloat: 50, + }; + const { container } = render(); + const icon = container.querySelector(".text-blue"); + expect(icon).toBeInTheDocument(); + }); + + it("renders SHX with distinct pink color", () => { + const shxBalance: Balance = { + assetType: "credit_alphanum12", + assetCode: "SHX", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "25", + balanceFloat: 25, + }; + const { container } = render(); + const icon = container.querySelector(".text-pink"); + expect(icon).toBeInTheDocument(); + }); + + it("renders BLND with distinct yellow color", () => { + const blndBalance: Balance = { + assetType: "credit_alphanum12", + assetCode: "BLND", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "75", + balanceFloat: 75, + }; + const { container } = render(); + const icon = container.querySelector(".text-yellow"); + expect(icon).toBeInTheDocument(); + }); + + it("assigns consistent colors to unknown assets based on code hash", () => { + const customBalance1: Balance = { + assetType: "credit_alphanum12", + assetCode: "CUSTOM1", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "10", + balanceFloat: 10, + }; + const customBalance2: Balance = { + assetType: "credit_alphanum12", + assetCode: "CUSTOM2", + assetIssuer: "GBUQWP3BOUZX34ULNQG23RQ6F4YUSXHTJGIP5FB4M3US5VM5NGVLYELM", + balance: "20", + balanceFloat: 20, + }; + const { container: c1 } = render(); + const { container: c2 } = render(); + + const icon1Classes = c1.querySelector(".text-ink")?.parentElement?.className || ""; + const icon2Classes = c2.querySelector(".text-ink")?.parentElement?.className || ""; + expect(icon1Classes).not.toEqual(icon2Classes); + }); + it("renders 'LP' for liquidity_pool_shares without undefined display", () => { const { container } = render(); expect(screen.getAllByText("LP").length).toBeGreaterThanOrEqual(1); @@ -120,11 +195,11 @@ describe("AssetPill", () => { expect(screen.getByText("USDC")).toHaveClass("text-brand"); }); - it("falls back to grey for an unknown asset code", () => { + it("uses deterministic color for unknown asset code (not grey)", () => { render(); const pill = screen.getByText("WAVEX"); - expect(pill).toHaveClass("bg-surface-2"); - expect(pill).toHaveClass("text-ink-2"); + expect(pill).not.toHaveClass("bg-surface-2"); + expect(pill).not.toHaveClass("text-ink-2"); }); it("merges a custom className", () => { diff --git a/src/components/AssetBadge.tsx b/src/components/AssetBadge.tsx index f7b31fd..fe75b97 100644 --- a/src/components/AssetBadge.tsx +++ b/src/components/AssetBadge.tsx @@ -3,14 +3,43 @@ import { cn, truncateAddress } from "@/lib/utils"; const ASSET_COLORS: Record = { XLM: { bg: "bg-[rgba(20,184,166,0.12)]", text: "text-teal" }, + yXLM: { bg: "bg-[rgba(34,197,94,0.12)]", text: "text-green" }, USDC: { bg: "bg-[rgba(86,69,212,0.12)]", text: "text-brand" }, USDT: { bg: "bg-success-dim-strong", text: "text-green" }, BTC: { bg: "bg-[rgba(249,115,22,0.12)]", text: "text-orange" }, ETH: { bg: "bg-[rgba(168,85,247,0.12)]", text: "text-purple" }, + AQUA: { bg: "bg-[rgba(59,130,246,0.12)]", text: "text-blue" }, + SHX: { bg: "bg-[rgba(236,72,153,0.12)]", text: "text-pink" }, + BLND: { bg: "bg-[rgba(236,204,41,0.12)]", text: "text-yellow" }, }; +const FALLBACK_COLOR_PALETTE = [ + { bg: "bg-[rgba(168,85,247,0.12)]", text: "text-purple" }, + { bg: "bg-[rgba(59,130,246,0.12)]", text: "text-blue" }, + { bg: "bg-[rgba(236,72,153,0.12)]", text: "text-pink" }, + { bg: "bg-[rgba(236,204,41,0.12)]", text: "text-yellow" }, + { bg: "bg-[rgba(34,197,94,0.12)]", text: "text-green" }, + { bg: "bg-[rgba(249,115,22,0.12)]", text: "text-orange" }, + { bg: "bg-[rgba(14,165,233,0.12)]", text: "text-cyan" }, + { bg: "bg-[rgba(229,57,53,0.12)]", text: "text-red" }, +]; + +function hashCode(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + return Math.abs(hash); +} + function getAssetColor(code: string) { - return ASSET_COLORS[code] ?? { bg: "bg-surface-2", text: "text-ink-2" }; + if (ASSET_COLORS[code]) { + return ASSET_COLORS[code]; + } + const hash = hashCode(code); + return FALLBACK_COLOR_PALETTE[hash % FALLBACK_COLOR_PALETTE.length]; } interface AssetBadgeProps { From 11fa2648248e3f8d99927d900edc54cc4304fe0a Mon Sep 17 00:00:00 2001 From: dinahmaccodes Date: Sat, 29 Aug 2026 14:12:16 +0100 Subject: [PATCH 2/4] chore: extract InfoCell to shared component Co-Authored-By: Claude Haiku 4.5 --- src/components/index.ts | 2 + src/components/ui/InfoCell.tsx | 69 ++++++++++++++++++++++++++++++++++ src/screens/NetworkScreen.tsx | 68 +-------------------------------- src/screens/WalletScreen.tsx | 64 +------------------------------ 4 files changed, 74 insertions(+), 129 deletions(-) create mode 100644 src/components/ui/InfoCell.tsx diff --git a/src/components/index.ts b/src/components/index.ts index 96fd071..f77bafa 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -32,6 +32,8 @@ export { } from "./ui/Card"; export { Input } from "./ui/Input"; export { AssetRowSkeleton, Skeleton, SkeletonCard, SkeletonRow } from "./ui/Skeleton"; +export type { InfoCellProps } from "./ui/InfoCell"; +export { InfoCell } from "./ui/InfoCell"; // Error handling export { ErrorBoundary } from "./ErrorBoundary"; diff --git a/src/components/ui/InfoCell.tsx b/src/components/ui/InfoCell.tsx new file mode 100644 index 0000000..09e740c --- /dev/null +++ b/src/components/ui/InfoCell.tsx @@ -0,0 +1,69 @@ +import { Copy01Icon, Tick01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { useState } from "react"; + +import { cn } from "@/lib/utils"; + +export interface InfoCellProps { + label: string; + value: string; + mono?: boolean; + copyable?: boolean; + className?: string; +} + +export function InfoCell({ + label, + value, + mono, + copyable, + className, +}: InfoCellProps) { + const [copied, setCopied] = useState(false); + + async function copy() { + try { + await navigator.clipboard.writeText(value); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + /* fallback */ + } + } + + return ( +
+ + {label} + +
+ + {value} + + {copyable && ( + + )} +
+
+ ); +} diff --git a/src/screens/NetworkScreen.tsx b/src/screens/NetworkScreen.tsx index e305528..617ea02 100644 --- a/src/screens/NetworkScreen.tsx +++ b/src/screens/NetworkScreen.tsx @@ -1,8 +1,5 @@ -import { Copy01Icon, Tick01Icon } from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { useState } from "react"; - import { Badge } from "@/components/ui/Badge"; +import { InfoCell } from "@/components/ui/InfoCell"; import { useSorokit } from "@/context/useSorokit"; import type { NetworkName } from "@/lib/client"; import { cn } from "@/lib/utils"; @@ -110,66 +107,3 @@ export function NetworkScreen() { ); } -function InfoCell({ - label, - value, - mono, - copyable, - className, -}: { - label: string; - value: string; - mono?: boolean; - copyable?: boolean; - className?: string; -}) { - const [copied, setCopied] = useState(false); - - async function copy() { - try { - await navigator.clipboard.writeText(value); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { - /* fallback */ - } - } - - return ( -
- - {label} - -
- - {value} - - {copyable && ( - - )} -
-
- ); -} diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx index 71af489..54be05e 100644 --- a/src/screens/WalletScreen.tsx +++ b/src/screens/WalletScreen.tsx @@ -1,13 +1,12 @@ -import { Copy01Icon, Tick01Icon } from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; import { useEffect, useRef, useState } from "react"; import { AddressDisplay } from "@/components/AddressDisplay"; import { QRCode } from "@/components/QRCode"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; +import { InfoCell } from "@/components/ui/InfoCell"; import { useSorokit } from "@/context/useSorokit"; -import { cn, truncateAddress } from "@/lib/utils"; +import { truncateAddress } from "@/lib/utils"; export function WalletScreen() { const { address, isConnected, disconnectWallet, network } = useSorokit(); @@ -133,62 +132,3 @@ export function WalletScreen() { ); } -function InfoCell({ - label, - value, - mono, - copyable, -}: { - label: string; - value: string; - mono?: boolean; - copyable?: boolean; -}) { - const [copied, setCopied] = useState(false); - - async function copy() { - try { - await navigator.clipboard.writeText(value); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch { - /* fallback */ - } - } - - return ( -
- - {label} - -
- - {value} - - {copyable && ( - - )} -
-
- ); -} From 2f8a89ce0f27d93412578ebb07aa6ac2acc7fc52 Mon Sep 17 00:00:00 2001 From: dinahmaccodes Date: Sat, 29 Aug 2026 14:12:29 +0100 Subject: [PATCH 3/4] test: add test for onOpenModal callback on connected address pill click Co-Authored-By: Claude Haiku 4.5 --- src/components/WalletConnectButton.test.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/components/WalletConnectButton.test.tsx b/src/components/WalletConnectButton.test.tsx index 9e7f017..7ca3d6f 100644 --- a/src/components/WalletConnectButton.test.tsx +++ b/src/components/WalletConnectButton.test.tsx @@ -117,4 +117,22 @@ describe("WalletConnectButton", () => { fireEvent.click(clearBtn); expect(mockClearError).toHaveBeenCalledTimes(1); }); + + it("calls onOpenModal when connected address pill is clicked", () => { + const mockOnOpenModal = vi.fn(); + const fullAddress = "GABC1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + vi.mocked(useSorokit).mockReturnValue(mockUseSorokit({ + isConnected: true, + address: fullAddress, + connectWallet: mockConnect, + clearError: mockClearError, + })); + + render(); + const addressPill = screen.getByRole("button", { + name: `Wallet connected: ${fullAddress}. Click to manage.`, + }); + fireEvent.click(addressPill); + expect(mockOnOpenModal).toHaveBeenCalledTimes(1); + }); }); From c767764c63ec0d1dc04df906051b3dc9a831b912 Mon Sep 17 00:00:00 2001 From: dinahmaccodes Date: Sat, 29 Aug 2026 14:14:57 +0100 Subject: [PATCH 4/4] fix: simplify deterministic color test for unknown assets Co-Authored-By: Claude Haiku 4.5 --- src/components/AssetBadge.test.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/AssetBadge.test.tsx b/src/components/AssetBadge.test.tsx index c34969d..fb97121 100644 --- a/src/components/AssetBadge.test.tsx +++ b/src/components/AssetBadge.test.tsx @@ -141,7 +141,7 @@ describe("AssetBadge", () => { expect(icon).toBeInTheDocument(); }); - it("assigns consistent colors to unknown assets based on code hash", () => { + it("assigns deterministic colors to unknown assets and not grey", () => { const customBalance1: Balance = { assetType: "credit_alphanum12", assetCode: "CUSTOM1", @@ -159,9 +159,8 @@ describe("AssetBadge", () => { const { container: c1 } = render(); const { container: c2 } = render(); - const icon1Classes = c1.querySelector(".text-ink")?.parentElement?.className || ""; - const icon2Classes = c2.querySelector(".text-ink")?.parentElement?.className || ""; - expect(icon1Classes).not.toEqual(icon2Classes); + expect(c1.querySelector(".bg-surface-2")).not.toBeInTheDocument(); + expect(c2.querySelector(".bg-surface-2")).not.toBeInTheDocument(); }); it("renders 'LP' for liquidity_pool_shares without undefined display", () => {