@medialane/ui 0.24.0 → 0.25.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.
@@ -40,80 +40,65 @@ var import_image = __toESM(require("next/image"), 1);
40
40
  var import_cn = require("../utils/cn.js");
41
41
  var import_ipfs = require("../utils/ipfs.js");
42
42
  var import_motion_primitives = require("./motion-primitives.js");
43
+ var import_currency_icon = require("./currency-icon.js");
43
44
  var import_coins = require("../data/coins.js");
44
45
  const KIND_TAG = {
45
- creator: "border-brand-purple/30 bg-brand-purple/10 text-brand-purple",
46
- memecoin: "border-brand-rose/30 bg-brand-rose/10 text-brand-rose"
46
+ creator: "border-brand-blue/30 bg-brand-blue/10 text-brand-blue",
47
+ memecoin: "border-brand-purple/30 bg-brand-purple/10 text-brand-purple"
47
48
  };
48
- const KIND_GLOW = {
49
- creator: "bg-brand-purple/25",
50
- memecoin: "bg-brand-rose/25"
49
+ const KIND_OVER_IMAGE = {
50
+ creator: "bg-brand-blue/85 text-white",
51
+ memecoin: "bg-brand-purple/85 text-white"
51
52
  };
52
53
  const KIND_FALLBACK = {
53
54
  creator: "from-brand-blue to-brand-purple",
54
55
  memecoin: "from-brand-purple to-brand-rose"
55
56
  };
57
+ const kindLabel = (kind) => kind === "creator" ? "Creator Coin" : "Memecoin";
56
58
  function useTileModel({ collection, usePrice }) {
57
59
  const { price, isLoading } = usePrice(collection);
58
60
  const kind = (0, import_coins.coinKind)(collection.service);
59
61
  const logoUri = collection.profile?.image ?? collection.image;
60
62
  const logo = logoUri ? (0, import_ipfs.ipfsToHttp)(logoUri) : null;
61
63
  const initials = (collection.symbol ?? collection.name ?? "?").trim().slice(0, 2).toUpperCase();
62
- const chain = (collection.chain ?? "").toString().toLowerCase();
63
- return { price, isLoading, kind, logo, initials, chain };
64
+ return { price, isLoading, kind, logo, initials };
64
65
  }
65
- function PriceSkel() {
66
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse" });
67
- }
68
- function KindChip({ kind, className }) {
69
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium", KIND_TAG[kind], className), children: kind === "creator" ? "Creator Coin" : "Memecoin" });
66
+ function PriceValue({ price, isLoading }) {
67
+ if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse" });
68
+ if (!price) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground", children: "\u2014" });
69
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex items-center gap-1 font-semibold tabular-nums", children: [
70
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_currency_icon.CurrencyIcon, { symbol: price.quoteSymbol, size: 14 }),
71
+ (0, import_coins.formatCoinPrice)(price.quotePerCoin)
72
+ ] });
70
73
  }
71
74
  function CoinCard({ collection, usePrice, href }) {
72
75
  const m = useTileModel({ collection, usePrice });
73
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "group relative h-full", children: [
74
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
75
- "div",
76
- {
77
- "aria-hidden": true,
78
- className: (0, import_cn.cn)(
79
- "pointer-events-none absolute -inset-2 rounded-[1.4rem] opacity-60 blur-xl transition-opacity duration-500 group-hover:opacity-100",
80
- KIND_GLOW[m.kind]
81
- )
82
- }
83
- ),
84
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_motion_primitives.MotionCard, { className: "card-base relative flex h-full flex-col", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href, className: "flex h-full flex-col", children: [
85
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative aspect-square w-full overflow-hidden bg-muted", children: [
86
- m.logo ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
87
- import_image.default,
88
- {
89
- src: m.logo,
90
- alt: collection.name ?? "Coin",
91
- fill: true,
92
- sizes: "(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw",
93
- className: "object-cover transition-transform duration-500 group-hover:scale-105",
94
- unoptimized: true
95
- }
96
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_cn.cn)("absolute inset-0 flex items-center justify-center bg-gradient-to-br", KIND_FALLBACK[m.kind]), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "select-none text-5xl font-black tracking-tighter text-white/90", children: m.initials }) }),
97
- m.chain && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute bottom-2 left-2 rounded-full bg-black/40 px-2 py-0.5 text-[9px] uppercase tracking-wide text-white/80 backdrop-blur-md", children: m.chain })
76
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_motion_primitives.MotionCard, { className: "card-base group relative flex h-full flex-col", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href, className: "flex h-full flex-col", children: [
77
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative aspect-square w-full overflow-hidden bg-muted", children: [
78
+ m.logo ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
79
+ import_image.default,
80
+ {
81
+ src: m.logo,
82
+ alt: collection.name ?? "Coin",
83
+ fill: true,
84
+ sizes: "(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw",
85
+ className: "object-cover transition-transform duration-500 group-hover:scale-105",
86
+ unoptimized: true
87
+ }
88
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_cn.cn)("absolute inset-0 flex items-center justify-center bg-gradient-to-br", KIND_FALLBACK[m.kind]), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "select-none text-5xl font-black tracking-tighter text-white/90", children: m.initials }) }),
89
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("absolute left-2 top-2 rounded-full px-2 py-0.5 text-[10px] font-semibold backdrop-blur-md", KIND_OVER_IMAGE[m.kind]), children: kindLabel(m.kind) })
90
+ ] }),
91
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-2 p-3", children: [
92
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0", children: [
93
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "truncate font-semibold leading-tight", children: collection.name ?? "Untitled coin" }),
94
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "truncate text-sm text-muted-foreground", children: collection.symbol ?? "\u2014" })
98
95
  ] }),
99
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-1 flex-col gap-3 p-3", children: [
100
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [
101
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0", children: [
102
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "truncate font-semibold leading-tight", children: collection.name ?? "Untitled coin" }),
103
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "truncate text-sm text-muted-foreground", children: collection.symbol ?? "\u2014" })
104
- ] }),
105
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KindChip, { kind: m.kind })
106
- ] }),
107
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-auto space-y-2", children: [
108
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-baseline justify-between gap-2", children: [
109
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: "Price" }),
110
- m.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceSkel, {}) : m.price ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-semibold tabular-nums", children: (0, import_coins.formatCoinPrice)(m.price.quotePerCoin) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground", children: "\u2014" })
111
- ] }),
112
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "block rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple px-3 py-2 text-center text-sm font-semibold text-white", children: "Trade" })
113
- ] })
96
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-baseline justify-between gap-2", children: [
97
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: "Price" }),
98
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceValue, { price: m.price, isLoading: m.isLoading })
114
99
  ] })
115
- ] }) })
116
- ] });
100
+ ] })
101
+ ] }) });
117
102
  }
118
103
  function CoinRow({ collection, usePrice, href }) {
119
104
  const m = useTileModel({ collection, usePrice });
@@ -123,20 +108,19 @@ function CoinRow({ collection, usePrice, href }) {
123
108
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "truncate text-sm font-semibold", children: collection.name ?? "Untitled coin" }),
124
109
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "block truncate text-xs text-muted-foreground", children: collection.symbol ?? "\u2014" })
125
110
  ] }),
126
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KindChip, { kind: m.kind, className: "hidden sm:inline-block" }),
127
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-24 shrink-0 text-right text-sm font-semibold tabular-nums", children: m.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ml-auto inline-block h-4 w-12 rounded bg-muted-foreground/20 animate-pulse" }) : m.price ? (0, import_coins.formatCoinPrice)(m.price.quotePerCoin) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground", children: "\u2014" }) }),
128
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "hidden shrink-0 text-sm font-medium text-primary sm:inline-block", children: "Trade" })
111
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block", KIND_TAG[m.kind]), children: kindLabel(m.kind) }),
112
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-28 shrink-0 text-right text-sm", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceValue, { price: m.price, isLoading: m.isLoading }) })
129
113
  ] });
130
114
  }
131
115
  function CoinCardSkeleton() {
132
116
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "card-base flex flex-col", children: [
133
117
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "aspect-square w-full animate-pulse bg-muted" }),
134
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-3 p-3", children: [
118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-2 p-3", children: [
135
119
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1.5", children: [
136
120
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-4 w-24 rounded bg-muted-foreground/20 animate-pulse" }),
137
121
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-3 w-12 rounded bg-muted-foreground/20 animate-pulse" })
138
122
  ] }),
139
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-9 w-full rounded-lg bg-muted-foreground/10 animate-pulse" })
123
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-4 w-full rounded bg-muted-foreground/10 animate-pulse" })
140
124
  ] })
141
125
  ] });
142
126
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic, art-forward coin discovery tiles.\n *\n * Pure presentation: the price read (`usePrice`) and the link target (`href`)\n * are injected by the consuming app, and the collection is typed structurally\n * (CoinCollectionLike) — so a coin on Starknet, Ethereum, or Solana renders the\n * same with zero changes here. The coin's artwork is the hero; the only stat is\n * the live spot price (FDV / holders / a \"verified\" mark are intentionally not\n * shown — Medialane indexes none of them).\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport {\n coinKind, formatCoinPrice,\n type CoinCollectionLike, type CoinPriceLike,\n} from \"../data/coins.js\";\n\n/** Injected per-chain spot-price read. `price` is null while loading/unknown. */\nexport type UseCoinPrice = (collection: CoinCollectionLike) => {\n price: CoinPriceLike | null;\n isLoading: boolean;\n};\n\nexport interface CoinTileProps {\n collection: CoinCollectionLike;\n usePrice: UseCoinPrice;\n /** Link target — internal coin page or the per-chain trading app. */\n href: string;\n}\n\nconst KIND_TAG: Record<string, string> = {\n creator: \"border-brand-purple/30 bg-brand-purple/10 text-brand-purple\",\n memecoin: \"border-brand-rose/30 bg-brand-rose/10 text-brand-rose\",\n};\n\n// Aurora glow + art-fallback gradient, tinted by kind (foundations §III: rose\n// is the coin surface, purple/blue the creator accent).\nconst KIND_GLOW: Record<string, string> = {\n creator: \"bg-brand-purple/25\",\n memecoin: \"bg-brand-rose/25\",\n};\nconst KIND_FALLBACK: Record<string, string> = {\n creator: \"from-brand-blue to-brand-purple\",\n memecoin: \"from-brand-purple to-brand-rose\",\n};\n\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const logoUri = collection.profile?.image ?? collection.image;\n const logo = logoUri ? ipfsToHttp(logoUri) : null;\n const initials = (collection.symbol ?? collection.name ?? \"?\").trim().slice(0, 2).toUpperCase();\n const chain = (collection.chain ?? \"\").toString().toLowerCase();\n return { price, isLoading, kind, logo, initials, chain };\n}\n\nfunction PriceSkel() {\n return <span className=\"inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse\" />;\n}\n\nfunction KindChip({ kind, className }: { kind: string; className?: string }) {\n return (\n <span className={cn(\"shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium\", KIND_TAG[kind], className)}>\n {kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\n );\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <div className=\"group relative h-full\">\n {/* Aurora glow — soft, kind-tinted, blooms past the card edge. */}\n <div\n aria-hidden\n className={cn(\n \"pointer-events-none absolute -inset-2 rounded-[1.4rem] opacity-60 blur-xl transition-opacity duration-500 group-hover:opacity-100\",\n KIND_GLOW[m.kind]\n )}\n />\n <MotionCard className=\"card-base relative flex h-full flex-col\">\n <Link href={href} className=\"flex h-full flex-col\">\n {/* Hero artwork */}\n <div className=\"relative aspect-square w-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image\n src={m.logo}\n alt={collection.name ?? \"Coin\"}\n fill\n sizes=\"(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n unoptimized\n />\n ) : (\n <div className={cn(\"absolute inset-0 flex items-center justify-center bg-gradient-to-br\", KIND_FALLBACK[m.kind])}>\n <span className=\"select-none text-5xl font-black tracking-tighter text-white/90\">{m.initials}</span>\n </div>\n )}\n {m.chain && (\n <span className=\"absolute bottom-2 left-2 rounded-full bg-black/40 px-2 py-0.5 text-[9px] uppercase tracking-wide text-white/80 backdrop-blur-md\">\n {m.chain}\n </span>\n )}\n </div>\n\n {/* Body */}\n <div className=\"flex flex-1 flex-col gap-3 p-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <div className=\"truncate font-semibold leading-tight\">{collection.name ?? \"Untitled coin\"}</div>\n <div className=\"truncate text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</div>\n </div>\n <KindChip kind={m.kind} />\n </div>\n <div className=\"mt-auto space-y-2\">\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Price</span>\n {m.isLoading ? <PriceSkel /> : m.price ? (\n <span className=\"font-semibold tabular-nums\">{formatCoinPrice(m.price.quotePerCoin)}</span>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </div>\n <span className=\"block rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple px-3 py-2 text-center text-sm font-semibold text-white\">\n Trade\n </span>\n </div>\n </div>\n </Link>\n </MotionCard>\n </div>\n );\n}\n\nexport function CoinRow({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40 active:scale-[0.99]\">\n <div className=\"relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted\">\n {m.logo ? <Image src={m.logo} alt=\"\" fill sizes=\"36px\" className=\"object-cover\" unoptimized /> :\n <div className={cn(\"flex h-full w-full items-center justify-center bg-gradient-to-br text-[11px] font-bold text-white\", KIND_FALLBACK[m.kind])}>{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <KindChip kind={m.kind} className=\"hidden sm:inline-block\" />\n <span className=\"w-24 shrink-0 text-right text-sm font-semibold tabular-nums\">\n {m.isLoading ? <span className=\"ml-auto inline-block h-4 w-12 rounded bg-muted-foreground/20 animate-pulse\" /> : m.price ? formatCoinPrice(m.price.quotePerCoin) : <span className=\"text-muted-foreground\">—</span>}\n </span>\n <span className=\"hidden shrink-0 text-sm font-medium text-primary sm:inline-block\">Trade</span>\n </Link>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"card-base flex flex-col\">\n <div className=\"aspect-square w-full animate-pulse bg-muted\" />\n <div className=\"flex flex-col gap-3 p-3\">\n <div className=\"space-y-1.5\">\n <div className=\"h-4 w-24 rounded bg-muted-foreground/20 animate-pulse\" />\n <div className=\"h-3 w-12 rounded bg-muted-foreground/20 animate-pulse\" />\n </div>\n <div className=\"h-9 w-full rounded-lg bg-muted-foreground/10 animate-pulse\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["Link","Image"]}
1
+ {"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic, art-forward coin discovery tiles.\n *\n * Pure presentation: the price read (`usePrice`) and the link target (`href`)\n * are injected by the consuming app, and the collection is typed structurally\n * (CoinCollectionLike) — so a coin on Starknet, Ethereum, or Solana renders the\n * same with zero changes here. The coin's artwork is the hero; the whole tile\n * links to the coin page (where trading happens), and the only stat is the live\n * spot price with its quote-currency icon. FDV / holders / a \"verified\" mark are\n * intentionally not shown — Medialane indexes none of them.\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport {\n coinKind, formatCoinPrice,\n type CoinCollectionLike, type CoinPriceLike,\n} from \"../data/coins.js\";\n\n/** Injected per-chain spot-price read. `price` is null while loading/unknown. */\nexport type UseCoinPrice = (collection: CoinCollectionLike) => {\n price: CoinPriceLike | null;\n isLoading: boolean;\n};\n\nexport interface CoinTileProps {\n collection: CoinCollectionLike;\n usePrice: UseCoinPrice;\n /** Link target — internal coin page or the per-chain trading app. */\n href: string;\n}\n\n// Kind tag on a card surface (table row) — brand hues, never red.\nconst KIND_TAG: Record<string, string> = {\n creator: \"border-brand-blue/30 bg-brand-blue/10 text-brand-blue\",\n memecoin: \"border-brand-purple/30 bg-brand-purple/10 text-brand-purple\",\n};\n// Kind tag layered over artwork — solid brand pill, white text for legibility.\nconst KIND_OVER_IMAGE: Record<string, string> = {\n creator: \"bg-brand-blue/85 text-white\",\n memecoin: \"bg-brand-purple/85 text-white\",\n};\n// Art-fallback gradient (no logo) — vivid brand wash.\nconst KIND_FALLBACK: Record<string, string> = {\n creator: \"from-brand-blue to-brand-purple\",\n memecoin: \"from-brand-purple to-brand-rose\",\n};\n\nconst kindLabel = (kind: string) => (kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\");\n\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const logoUri = collection.profile?.image ?? collection.image;\n const logo = logoUri ? ipfsToHttp(logoUri) : null;\n const initials = (collection.symbol ?? collection.name ?? \"?\").trim().slice(0, 2).toUpperCase();\n return { price, isLoading, kind, logo, initials };\n}\n\nfunction PriceValue({ price, isLoading }: { price: CoinPriceLike | null; isLoading: boolean }) {\n if (isLoading) return <span className=\"inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse\" />;\n if (!price) return <span className=\"text-muted-foreground\">—</span>;\n return (\n <span className=\"inline-flex items-center gap-1 font-semibold tabular-nums\">\n <CurrencyIcon symbol={price.quoteSymbol} size={14} />\n {formatCoinPrice(price.quotePerCoin)}\n </span>\n );\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <MotionCard className=\"card-base group relative flex h-full flex-col\">\n <Link href={href} className=\"flex h-full flex-col\">\n {/* Hero artwork */}\n <div className=\"relative aspect-square w-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image\n src={m.logo}\n alt={collection.name ?? \"Coin\"}\n fill\n sizes=\"(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n unoptimized\n />\n ) : (\n <div className={cn(\"absolute inset-0 flex items-center justify-center bg-gradient-to-br\", KIND_FALLBACK[m.kind])}>\n <span className=\"select-none text-5xl font-black tracking-tighter text-white/90\">{m.initials}</span>\n </div>\n )}\n <span className={cn(\"absolute left-2 top-2 rounded-full px-2 py-0.5 text-[10px] font-semibold backdrop-blur-md\", KIND_OVER_IMAGE[m.kind])}>\n {kindLabel(m.kind)}\n </span>\n </div>\n\n {/* Body */}\n <div className=\"flex flex-col gap-2 p-3\">\n <div className=\"min-w-0\">\n <div className=\"truncate font-semibold leading-tight\">{collection.name ?? \"Untitled coin\"}</div>\n <div className=\"truncate text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</div>\n </div>\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Price</span>\n <PriceValue price={m.price} isLoading={m.isLoading} />\n </div>\n </div>\n </Link>\n </MotionCard>\n );\n}\n\nexport function CoinRow({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40 active:scale-[0.99]\">\n <div className=\"relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted\">\n {m.logo ? <Image src={m.logo} alt=\"\" fill sizes=\"36px\" className=\"object-cover\" unoptimized /> :\n <div className={cn(\"flex h-full w-full items-center justify-center bg-gradient-to-br text-[11px] font-bold text-white\", KIND_FALLBACK[m.kind])}>{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block\", KIND_TAG[m.kind])}>\n {kindLabel(m.kind)}\n </span>\n <span className=\"w-28 shrink-0 text-right text-sm\">\n <PriceValue price={m.price} isLoading={m.isLoading} />\n </span>\n </Link>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"card-base flex flex-col\">\n <div className=\"aspect-square w-full animate-pulse bg-muted\" />\n <div className=\"flex flex-col gap-2 p-3\">\n <div className=\"space-y-1.5\">\n <div className=\"h-4 w-24 rounded bg-muted-foreground/20 animate-pulse\" />\n <div className=\"h-3 w-12 rounded bg-muted-foreground/20 animate-pulse\" />\n </div>\n <div className=\"h-4 w-full rounded bg-muted-foreground/10 animate-pulse\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["Link","Image"]}
@@ -5,83 +5,68 @@ import Image from "next/image";
5
5
  import { cn } from "../utils/cn.js";
6
6
  import { ipfsToHttp } from "../utils/ipfs.js";
7
7
  import { MotionCard } from "./motion-primitives.js";
8
+ import { CurrencyIcon } from "./currency-icon.js";
8
9
  import {
9
10
  coinKind,
10
11
  formatCoinPrice
11
12
  } from "../data/coins.js";
12
13
  const KIND_TAG = {
13
- creator: "border-brand-purple/30 bg-brand-purple/10 text-brand-purple",
14
- memecoin: "border-brand-rose/30 bg-brand-rose/10 text-brand-rose"
14
+ creator: "border-brand-blue/30 bg-brand-blue/10 text-brand-blue",
15
+ memecoin: "border-brand-purple/30 bg-brand-purple/10 text-brand-purple"
15
16
  };
16
- const KIND_GLOW = {
17
- creator: "bg-brand-purple/25",
18
- memecoin: "bg-brand-rose/25"
17
+ const KIND_OVER_IMAGE = {
18
+ creator: "bg-brand-blue/85 text-white",
19
+ memecoin: "bg-brand-purple/85 text-white"
19
20
  };
20
21
  const KIND_FALLBACK = {
21
22
  creator: "from-brand-blue to-brand-purple",
22
23
  memecoin: "from-brand-purple to-brand-rose"
23
24
  };
25
+ const kindLabel = (kind) => kind === "creator" ? "Creator Coin" : "Memecoin";
24
26
  function useTileModel({ collection, usePrice }) {
25
27
  const { price, isLoading } = usePrice(collection);
26
28
  const kind = coinKind(collection.service);
27
29
  const logoUri = collection.profile?.image ?? collection.image;
28
30
  const logo = logoUri ? ipfsToHttp(logoUri) : null;
29
31
  const initials = (collection.symbol ?? collection.name ?? "?").trim().slice(0, 2).toUpperCase();
30
- const chain = (collection.chain ?? "").toString().toLowerCase();
31
- return { price, isLoading, kind, logo, initials, chain };
32
+ return { price, isLoading, kind, logo, initials };
32
33
  }
33
- function PriceSkel() {
34
- return /* @__PURE__ */ jsx("span", { className: "inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse" });
35
- }
36
- function KindChip({ kind, className }) {
37
- return /* @__PURE__ */ jsx("span", { className: cn("shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium", KIND_TAG[kind], className), children: kind === "creator" ? "Creator Coin" : "Memecoin" });
34
+ function PriceValue({ price, isLoading }) {
35
+ if (isLoading) return /* @__PURE__ */ jsx("span", { className: "inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse" });
36
+ if (!price) return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" });
37
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 font-semibold tabular-nums", children: [
38
+ /* @__PURE__ */ jsx(CurrencyIcon, { symbol: price.quoteSymbol, size: 14 }),
39
+ formatCoinPrice(price.quotePerCoin)
40
+ ] });
38
41
  }
39
42
  function CoinCard({ collection, usePrice, href }) {
40
43
  const m = useTileModel({ collection, usePrice });
41
- return /* @__PURE__ */ jsxs("div", { className: "group relative h-full", children: [
42
- /* @__PURE__ */ jsx(
43
- "div",
44
- {
45
- "aria-hidden": true,
46
- className: cn(
47
- "pointer-events-none absolute -inset-2 rounded-[1.4rem] opacity-60 blur-xl transition-opacity duration-500 group-hover:opacity-100",
48
- KIND_GLOW[m.kind]
49
- )
50
- }
51
- ),
52
- /* @__PURE__ */ jsx(MotionCard, { className: "card-base relative flex h-full flex-col", children: /* @__PURE__ */ jsxs(Link, { href, className: "flex h-full flex-col", children: [
53
- /* @__PURE__ */ jsxs("div", { className: "relative aspect-square w-full overflow-hidden bg-muted", children: [
54
- m.logo ? /* @__PURE__ */ jsx(
55
- Image,
56
- {
57
- src: m.logo,
58
- alt: collection.name ?? "Coin",
59
- fill: true,
60
- sizes: "(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw",
61
- className: "object-cover transition-transform duration-500 group-hover:scale-105",
62
- unoptimized: true
63
- }
64
- ) : /* @__PURE__ */ jsx("div", { className: cn("absolute inset-0 flex items-center justify-center bg-gradient-to-br", KIND_FALLBACK[m.kind]), children: /* @__PURE__ */ jsx("span", { className: "select-none text-5xl font-black tracking-tighter text-white/90", children: m.initials }) }),
65
- m.chain && /* @__PURE__ */ jsx("span", { className: "absolute bottom-2 left-2 rounded-full bg-black/40 px-2 py-0.5 text-[9px] uppercase tracking-wide text-white/80 backdrop-blur-md", children: m.chain })
44
+ return /* @__PURE__ */ jsx(MotionCard, { className: "card-base group relative flex h-full flex-col", children: /* @__PURE__ */ jsxs(Link, { href, className: "flex h-full flex-col", children: [
45
+ /* @__PURE__ */ jsxs("div", { className: "relative aspect-square w-full overflow-hidden bg-muted", children: [
46
+ m.logo ? /* @__PURE__ */ jsx(
47
+ Image,
48
+ {
49
+ src: m.logo,
50
+ alt: collection.name ?? "Coin",
51
+ fill: true,
52
+ sizes: "(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw",
53
+ className: "object-cover transition-transform duration-500 group-hover:scale-105",
54
+ unoptimized: true
55
+ }
56
+ ) : /* @__PURE__ */ jsx("div", { className: cn("absolute inset-0 flex items-center justify-center bg-gradient-to-br", KIND_FALLBACK[m.kind]), children: /* @__PURE__ */ jsx("span", { className: "select-none text-5xl font-black tracking-tighter text-white/90", children: m.initials }) }),
57
+ /* @__PURE__ */ jsx("span", { className: cn("absolute left-2 top-2 rounded-full px-2 py-0.5 text-[10px] font-semibold backdrop-blur-md", KIND_OVER_IMAGE[m.kind]), children: kindLabel(m.kind) })
58
+ ] }),
59
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 p-3", children: [
60
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
61
+ /* @__PURE__ */ jsx("div", { className: "truncate font-semibold leading-tight", children: collection.name ?? "Untitled coin" }),
62
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm text-muted-foreground", children: collection.symbol ?? "\u2014" })
66
63
  ] }),
67
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col gap-3 p-3", children: [
68
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
69
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
70
- /* @__PURE__ */ jsx("div", { className: "truncate font-semibold leading-tight", children: collection.name ?? "Untitled coin" }),
71
- /* @__PURE__ */ jsx("div", { className: "truncate text-sm text-muted-foreground", children: collection.symbol ?? "\u2014" })
72
- ] }),
73
- /* @__PURE__ */ jsx(KindChip, { kind: m.kind })
74
- ] }),
75
- /* @__PURE__ */ jsxs("div", { className: "mt-auto space-y-2", children: [
76
- /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
77
- /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: "Price" }),
78
- m.isLoading ? /* @__PURE__ */ jsx(PriceSkel, {}) : m.price ? /* @__PURE__ */ jsx("span", { className: "font-semibold tabular-nums", children: formatCoinPrice(m.price.quotePerCoin) }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" })
79
- ] }),
80
- /* @__PURE__ */ jsx("span", { className: "block rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple px-3 py-2 text-center text-sm font-semibold text-white", children: "Trade" })
81
- ] })
64
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
65
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: "Price" }),
66
+ /* @__PURE__ */ jsx(PriceValue, { price: m.price, isLoading: m.isLoading })
82
67
  ] })
83
- ] }) })
84
- ] });
68
+ ] })
69
+ ] }) });
85
70
  }
86
71
  function CoinRow({ collection, usePrice, href }) {
87
72
  const m = useTileModel({ collection, usePrice });
@@ -91,20 +76,19 @@ function CoinRow({ collection, usePrice, href }) {
91
76
  /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold", children: collection.name ?? "Untitled coin" }),
92
77
  /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-muted-foreground", children: collection.symbol ?? "\u2014" })
93
78
  ] }),
94
- /* @__PURE__ */ jsx(KindChip, { kind: m.kind, className: "hidden sm:inline-block" }),
95
- /* @__PURE__ */ jsx("span", { className: "w-24 shrink-0 text-right text-sm font-semibold tabular-nums", children: m.isLoading ? /* @__PURE__ */ jsx("span", { className: "ml-auto inline-block h-4 w-12 rounded bg-muted-foreground/20 animate-pulse" }) : m.price ? formatCoinPrice(m.price.quotePerCoin) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }),
96
- /* @__PURE__ */ jsx("span", { className: "hidden shrink-0 text-sm font-medium text-primary sm:inline-block", children: "Trade" })
79
+ /* @__PURE__ */ jsx("span", { className: cn("hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block", KIND_TAG[m.kind]), children: kindLabel(m.kind) }),
80
+ /* @__PURE__ */ jsx("span", { className: "w-28 shrink-0 text-right text-sm", children: /* @__PURE__ */ jsx(PriceValue, { price: m.price, isLoading: m.isLoading }) })
97
81
  ] });
98
82
  }
99
83
  function CoinCardSkeleton() {
100
84
  return /* @__PURE__ */ jsxs("div", { className: "card-base flex flex-col", children: [
101
85
  /* @__PURE__ */ jsx("div", { className: "aspect-square w-full animate-pulse bg-muted" }),
102
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 p-3", children: [
86
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 p-3", children: [
103
87
  /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
104
88
  /* @__PURE__ */ jsx("div", { className: "h-4 w-24 rounded bg-muted-foreground/20 animate-pulse" }),
105
89
  /* @__PURE__ */ jsx("div", { className: "h-3 w-12 rounded bg-muted-foreground/20 animate-pulse" })
106
90
  ] }),
107
- /* @__PURE__ */ jsx("div", { className: "h-9 w-full rounded-lg bg-muted-foreground/10 animate-pulse" })
91
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-full rounded bg-muted-foreground/10 animate-pulse" })
108
92
  ] })
109
93
  ] });
110
94
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic, art-forward coin discovery tiles.\n *\n * Pure presentation: the price read (`usePrice`) and the link target (`href`)\n * are injected by the consuming app, and the collection is typed structurally\n * (CoinCollectionLike) — so a coin on Starknet, Ethereum, or Solana renders the\n * same with zero changes here. The coin's artwork is the hero; the only stat is\n * the live spot price (FDV / holders / a \"verified\" mark are intentionally not\n * shown — Medialane indexes none of them).\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport {\n coinKind, formatCoinPrice,\n type CoinCollectionLike, type CoinPriceLike,\n} from \"../data/coins.js\";\n\n/** Injected per-chain spot-price read. `price` is null while loading/unknown. */\nexport type UseCoinPrice = (collection: CoinCollectionLike) => {\n price: CoinPriceLike | null;\n isLoading: boolean;\n};\n\nexport interface CoinTileProps {\n collection: CoinCollectionLike;\n usePrice: UseCoinPrice;\n /** Link target — internal coin page or the per-chain trading app. */\n href: string;\n}\n\nconst KIND_TAG: Record<string, string> = {\n creator: \"border-brand-purple/30 bg-brand-purple/10 text-brand-purple\",\n memecoin: \"border-brand-rose/30 bg-brand-rose/10 text-brand-rose\",\n};\n\n// Aurora glow + art-fallback gradient, tinted by kind (foundations §III: rose\n// is the coin surface, purple/blue the creator accent).\nconst KIND_GLOW: Record<string, string> = {\n creator: \"bg-brand-purple/25\",\n memecoin: \"bg-brand-rose/25\",\n};\nconst KIND_FALLBACK: Record<string, string> = {\n creator: \"from-brand-blue to-brand-purple\",\n memecoin: \"from-brand-purple to-brand-rose\",\n};\n\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const logoUri = collection.profile?.image ?? collection.image;\n const logo = logoUri ? ipfsToHttp(logoUri) : null;\n const initials = (collection.symbol ?? collection.name ?? \"?\").trim().slice(0, 2).toUpperCase();\n const chain = (collection.chain ?? \"\").toString().toLowerCase();\n return { price, isLoading, kind, logo, initials, chain };\n}\n\nfunction PriceSkel() {\n return <span className=\"inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse\" />;\n}\n\nfunction KindChip({ kind, className }: { kind: string; className?: string }) {\n return (\n <span className={cn(\"shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium\", KIND_TAG[kind], className)}>\n {kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\n );\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <div className=\"group relative h-full\">\n {/* Aurora glow — soft, kind-tinted, blooms past the card edge. */}\n <div\n aria-hidden\n className={cn(\n \"pointer-events-none absolute -inset-2 rounded-[1.4rem] opacity-60 blur-xl transition-opacity duration-500 group-hover:opacity-100\",\n KIND_GLOW[m.kind]\n )}\n />\n <MotionCard className=\"card-base relative flex h-full flex-col\">\n <Link href={href} className=\"flex h-full flex-col\">\n {/* Hero artwork */}\n <div className=\"relative aspect-square w-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image\n src={m.logo}\n alt={collection.name ?? \"Coin\"}\n fill\n sizes=\"(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n unoptimized\n />\n ) : (\n <div className={cn(\"absolute inset-0 flex items-center justify-center bg-gradient-to-br\", KIND_FALLBACK[m.kind])}>\n <span className=\"select-none text-5xl font-black tracking-tighter text-white/90\">{m.initials}</span>\n </div>\n )}\n {m.chain && (\n <span className=\"absolute bottom-2 left-2 rounded-full bg-black/40 px-2 py-0.5 text-[9px] uppercase tracking-wide text-white/80 backdrop-blur-md\">\n {m.chain}\n </span>\n )}\n </div>\n\n {/* Body */}\n <div className=\"flex flex-1 flex-col gap-3 p-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <div className=\"truncate font-semibold leading-tight\">{collection.name ?? \"Untitled coin\"}</div>\n <div className=\"truncate text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</div>\n </div>\n <KindChip kind={m.kind} />\n </div>\n <div className=\"mt-auto space-y-2\">\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Price</span>\n {m.isLoading ? <PriceSkel /> : m.price ? (\n <span className=\"font-semibold tabular-nums\">{formatCoinPrice(m.price.quotePerCoin)}</span>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n </div>\n <span className=\"block rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple px-3 py-2 text-center text-sm font-semibold text-white\">\n Trade\n </span>\n </div>\n </div>\n </Link>\n </MotionCard>\n </div>\n );\n}\n\nexport function CoinRow({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40 active:scale-[0.99]\">\n <div className=\"relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted\">\n {m.logo ? <Image src={m.logo} alt=\"\" fill sizes=\"36px\" className=\"object-cover\" unoptimized /> :\n <div className={cn(\"flex h-full w-full items-center justify-center bg-gradient-to-br text-[11px] font-bold text-white\", KIND_FALLBACK[m.kind])}>{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <KindChip kind={m.kind} className=\"hidden sm:inline-block\" />\n <span className=\"w-24 shrink-0 text-right text-sm font-semibold tabular-nums\">\n {m.isLoading ? <span className=\"ml-auto inline-block h-4 w-12 rounded bg-muted-foreground/20 animate-pulse\" /> : m.price ? formatCoinPrice(m.price.quotePerCoin) : <span className=\"text-muted-foreground\">—</span>}\n </span>\n <span className=\"hidden shrink-0 text-sm font-medium text-primary sm:inline-block\">Trade</span>\n </Link>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"card-base flex flex-col\">\n <div className=\"aspect-square w-full animate-pulse bg-muted\" />\n <div className=\"flex flex-col gap-3 p-3\">\n <div className=\"space-y-1.5\">\n <div className=\"h-4 w-24 rounded bg-muted-foreground/20 animate-pulse\" />\n <div className=\"h-3 w-12 rounded bg-muted-foreground/20 animate-pulse\" />\n </div>\n <div className=\"h-9 w-full rounded-lg bg-muted-foreground/10 animate-pulse\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic, art-forward coin discovery tiles.\n *\n * Pure presentation: the price read (`usePrice`) and the link target (`href`)\n * are injected by the consuming app, and the collection is typed structurally\n * (CoinCollectionLike) — so a coin on Starknet, Ethereum, or Solana renders the\n * same with zero changes here. The coin's artwork is the hero; the whole tile\n * links to the coin page (where trading happens), and the only stat is the live\n * spot price with its quote-currency icon. FDV / holders / a \"verified\" mark are\n * intentionally not shown — Medialane indexes none of them.\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport {\n coinKind, formatCoinPrice,\n type CoinCollectionLike, type CoinPriceLike,\n} from \"../data/coins.js\";\n\n/** Injected per-chain spot-price read. `price` is null while loading/unknown. */\nexport type UseCoinPrice = (collection: CoinCollectionLike) => {\n price: CoinPriceLike | null;\n isLoading: boolean;\n};\n\nexport interface CoinTileProps {\n collection: CoinCollectionLike;\n usePrice: UseCoinPrice;\n /** Link target — internal coin page or the per-chain trading app. */\n href: string;\n}\n\n// Kind tag on a card surface (table row) — brand hues, never red.\nconst KIND_TAG: Record<string, string> = {\n creator: \"border-brand-blue/30 bg-brand-blue/10 text-brand-blue\",\n memecoin: \"border-brand-purple/30 bg-brand-purple/10 text-brand-purple\",\n};\n// Kind tag layered over artwork — solid brand pill, white text for legibility.\nconst KIND_OVER_IMAGE: Record<string, string> = {\n creator: \"bg-brand-blue/85 text-white\",\n memecoin: \"bg-brand-purple/85 text-white\",\n};\n// Art-fallback gradient (no logo) — vivid brand wash.\nconst KIND_FALLBACK: Record<string, string> = {\n creator: \"from-brand-blue to-brand-purple\",\n memecoin: \"from-brand-purple to-brand-rose\",\n};\n\nconst kindLabel = (kind: string) => (kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\");\n\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const logoUri = collection.profile?.image ?? collection.image;\n const logo = logoUri ? ipfsToHttp(logoUri) : null;\n const initials = (collection.symbol ?? collection.name ?? \"?\").trim().slice(0, 2).toUpperCase();\n return { price, isLoading, kind, logo, initials };\n}\n\nfunction PriceValue({ price, isLoading }: { price: CoinPriceLike | null; isLoading: boolean }) {\n if (isLoading) return <span className=\"inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse\" />;\n if (!price) return <span className=\"text-muted-foreground\">—</span>;\n return (\n <span className=\"inline-flex items-center gap-1 font-semibold tabular-nums\">\n <CurrencyIcon symbol={price.quoteSymbol} size={14} />\n {formatCoinPrice(price.quotePerCoin)}\n </span>\n );\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <MotionCard className=\"card-base group relative flex h-full flex-col\">\n <Link href={href} className=\"flex h-full flex-col\">\n {/* Hero artwork */}\n <div className=\"relative aspect-square w-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image\n src={m.logo}\n alt={collection.name ?? \"Coin\"}\n fill\n sizes=\"(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n unoptimized\n />\n ) : (\n <div className={cn(\"absolute inset-0 flex items-center justify-center bg-gradient-to-br\", KIND_FALLBACK[m.kind])}>\n <span className=\"select-none text-5xl font-black tracking-tighter text-white/90\">{m.initials}</span>\n </div>\n )}\n <span className={cn(\"absolute left-2 top-2 rounded-full px-2 py-0.5 text-[10px] font-semibold backdrop-blur-md\", KIND_OVER_IMAGE[m.kind])}>\n {kindLabel(m.kind)}\n </span>\n </div>\n\n {/* Body */}\n <div className=\"flex flex-col gap-2 p-3\">\n <div className=\"min-w-0\">\n <div className=\"truncate font-semibold leading-tight\">{collection.name ?? \"Untitled coin\"}</div>\n <div className=\"truncate text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</div>\n </div>\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Price</span>\n <PriceValue price={m.price} isLoading={m.isLoading} />\n </div>\n </div>\n </Link>\n </MotionCard>\n );\n}\n\nexport function CoinRow({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40 active:scale-[0.99]\">\n <div className=\"relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted\">\n {m.logo ? <Image src={m.logo} alt=\"\" fill sizes=\"36px\" className=\"object-cover\" unoptimized /> :\n <div className={cn(\"flex h-full w-full items-center justify-center bg-gradient-to-br text-[11px] font-bold text-white\", KIND_FALLBACK[m.kind])}>{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block\", KIND_TAG[m.kind])}>\n {kindLabel(m.kind)}\n </span>\n <span className=\"w-28 shrink-0 text-right text-sm\">\n <PriceValue price={m.price} isLoading={m.isLoading} />\n </span>\n </Link>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"card-base flex flex-col\">\n <div className=\"aspect-square w-full animate-pulse bg-muted\" />\n <div className=\"flex flex-col gap-2 p-3\">\n <div className=\"space-y-1.5\">\n <div className=\"h-4 w-24 rounded bg-muted-foreground/20 animate-pulse\" />\n <div className=\"h-3 w-12 rounded bg-muted-foreground/20 animate-pulse\" />\n </div>\n <div className=\"h-4 w-full rounded bg-muted-foreground/10 animate-pulse\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -36,11 +36,21 @@ const SORT_OPTIONS = [
36
36
  { label: "Recently launched", value: "recent" },
37
37
  { label: "Name", value: "name" }
38
38
  ];
39
+ const filterLabel = (v) => FILTER_TABS.find((t) => t.value === v)?.label ?? "";
40
+ const sortLabel = (v) => SORT_OPTIONS.find((o) => o.value === v)?.label ?? "";
39
41
  function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
40
42
  const [filter, setFilter] = (0, import_react.useState)("all");
41
43
  const [sort, setSort] = (0, import_react.useState)("recent");
42
44
  const [view, setView] = (0, import_react.useState)("grid");
43
45
  const [query, setQuery] = (0, import_react.useState)("");
46
+ const [filtersOpen, setFiltersOpen] = (0, import_react.useState)(false);
47
+ const filterCount = (filter !== "all" ? 1 : 0) + (sort !== "recent" ? 1 : 0);
48
+ (0, import_react.useEffect)(() => {
49
+ if (!filtersOpen) return;
50
+ const onKey = (e) => e.key === "Escape" && setFiltersOpen(false);
51
+ window.addEventListener("keydown", onKey);
52
+ return () => window.removeEventListener("keydown", onKey);
53
+ }, [filtersOpen]);
44
54
  const { collections, isLoading } = useCoins({ filter, sort });
45
55
  const items = (0, import_react.useMemo)(() => {
46
56
  const q = query.trim().toLowerCase();
@@ -55,59 +65,113 @@ function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
55
65
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Coins, { className: "h-5 w-5" }),
56
66
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "Tokens" })
57
67
  ] }),
58
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-3xl font-bold", children: "Creator coins & memecoins" }),
59
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-muted-foreground", children: "Discover creator-issued social tokens and claimed memecoins." })
68
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-3xl font-bold", children: "Creator coins & memecoins" })
60
69
  ] }),
61
70
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-3 border-b border-border/60 pb-3", children: [
62
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
63
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex gap-1.5", children: FILTER_TABS.map(({ label, value }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
64
- "button",
65
- {
66
- onClick: () => setFilter(value),
67
- className: (0, import_cn.cn)(
68
- "rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
69
- filter === value ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:border-primary/50 hover:text-foreground"
70
- ),
71
- children: label
72
- },
73
- value
74
- )) }),
75
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
71
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
72
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative flex-1", children: [
73
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Search, { className: "pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
76
74
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
77
- "select",
75
+ "input",
78
76
  {
79
- value: sort,
80
- onChange: (e) => setSort(e.target.value),
81
- className: "rounded-lg border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground",
82
- children: SORT_OPTIONS.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: o.value, children: o.label }, o.value))
77
+ value: query,
78
+ onChange: (e) => setQuery(e.target.value),
79
+ placeholder: "Search coins by name or symbol\u2026",
80
+ className: "w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50"
83
81
  }
84
- ),
85
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "inline-flex rounded-lg border border-border p-0.5", children: [{ v: "grid", Icon: import_lucide_react.LayoutGrid }, { v: "table", Icon: import_lucide_react.List }].map(({ v, Icon }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
82
+ )
83
+ ] }),
84
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
85
+ "button",
86
+ {
87
+ onClick: () => setFiltersOpen(true),
88
+ className: "inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-xs font-medium text-foreground hover:border-primary/50",
89
+ children: [
90
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.SlidersHorizontal, { className: "h-3.5 w-3.5" }),
91
+ "Filters",
92
+ filterCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground", children: filterCount })
93
+ ]
94
+ }
95
+ ),
96
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "inline-flex shrink-0 rounded-lg border border-border p-0.5", children: [{ v: "grid", Icon: import_lucide_react.LayoutGrid }, { v: "table", Icon: import_lucide_react.List }].map(({ v, Icon }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
97
+ "button",
98
+ {
99
+ onClick: () => setView(v),
100
+ "aria-label": v === "grid" ? "Grid view" : "Table view",
101
+ className: (0, import_cn.cn)("rounded-md p-1.5 transition-colors", view === v ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"),
102
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: "h-4 w-4" })
103
+ },
104
+ v
105
+ )) })
106
+ ] }),
107
+ filterCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-center gap-1.5", children: [
108
+ filter !== "all" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, { onClear: () => setFilter("all"), children: filterLabel(filter) }),
109
+ sort !== "recent" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, { onClear: () => setSort("recent"), children: sortLabel(sort) })
110
+ ] })
111
+ ] }),
112
+ isLoading && items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_coin_card.CoinCardSkeleton, {}, i)) }) : items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "rounded-xl border border-border/60 py-16 text-center text-muted-foreground", children: query.trim() ? `No coins match "${query.trim()}".` : "No coins yet." }) : view === "table" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "space-y-2", children: items.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_coin_card.CoinRow, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: items.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_coin_card.CoinCard, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }),
113
+ filtersOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", role: "dialog", "aria-modal": "true", "aria-label": "Filters", children: [
114
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute inset-0 bg-background/70 backdrop-blur-sm", onClick: () => setFiltersOpen(false) }),
115
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "card-base relative z-10 w-full max-w-sm space-y-5 p-5", children: [
116
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between", children: [
117
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h2", { className: "flex items-center gap-2 text-base font-bold", children: [
118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.SlidersHorizontal, { className: "h-4 w-4 text-primary" }),
119
+ "Filters"
120
+ ] }),
121
+ filterCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
86
122
  "button",
87
123
  {
88
- onClick: () => setView(v),
89
- "aria-label": v === "grid" ? "Grid view" : "Table view",
90
- className: (0, import_cn.cn)("rounded-md p-1.5 transition-colors", view === v ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"),
91
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: "h-4 w-4" })
92
- },
93
- v
94
- )) })
95
- ] })
96
- ] }),
97
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative", children: [
98
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Search, { className: "pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
99
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
100
- "input",
124
+ onClick: () => {
125
+ setFilter("all");
126
+ setSort("recent");
127
+ },
128
+ className: "text-xs font-medium text-muted-foreground hover:text-foreground",
129
+ children: "Clear all"
130
+ }
131
+ )
132
+ ] }),
133
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FilterGroup, { label: "Type", children: FILTER_TABS.map(({ label, value }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PillButton, { active: filter === value, onClick: () => setFilter(value), children: label }, value)) }),
134
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FilterGroup, { label: "Sort", children: SORT_OPTIONS.map(({ label, value }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PillButton, { active: sort === value, onClick: () => setSort(value), children: label }, value)) }),
135
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
136
+ "button",
101
137
  {
102
- value: query,
103
- onChange: (e) => setQuery(e.target.value),
104
- placeholder: "Search coins by name or symbol\u2026",
105
- className: "w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50"
138
+ onClick: () => setFiltersOpen(false),
139
+ className: "w-full rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple py-2.5 text-sm font-semibold text-white",
140
+ children: [
141
+ "Show ",
142
+ items.length,
143
+ " ",
144
+ items.length === 1 ? "coin" : "coins"
145
+ ]
106
146
  }
107
147
  )
108
148
  ] })
109
- ] }),
110
- isLoading && items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_coin_card.CoinCardSkeleton, {}, i)) }) : items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "rounded-xl border border-border/60 py-16 text-center text-muted-foreground", children: query.trim() ? `No coins match "${query.trim()}".` : "No coins yet." }) : view === "table" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "space-y-2", children: items.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_coin_card.CoinRow, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: items.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_coin_card.CoinCard, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) })
149
+ ] })
150
+ ] });
151
+ }
152
+ function FilterGroup({ label, children }) {
153
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-2", children: [
154
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-[10px] font-semibold uppercase tracking-wider text-muted-foreground", children: label }),
155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-1.5", children })
156
+ ] });
157
+ }
158
+ function PillButton({ active, onClick, children }) {
159
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
160
+ "button",
161
+ {
162
+ onClick,
163
+ className: (0, import_cn.cn)(
164
+ "rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
165
+ active ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:border-primary/50 hover:text-foreground"
166
+ ),
167
+ children
168
+ }
169
+ );
170
+ }
171
+ function Chip({ children, onClear }) {
172
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex items-center gap-1 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary", children: [
173
+ children,
174
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: onClear, "aria-label": "Clear filter", className: "hover:text-primary/60", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-3 w-3" }) })
111
175
  ] });
112
176
  }
113
177
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/coins-explorer.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinsExplorer — the shared coin-discovery surface (chain-agnostic).\n *\n * Owns view/search/filter/sort UI only. The data source (`useCoins`), the price\n * read (`usePrice`), and the link target (`coinHref`) are injected, so each app\n * (and each chain) wires its own without forking this component.\n */\n\nimport { useState, useMemo } from \"react\";\nimport { Coins, LayoutGrid, List, Search } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { CoinCard, CoinRow, CoinCardSkeleton, type UseCoinPrice } from \"./coin-card.js\";\nimport type { CoinCollectionLike } from \"../data/coins.js\";\n\nexport type CoinFilter = \"all\" | \"creator\" | \"memecoin\";\nexport type CoinSort = \"recent\" | \"name\";\nexport type UseCoins = (opts: { filter: CoinFilter; sort: CoinSort }) => {\n collections: CoinCollectionLike[];\n isLoading: boolean;\n};\n\nexport interface CoinsExplorerProps {\n useCoins: UseCoins;\n usePrice: UseCoinPrice;\n /** Build the link target for a coin (internal or per-chain trading app). */\n coinHref: (collection: CoinCollectionLike) => string;\n heading?: boolean;\n}\n\nconst FILTER_TABS: { label: string; value: CoinFilter }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Creator Coins\", value: \"creator\" },\n { label: \"Memecoins\", value: \"memecoin\" },\n];\n\n// Recency default — never raw swap volume (05 §11 anti-wash hygiene).\nconst SORT_OPTIONS: { label: string; value: CoinSort }[] = [\n { label: \"Recently launched\", value: \"recent\" },\n { label: \"Name\", value: \"name\" },\n];\n\nexport function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }: CoinsExplorerProps) {\n const [filter, setFilter] = useState<CoinFilter>(\"all\");\n const [sort, setSort] = useState<CoinSort>(\"recent\");\n const [view, setView] = useState<\"grid\" | \"table\">(\"grid\");\n const [query, setQuery] = useState(\"\");\n\n const { collections, isLoading } = useCoins({ filter, sort });\n const items = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return collections;\n return collections.filter(\n (c) => (c.name ?? \"\").toLowerCase().includes(q) || (c.symbol ?? \"\").toLowerCase().includes(q)\n );\n }, [collections, query]);\n\n return (\n <div className=\"space-y-6\">\n {heading && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Coins className=\"h-5 w-5\" />\n <span className=\"text-sm font-semibold uppercase tracking-wider\">Tokens</span>\n </div>\n <h1 className=\"text-3xl font-bold\">Creator coins &amp; memecoins</h1>\n <p className=\"text-muted-foreground\">Discover creator-issued social tokens and claimed memecoins.</p>\n </div>\n )}\n\n <div className=\"space-y-3 border-b border-border/60 pb-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex gap-1.5\">\n {FILTER_TABS.map(({ label, value }) => (\n <button\n key={value}\n onClick={() => setFilter(value)}\n className={cn(\n \"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors\",\n filter === value\n ? \"border-primary bg-primary/10 text-primary\"\n : \"border-border text-muted-foreground hover:border-primary/50 hover:text-foreground\"\n )}\n >\n {label}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={sort}\n onChange={(e) => setSort(e.target.value as CoinSort)}\n className=\"rounded-lg border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground\"\n >\n {SORT_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}\n </select>\n <div className=\"inline-flex rounded-lg border border-border p-0.5\">\n {([{ v: \"grid\", Icon: LayoutGrid }, { v: \"table\", Icon: List }] as const).map(({ v, Icon }) => (\n <button\n key={v}\n onClick={() => setView(v)}\n aria-label={v === \"grid\" ? \"Grid view\" : \"Table view\"}\n className={cn(\"rounded-md p-1.5 transition-colors\", view === v ? \"bg-primary/10 text-primary\" : \"text-muted-foreground hover:text-foreground\")}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n </div>\n </div>\n </div>\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search coins by name or symbol…\"\n className=\"w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50\"\n />\n </div>\n </div>\n\n {isLoading && items.length === 0 ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 8 }).map((_, i) => <CoinCardSkeleton key={i} />)}\n </div>\n ) : items.length === 0 ? (\n <div className=\"rounded-xl border border-border/60 py-16 text-center text-muted-foreground\">\n {query.trim() ? `No coins match \"${query.trim()}\".` : \"No coins yet.\"}\n </div>\n ) : view === \"table\" ? (\n <div className=\"space-y-2\">\n {items.map((c) => <CoinRow key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((c) => <CoinCard key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/coins-explorer.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinsExplorer — the shared coin-discovery surface (chain-agnostic).\n *\n * Owns view/search/filter/sort UI only. The data source (`useCoins`), the price\n * read (`usePrice`), and the link target (`coinHref`) are injected, so each app\n * (and each chain) wires its own without forking this component.\n *\n * Type + sort live in a centered Filters dialog (the \"action focus\" pattern, and\n * matching the marketplace page); the header stays to two lines.\n */\n\nimport { useState, useMemo, useEffect } from \"react\";\nimport { Coins, LayoutGrid, List, Search, SlidersHorizontal, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { CoinCard, CoinRow, CoinCardSkeleton, type UseCoinPrice } from \"./coin-card.js\";\nimport type { CoinCollectionLike } from \"../data/coins.js\";\n\nexport type CoinFilter = \"all\" | \"creator\" | \"memecoin\";\nexport type CoinSort = \"recent\" | \"name\";\nexport type UseCoins = (opts: { filter: CoinFilter; sort: CoinSort }) => {\n collections: CoinCollectionLike[];\n isLoading: boolean;\n};\n\nexport interface CoinsExplorerProps {\n useCoins: UseCoins;\n usePrice: UseCoinPrice;\n /** Build the link target for a coin (internal or per-chain trading app). */\n coinHref: (collection: CoinCollectionLike) => string;\n heading?: boolean;\n}\n\nconst FILTER_TABS: { label: string; value: CoinFilter }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Creator Coins\", value: \"creator\" },\n { label: \"Memecoins\", value: \"memecoin\" },\n];\n\n// Recency default — never raw swap volume (05 §11 anti-wash hygiene).\nconst SORT_OPTIONS: { label: string; value: CoinSort }[] = [\n { label: \"Recently launched\", value: \"recent\" },\n { label: \"Name\", value: \"name\" },\n];\n\nconst filterLabel = (v: CoinFilter) => FILTER_TABS.find((t) => t.value === v)?.label ?? \"\";\nconst sortLabel = (v: CoinSort) => SORT_OPTIONS.find((o) => o.value === v)?.label ?? \"\";\n\nexport function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }: CoinsExplorerProps) {\n const [filter, setFilter] = useState<CoinFilter>(\"all\");\n const [sort, setSort] = useState<CoinSort>(\"recent\");\n const [view, setView] = useState<\"grid\" | \"table\">(\"grid\");\n const [query, setQuery] = useState(\"\");\n const [filtersOpen, setFiltersOpen] = useState(false);\n\n const filterCount = (filter !== \"all\" ? 1 : 0) + (sort !== \"recent\" ? 1 : 0);\n\n useEffect(() => {\n if (!filtersOpen) return;\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setFiltersOpen(false);\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [filtersOpen]);\n\n const { collections, isLoading } = useCoins({ filter, sort });\n const items = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return collections;\n return collections.filter(\n (c) => (c.name ?? \"\").toLowerCase().includes(q) || (c.symbol ?? \"\").toLowerCase().includes(q)\n );\n }, [collections, query]);\n\n return (\n <div className=\"space-y-6\">\n {heading && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Coins className=\"h-5 w-5\" />\n <span className=\"text-sm font-semibold uppercase tracking-wider\">Tokens</span>\n </div>\n <h1 className=\"text-3xl font-bold\">Creator coins &amp; memecoins</h1>\n </div>\n )}\n\n {/* Controls — search + filters + view, kept lean. */}\n <div className=\"space-y-3 border-b border-border/60 pb-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search coins by name or symbol…\"\n className=\"w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50\"\n />\n </div>\n <button\n onClick={() => setFiltersOpen(true)}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-xs font-medium text-foreground hover:border-primary/50\"\n >\n <SlidersHorizontal className=\"h-3.5 w-3.5\" />\n Filters\n {filterCount > 0 && (\n <span className=\"ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground\">\n {filterCount}\n </span>\n )}\n </button>\n <div className=\"inline-flex shrink-0 rounded-lg border border-border p-0.5\">\n {([{ v: \"grid\", Icon: LayoutGrid }, { v: \"table\", Icon: List }] as const).map(({ v, Icon }) => (\n <button\n key={v}\n onClick={() => setView(v)}\n aria-label={v === \"grid\" ? \"Grid view\" : \"Table view\"}\n className={cn(\"rounded-md p-1.5 transition-colors\", view === v ? \"bg-primary/10 text-primary\" : \"text-muted-foreground hover:text-foreground\")}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n </div>\n </div>\n\n {/* Active-filter chips */}\n {filterCount > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {filter !== \"all\" && (\n <Chip onClear={() => setFilter(\"all\")}>{filterLabel(filter)}</Chip>\n )}\n {sort !== \"recent\" && (\n <Chip onClear={() => setSort(\"recent\")}>{sortLabel(sort)}</Chip>\n )}\n </div>\n )}\n </div>\n\n {isLoading && items.length === 0 ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 8 }).map((_, i) => <CoinCardSkeleton key={i} />)}\n </div>\n ) : items.length === 0 ? (\n <div className=\"rounded-xl border border-border/60 py-16 text-center text-muted-foreground\">\n {query.trim() ? `No coins match \"${query.trim()}\".` : \"No coins yet.\"}\n </div>\n ) : view === \"table\" ? (\n <div className=\"space-y-2\">\n {items.map((c) => <CoinRow key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((c) => <CoinCard key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n )}\n\n {filtersOpen && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Filters\">\n <div className=\"absolute inset-0 bg-background/70 backdrop-blur-sm\" onClick={() => setFiltersOpen(false)} />\n <div className=\"card-base relative z-10 w-full max-w-sm space-y-5 p-5\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"flex items-center gap-2 text-base font-bold\">\n <SlidersHorizontal className=\"h-4 w-4 text-primary\" />\n Filters\n </h2>\n {filterCount > 0 && (\n <button\n onClick={() => { setFilter(\"all\"); setSort(\"recent\"); }}\n className=\"text-xs font-medium text-muted-foreground hover:text-foreground\"\n >\n Clear all\n </button>\n )}\n </div>\n\n <FilterGroup label=\"Type\">\n {FILTER_TABS.map(({ label, value }) => (\n <PillButton key={value} active={filter === value} onClick={() => setFilter(value)}>{label}</PillButton>\n ))}\n </FilterGroup>\n\n <FilterGroup label=\"Sort\">\n {SORT_OPTIONS.map(({ label, value }) => (\n <PillButton key={value} active={sort === value} onClick={() => setSort(value)}>{label}</PillButton>\n ))}\n </FilterGroup>\n\n <button\n onClick={() => setFiltersOpen(false)}\n className=\"w-full rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple py-2.5 text-sm font-semibold text-white\"\n >\n Show {items.length} {items.length === 1 ? \"coin\" : \"coins\"}\n </button>\n </div>\n </div>\n )}\n </div>\n );\n}\n\nfunction FilterGroup({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"space-y-2\">\n <p className=\"text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\">{label}</p>\n <div className=\"flex flex-wrap gap-1.5\">{children}</div>\n </div>\n );\n}\n\nfunction PillButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {\n return (\n <button\n onClick={onClick}\n className={cn(\n \"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors\",\n active ? \"border-primary bg-primary/10 text-primary\" : \"border-border text-muted-foreground hover:border-primary/50 hover:text-foreground\"\n )}\n >\n {children}\n </button>\n );\n}\n\nfunction Chip({ children, onClear }: { children: React.ReactNode; onClear: () => void }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary\">\n {children}\n <button onClick={onClear} aria-label=\"Clear filter\" className=\"hover:text-primary/60\">\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useState, useMemo } from "react";
4
- import { Coins, LayoutGrid, List, Search } from "lucide-react";
3
+ import { useState, useMemo, useEffect } from "react";
4
+ import { Coins, LayoutGrid, List, Search, SlidersHorizontal, X } from "lucide-react";
5
5
  import { cn } from "../utils/cn.js";
6
6
  import { CoinCard, CoinRow, CoinCardSkeleton } from "./coin-card.js";
7
7
  const FILTER_TABS = [
@@ -13,11 +13,21 @@ const SORT_OPTIONS = [
13
13
  { label: "Recently launched", value: "recent" },
14
14
  { label: "Name", value: "name" }
15
15
  ];
16
+ const filterLabel = (v) => FILTER_TABS.find((t) => t.value === v)?.label ?? "";
17
+ const sortLabel = (v) => SORT_OPTIONS.find((o) => o.value === v)?.label ?? "";
16
18
  function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
17
19
  const [filter, setFilter] = useState("all");
18
20
  const [sort, setSort] = useState("recent");
19
21
  const [view, setView] = useState("grid");
20
22
  const [query, setQuery] = useState("");
23
+ const [filtersOpen, setFiltersOpen] = useState(false);
24
+ const filterCount = (filter !== "all" ? 1 : 0) + (sort !== "recent" ? 1 : 0);
25
+ useEffect(() => {
26
+ if (!filtersOpen) return;
27
+ const onKey = (e) => e.key === "Escape" && setFiltersOpen(false);
28
+ window.addEventListener("keydown", onKey);
29
+ return () => window.removeEventListener("keydown", onKey);
30
+ }, [filtersOpen]);
21
31
  const { collections, isLoading } = useCoins({ filter, sort });
22
32
  const items = useMemo(() => {
23
33
  const q = query.trim().toLowerCase();
@@ -32,59 +42,113 @@ function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
32
42
  /* @__PURE__ */ jsx(Coins, { className: "h-5 w-5" }),
33
43
  /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "Tokens" })
34
44
  ] }),
35
- /* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold", children: "Creator coins & memecoins" }),
36
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "Discover creator-issued social tokens and claimed memecoins." })
45
+ /* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold", children: "Creator coins & memecoins" })
37
46
  ] }),
38
47
  /* @__PURE__ */ jsxs("div", { className: "space-y-3 border-b border-border/60 pb-3", children: [
39
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
40
- /* @__PURE__ */ jsx("div", { className: "flex gap-1.5", children: FILTER_TABS.map(({ label, value }) => /* @__PURE__ */ jsx(
41
- "button",
42
- {
43
- onClick: () => setFilter(value),
44
- className: cn(
45
- "rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
46
- filter === value ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:border-primary/50 hover:text-foreground"
47
- ),
48
- children: label
49
- },
50
- value
51
- )) }),
52
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
48
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
49
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
50
+ /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
53
51
  /* @__PURE__ */ jsx(
54
- "select",
52
+ "input",
55
53
  {
56
- value: sort,
57
- onChange: (e) => setSort(e.target.value),
58
- className: "rounded-lg border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground",
59
- children: SORT_OPTIONS.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
54
+ value: query,
55
+ onChange: (e) => setQuery(e.target.value),
56
+ placeholder: "Search coins by name or symbol\u2026",
57
+ className: "w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50"
60
58
  }
61
- ),
62
- /* @__PURE__ */ jsx("div", { className: "inline-flex rounded-lg border border-border p-0.5", children: [{ v: "grid", Icon: LayoutGrid }, { v: "table", Icon: List }].map(({ v, Icon }) => /* @__PURE__ */ jsx(
59
+ )
60
+ ] }),
61
+ /* @__PURE__ */ jsxs(
62
+ "button",
63
+ {
64
+ onClick: () => setFiltersOpen(true),
65
+ className: "inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-xs font-medium text-foreground hover:border-primary/50",
66
+ children: [
67
+ /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }),
68
+ "Filters",
69
+ filterCount > 0 && /* @__PURE__ */ jsx("span", { className: "ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground", children: filterCount })
70
+ ]
71
+ }
72
+ ),
73
+ /* @__PURE__ */ jsx("div", { className: "inline-flex shrink-0 rounded-lg border border-border p-0.5", children: [{ v: "grid", Icon: LayoutGrid }, { v: "table", Icon: List }].map(({ v, Icon }) => /* @__PURE__ */ jsx(
74
+ "button",
75
+ {
76
+ onClick: () => setView(v),
77
+ "aria-label": v === "grid" ? "Grid view" : "Table view",
78
+ className: cn("rounded-md p-1.5 transition-colors", view === v ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"),
79
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
80
+ },
81
+ v
82
+ )) })
83
+ ] }),
84
+ filterCount > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
85
+ filter !== "all" && /* @__PURE__ */ jsx(Chip, { onClear: () => setFilter("all"), children: filterLabel(filter) }),
86
+ sort !== "recent" && /* @__PURE__ */ jsx(Chip, { onClear: () => setSort("recent"), children: sortLabel(sort) })
87
+ ] })
88
+ ] }),
89
+ isLoading && items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(CoinCardSkeleton, {}, i)) }) : items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-xl border border-border/60 py-16 text-center text-muted-foreground", children: query.trim() ? `No coins match "${query.trim()}".` : "No coins yet." }) : view === "table" ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: items.map((c) => /* @__PURE__ */ jsx(CoinRow, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: items.map((c) => /* @__PURE__ */ jsx(CoinCard, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }),
90
+ filtersOpen && /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", role: "dialog", "aria-modal": "true", "aria-label": "Filters", children: [
91
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-background/70 backdrop-blur-sm", onClick: () => setFiltersOpen(false) }),
92
+ /* @__PURE__ */ jsxs("div", { className: "card-base relative z-10 w-full max-w-sm space-y-5 p-5", children: [
93
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
94
+ /* @__PURE__ */ jsxs("h2", { className: "flex items-center gap-2 text-base font-bold", children: [
95
+ /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-4 w-4 text-primary" }),
96
+ "Filters"
97
+ ] }),
98
+ filterCount > 0 && /* @__PURE__ */ jsx(
63
99
  "button",
64
100
  {
65
- onClick: () => setView(v),
66
- "aria-label": v === "grid" ? "Grid view" : "Table view",
67
- className: cn("rounded-md p-1.5 transition-colors", view === v ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"),
68
- children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
69
- },
70
- v
71
- )) })
72
- ] })
73
- ] }),
74
- /* @__PURE__ */ jsxs("div", { className: "relative", children: [
75
- /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
76
- /* @__PURE__ */ jsx(
77
- "input",
101
+ onClick: () => {
102
+ setFilter("all");
103
+ setSort("recent");
104
+ },
105
+ className: "text-xs font-medium text-muted-foreground hover:text-foreground",
106
+ children: "Clear all"
107
+ }
108
+ )
109
+ ] }),
110
+ /* @__PURE__ */ jsx(FilterGroup, { label: "Type", children: FILTER_TABS.map(({ label, value }) => /* @__PURE__ */ jsx(PillButton, { active: filter === value, onClick: () => setFilter(value), children: label }, value)) }),
111
+ /* @__PURE__ */ jsx(FilterGroup, { label: "Sort", children: SORT_OPTIONS.map(({ label, value }) => /* @__PURE__ */ jsx(PillButton, { active: sort === value, onClick: () => setSort(value), children: label }, value)) }),
112
+ /* @__PURE__ */ jsxs(
113
+ "button",
78
114
  {
79
- value: query,
80
- onChange: (e) => setQuery(e.target.value),
81
- placeholder: "Search coins by name or symbol\u2026",
82
- className: "w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50"
115
+ onClick: () => setFiltersOpen(false),
116
+ className: "w-full rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple py-2.5 text-sm font-semibold text-white",
117
+ children: [
118
+ "Show ",
119
+ items.length,
120
+ " ",
121
+ items.length === 1 ? "coin" : "coins"
122
+ ]
83
123
  }
84
124
  )
85
125
  ] })
86
- ] }),
87
- isLoading && items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(CoinCardSkeleton, {}, i)) }) : items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-xl border border-border/60 py-16 text-center text-muted-foreground", children: query.trim() ? `No coins match "${query.trim()}".` : "No coins yet." }) : view === "table" ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: items.map((c) => /* @__PURE__ */ jsx(CoinRow, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: items.map((c) => /* @__PURE__ */ jsx(CoinCard, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) })
126
+ ] })
127
+ ] });
128
+ }
129
+ function FilterGroup({ label, children }) {
130
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
131
+ /* @__PURE__ */ jsx("p", { className: "text-[10px] font-semibold uppercase tracking-wider text-muted-foreground", children: label }),
132
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children })
133
+ ] });
134
+ }
135
+ function PillButton({ active, onClick, children }) {
136
+ return /* @__PURE__ */ jsx(
137
+ "button",
138
+ {
139
+ onClick,
140
+ className: cn(
141
+ "rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
142
+ active ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:border-primary/50 hover:text-foreground"
143
+ ),
144
+ children
145
+ }
146
+ );
147
+ }
148
+ function Chip({ children, onClear }) {
149
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary", children: [
150
+ children,
151
+ /* @__PURE__ */ jsx("button", { onClick: onClear, "aria-label": "Clear filter", className: "hover:text-primary/60", children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }) })
88
152
  ] });
89
153
  }
90
154
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/coins-explorer.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinsExplorer — the shared coin-discovery surface (chain-agnostic).\n *\n * Owns view/search/filter/sort UI only. The data source (`useCoins`), the price\n * read (`usePrice`), and the link target (`coinHref`) are injected, so each app\n * (and each chain) wires its own without forking this component.\n */\n\nimport { useState, useMemo } from \"react\";\nimport { Coins, LayoutGrid, List, Search } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { CoinCard, CoinRow, CoinCardSkeleton, type UseCoinPrice } from \"./coin-card.js\";\nimport type { CoinCollectionLike } from \"../data/coins.js\";\n\nexport type CoinFilter = \"all\" | \"creator\" | \"memecoin\";\nexport type CoinSort = \"recent\" | \"name\";\nexport type UseCoins = (opts: { filter: CoinFilter; sort: CoinSort }) => {\n collections: CoinCollectionLike[];\n isLoading: boolean;\n};\n\nexport interface CoinsExplorerProps {\n useCoins: UseCoins;\n usePrice: UseCoinPrice;\n /** Build the link target for a coin (internal or per-chain trading app). */\n coinHref: (collection: CoinCollectionLike) => string;\n heading?: boolean;\n}\n\nconst FILTER_TABS: { label: string; value: CoinFilter }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Creator Coins\", value: \"creator\" },\n { label: \"Memecoins\", value: \"memecoin\" },\n];\n\n// Recency default — never raw swap volume (05 §11 anti-wash hygiene).\nconst SORT_OPTIONS: { label: string; value: CoinSort }[] = [\n { label: \"Recently launched\", value: \"recent\" },\n { label: \"Name\", value: \"name\" },\n];\n\nexport function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }: CoinsExplorerProps) {\n const [filter, setFilter] = useState<CoinFilter>(\"all\");\n const [sort, setSort] = useState<CoinSort>(\"recent\");\n const [view, setView] = useState<\"grid\" | \"table\">(\"grid\");\n const [query, setQuery] = useState(\"\");\n\n const { collections, isLoading } = useCoins({ filter, sort });\n const items = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return collections;\n return collections.filter(\n (c) => (c.name ?? \"\").toLowerCase().includes(q) || (c.symbol ?? \"\").toLowerCase().includes(q)\n );\n }, [collections, query]);\n\n return (\n <div className=\"space-y-6\">\n {heading && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Coins className=\"h-5 w-5\" />\n <span className=\"text-sm font-semibold uppercase tracking-wider\">Tokens</span>\n </div>\n <h1 className=\"text-3xl font-bold\">Creator coins &amp; memecoins</h1>\n <p className=\"text-muted-foreground\">Discover creator-issued social tokens and claimed memecoins.</p>\n </div>\n )}\n\n <div className=\"space-y-3 border-b border-border/60 pb-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex gap-1.5\">\n {FILTER_TABS.map(({ label, value }) => (\n <button\n key={value}\n onClick={() => setFilter(value)}\n className={cn(\n \"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors\",\n filter === value\n ? \"border-primary bg-primary/10 text-primary\"\n : \"border-border text-muted-foreground hover:border-primary/50 hover:text-foreground\"\n )}\n >\n {label}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={sort}\n onChange={(e) => setSort(e.target.value as CoinSort)}\n className=\"rounded-lg border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground\"\n >\n {SORT_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}\n </select>\n <div className=\"inline-flex rounded-lg border border-border p-0.5\">\n {([{ v: \"grid\", Icon: LayoutGrid }, { v: \"table\", Icon: List }] as const).map(({ v, Icon }) => (\n <button\n key={v}\n onClick={() => setView(v)}\n aria-label={v === \"grid\" ? \"Grid view\" : \"Table view\"}\n className={cn(\"rounded-md p-1.5 transition-colors\", view === v ? \"bg-primary/10 text-primary\" : \"text-muted-foreground hover:text-foreground\")}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n </div>\n </div>\n </div>\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search coins by name or symbol…\"\n className=\"w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50\"\n />\n </div>\n </div>\n\n {isLoading && items.length === 0 ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 8 }).map((_, i) => <CoinCardSkeleton key={i} />)}\n </div>\n ) : items.length === 0 ? (\n <div className=\"rounded-xl border border-border/60 py-16 text-center text-muted-foreground\">\n {query.trim() ? `No coins match \"${query.trim()}\".` : \"No coins yet.\"}\n </div>\n ) : view === \"table\" ? (\n <div className=\"space-y-2\">\n {items.map((c) => <CoinRow key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((c) => <CoinCard key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/coins-explorer.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinsExplorer — the shared coin-discovery surface (chain-agnostic).\n *\n * Owns view/search/filter/sort UI only. The data source (`useCoins`), the price\n * read (`usePrice`), and the link target (`coinHref`) are injected, so each app\n * (and each chain) wires its own without forking this component.\n *\n * Type + sort live in a centered Filters dialog (the \"action focus\" pattern, and\n * matching the marketplace page); the header stays to two lines.\n */\n\nimport { useState, useMemo, useEffect } from \"react\";\nimport { Coins, LayoutGrid, List, Search, SlidersHorizontal, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { CoinCard, CoinRow, CoinCardSkeleton, type UseCoinPrice } from \"./coin-card.js\";\nimport type { CoinCollectionLike } from \"../data/coins.js\";\n\nexport type CoinFilter = \"all\" | \"creator\" | \"memecoin\";\nexport type CoinSort = \"recent\" | \"name\";\nexport type UseCoins = (opts: { filter: CoinFilter; sort: CoinSort }) => {\n collections: CoinCollectionLike[];\n isLoading: boolean;\n};\n\nexport interface CoinsExplorerProps {\n useCoins: UseCoins;\n usePrice: UseCoinPrice;\n /** Build the link target for a coin (internal or per-chain trading app). */\n coinHref: (collection: CoinCollectionLike) => string;\n heading?: boolean;\n}\n\nconst FILTER_TABS: { label: string; value: CoinFilter }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Creator Coins\", value: \"creator\" },\n { label: \"Memecoins\", value: \"memecoin\" },\n];\n\n// Recency default — never raw swap volume (05 §11 anti-wash hygiene).\nconst SORT_OPTIONS: { label: string; value: CoinSort }[] = [\n { label: \"Recently launched\", value: \"recent\" },\n { label: \"Name\", value: \"name\" },\n];\n\nconst filterLabel = (v: CoinFilter) => FILTER_TABS.find((t) => t.value === v)?.label ?? \"\";\nconst sortLabel = (v: CoinSort) => SORT_OPTIONS.find((o) => o.value === v)?.label ?? \"\";\n\nexport function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }: CoinsExplorerProps) {\n const [filter, setFilter] = useState<CoinFilter>(\"all\");\n const [sort, setSort] = useState<CoinSort>(\"recent\");\n const [view, setView] = useState<\"grid\" | \"table\">(\"grid\");\n const [query, setQuery] = useState(\"\");\n const [filtersOpen, setFiltersOpen] = useState(false);\n\n const filterCount = (filter !== \"all\" ? 1 : 0) + (sort !== \"recent\" ? 1 : 0);\n\n useEffect(() => {\n if (!filtersOpen) return;\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setFiltersOpen(false);\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [filtersOpen]);\n\n const { collections, isLoading } = useCoins({ filter, sort });\n const items = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return collections;\n return collections.filter(\n (c) => (c.name ?? \"\").toLowerCase().includes(q) || (c.symbol ?? \"\").toLowerCase().includes(q)\n );\n }, [collections, query]);\n\n return (\n <div className=\"space-y-6\">\n {heading && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Coins className=\"h-5 w-5\" />\n <span className=\"text-sm font-semibold uppercase tracking-wider\">Tokens</span>\n </div>\n <h1 className=\"text-3xl font-bold\">Creator coins &amp; memecoins</h1>\n </div>\n )}\n\n {/* Controls — search + filters + view, kept lean. */}\n <div className=\"space-y-3 border-b border-border/60 pb-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search coins by name or symbol…\"\n className=\"w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50\"\n />\n </div>\n <button\n onClick={() => setFiltersOpen(true)}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-xs font-medium text-foreground hover:border-primary/50\"\n >\n <SlidersHorizontal className=\"h-3.5 w-3.5\" />\n Filters\n {filterCount > 0 && (\n <span className=\"ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground\">\n {filterCount}\n </span>\n )}\n </button>\n <div className=\"inline-flex shrink-0 rounded-lg border border-border p-0.5\">\n {([{ v: \"grid\", Icon: LayoutGrid }, { v: \"table\", Icon: List }] as const).map(({ v, Icon }) => (\n <button\n key={v}\n onClick={() => setView(v)}\n aria-label={v === \"grid\" ? \"Grid view\" : \"Table view\"}\n className={cn(\"rounded-md p-1.5 transition-colors\", view === v ? \"bg-primary/10 text-primary\" : \"text-muted-foreground hover:text-foreground\")}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n </div>\n </div>\n\n {/* Active-filter chips */}\n {filterCount > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {filter !== \"all\" && (\n <Chip onClear={() => setFilter(\"all\")}>{filterLabel(filter)}</Chip>\n )}\n {sort !== \"recent\" && (\n <Chip onClear={() => setSort(\"recent\")}>{sortLabel(sort)}</Chip>\n )}\n </div>\n )}\n </div>\n\n {isLoading && items.length === 0 ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 8 }).map((_, i) => <CoinCardSkeleton key={i} />)}\n </div>\n ) : items.length === 0 ? (\n <div className=\"rounded-xl border border-border/60 py-16 text-center text-muted-foreground\">\n {query.trim() ? `No coins match \"${query.trim()}\".` : \"No coins yet.\"}\n </div>\n ) : view === \"table\" ? (\n <div className=\"space-y-2\">\n {items.map((c) => <CoinRow key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((c) => <CoinCard key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n )}\n\n {filtersOpen && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Filters\">\n <div className=\"absolute inset-0 bg-background/70 backdrop-blur-sm\" onClick={() => setFiltersOpen(false)} />\n <div className=\"card-base relative z-10 w-full max-w-sm space-y-5 p-5\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"flex items-center gap-2 text-base font-bold\">\n <SlidersHorizontal className=\"h-4 w-4 text-primary\" />\n Filters\n </h2>\n {filterCount > 0 && (\n <button\n onClick={() => { setFilter(\"all\"); setSort(\"recent\"); }}\n className=\"text-xs font-medium text-muted-foreground hover:text-foreground\"\n >\n Clear all\n </button>\n )}\n </div>\n\n <FilterGroup label=\"Type\">\n {FILTER_TABS.map(({ label, value }) => (\n <PillButton key={value} active={filter === value} onClick={() => setFilter(value)}>{label}</PillButton>\n ))}\n </FilterGroup>\n\n <FilterGroup label=\"Sort\">\n {SORT_OPTIONS.map(({ label, value }) => (\n <PillButton key={value} active={sort === value} onClick={() => setSort(value)}>{label}</PillButton>\n ))}\n </FilterGroup>\n\n <button\n onClick={() => setFiltersOpen(false)}\n className=\"w-full rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple py-2.5 text-sm font-semibold text-white\"\n >\n Show {items.length} {items.length === 1 ? \"coin\" : \"coins\"}\n </button>\n </div>\n </div>\n )}\n </div>\n );\n}\n\nfunction FilterGroup({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"space-y-2\">\n <p className=\"text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\">{label}</p>\n <div className=\"flex flex-wrap gap-1.5\">{children}</div>\n </div>\n );\n}\n\nfunction PillButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {\n return (\n <button\n onClick={onClick}\n className={cn(\n \"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors\",\n active ? \"border-primary bg-primary/10 text-primary\" : \"border-border text-muted-foreground hover:border-primary/50 hover:text-foreground\"\n )}\n >\n {children}\n </button>\n );\n}\n\nfunction Chip({ children, onClear }: { children: React.ReactNode; onClear: () => void }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary\">\n {children}\n <button onClick={onClear} aria-label=\"Clear filter\" className=\"hover:text-primary/60\">\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medialane/ui",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Shared UI components for Medialane apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",