@medialane/ui 0.13.1 → 0.15.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.
Files changed (36) hide show
  1. package/README.md +31 -5
  2. package/dist/components/coin-card.cjs +131 -0
  3. package/dist/components/coin-card.cjs.map +1 -0
  4. package/dist/components/coin-card.d.cts +19 -0
  5. package/dist/components/coin-card.d.ts +19 -0
  6. package/dist/components/coin-card.js +99 -0
  7. package/dist/components/coin-card.js.map +1 -0
  8. package/dist/components/coins-explorer.cjs +117 -0
  9. package/dist/components/coins-explorer.cjs.map +1 -0
  10. package/dist/components/coins-explorer.d.cts +23 -0
  11. package/dist/components/coins-explorer.d.ts +23 -0
  12. package/dist/components/coins-explorer.js +93 -0
  13. package/dist/components/coins-explorer.js.map +1 -0
  14. package/dist/components/ip-type-display.cjs +26 -1
  15. package/dist/components/ip-type-display.cjs.map +1 -1
  16. package/dist/components/ip-type-display.js +27 -2
  17. package/dist/components/ip-type-display.js.map +1 -1
  18. package/dist/data/coins.cjs +48 -0
  19. package/dist/data/coins.cjs.map +1 -0
  20. package/dist/data/coins.d.cts +33 -0
  21. package/dist/data/coins.d.ts +33 -0
  22. package/dist/data/coins.js +22 -0
  23. package/dist/data/coins.js.map +1 -0
  24. package/dist/data/ip-templates.cjs +13 -0
  25. package/dist/data/ip-templates.cjs.map +1 -1
  26. package/dist/data/ip-templates.d.cts +16 -1
  27. package/dist/data/ip-templates.d.ts +16 -1
  28. package/dist/data/ip-templates.js +12 -0
  29. package/dist/data/ip-templates.js.map +1 -1
  30. package/dist/index.cjs +19 -0
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +4 -1
  33. package/dist/index.d.ts +4 -1
  34. package/dist/index.js +19 -1
  35. package/dist/index.js.map +1 -1
  36. package/package.json +1 -1
package/README.md CHANGED
@@ -173,16 +173,37 @@ import {
173
173
 
174
174
  ---
175
175
 
176
- ### v0.4 Launchpad Services
176
+ ### Launchpad (single page-UI source since v0.8)
177
177
 
178
178
  ```ts
179
- import { LaunchpadServicesGrid, LAUNCHPAD_SERVICE_DEFINITIONS } from "@medialane/ui";
179
+ import { LaunchpadGroupedSections, LaunchpadStrip, LAUNCHPAD_SERVICE_DEFINITIONS, SERVICE_HUES } from "@medialane/ui";
180
180
  ```
181
181
 
182
182
  | Export | Description |
183
183
  |---|---|
184
- | `<LaunchpadServicesGrid services={LAUNCHPAD_SERVICE_DEFINITIONS} />` | Launchpad services grid with live/coming-soon badges |
185
- | `LAUNCHPAD_SERVICE_DEFINITIONS` | Pre-built service definitions for all launchpad products |
184
+ | `<LaunchpadGroupedSections overrides={...} />` | The full grouped launchpad page UI — apps inject only hrefs / per-app rollout flips |
185
+ | `<LaunchpadStrip hrefs={...} />` | Homepage launchpad carousel cards derive from the shared service definitions |
186
+ | `LAUNCHPAD_SERVICE_DEFINITIONS` / `SERVICE_HUES` | Canonical service copy (titles, blurbs, examples) + one unique hue per service |
187
+
188
+ ### Asset page modules (v0.13+)
189
+
190
+ ```ts
191
+ import { AssetOverviewContent, AssetMarketsTab, AssetMediaColumn, AssetHeaderBlock, ParentAttributionBanner, IPTypeDisplay } from "@medialane/ui";
192
+ ```
193
+
194
+ Shared presentation modules for the asset detail pages — both apps re-export
195
+ them as shims at their original paths and inject wallet hooks/dialogs locally.
196
+
197
+ ### IP data layer (v0.13+)
198
+
199
+ ```ts
200
+ import { IP_TYPES, LICENSE_TYPES, IP_TEMPLATES, DOC_UPLOAD, TEMPLATE_TRAIT_TYPES } from "@medialane/ui";
201
+ ```
202
+
203
+ Canonical IP types, license presets, and per-type templates (embeds, socials,
204
+ trait suggestions, and the `docUpload` config powering the document/PDF-to-IPFS
205
+ upload on Documents / Patents / Publications / Software). The apps' `types/ip`
206
+ and `lib/ip-templates` are re-export shims of this layer — edit here, never there.
186
207
 
187
208
  ---
188
209
 
@@ -212,7 +233,12 @@ The package uses [tsup](https://tsup.egoist.dev/) and outputs ESM + CJS + type d
212
233
 
213
234
  | Version | Added |
214
235
  |---|---|
215
- | **v0.4.0** | `LaunchpadServicesGrid`, `LAUNCHPAD_SERVICE_DEFINITIONS` |
236
+ | **v0.14.0** | `docUpload` template config + `DOC_UPLOAD` (document/PDF → IPFS for Documents/Patents/Publications/Software), `IPTypeDisplay` document card |
237
+ | **v0.13.x** | Asset-page modules lifted: `AssetOverviewContent`, `AssetMarketsTab`, `AssetMediaColumn`/`AssetHeaderBlock`, `ParentAttributionBanner`, `IPTypeDisplay`; IP data layer (`data/ip`, `data/ip-templates`); `timeUntil` |
238
+ | **v0.12.x** | `LaunchpadStrip` (homepage carousel from service defs); Discover strips restyled to the approved design; `CollectionCard` gated-content border + currency floor |
239
+ | **v0.11.x** | `DiscoverFeedSection` rebuilt as carousels; `ActivityCard`; coins live in shared service defaults |
240
+ | **v0.8–0.10** | `LaunchpadGroupedSections` + creator-first card redesign (chips, examples, gradient section titles); `PageContainer`, `NavCommandMenu`, `PortfolioSubnav` |
241
+ | **v0.4.0** | `LaunchpadServicesGrid` (removed in v0.8), `LAUNCHPAD_SERVICE_DEFINITIONS` |
216
242
  | **v0.3.2** | `DiscoverHero`, `FeaturedCarousel`, `DiscoverCollectionsStrip`, `DiscoverCreatorsStrip`, `DiscoverFeedSection` |
217
243
  | **v0.3.0** | `ActivityRow`, `ActivityFeedShell`, `ActivityTicker`, `HeroSlider`, `ListingCard`, `LaunchpadGrid`, `CtaCardGrid`, `timeAgo`, `ACTIVITY_TYPE_CONFIG` |
218
244
  | **v0.2.0** | `MotionCard`, `FadeIn`, `Stagger`, `StaggerItem`, `KineticWords`, `ScrollSection`, `ShareButton`, `CollectionCard`, `TokenCard` |
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+ var coin_card_exports = {};
31
+ __export(coin_card_exports, {
32
+ CoinCard: () => CoinCard,
33
+ CoinCardSkeleton: () => CoinCardSkeleton,
34
+ CoinRow: () => CoinRow
35
+ });
36
+ module.exports = __toCommonJS(coin_card_exports);
37
+ var import_jsx_runtime = require("react/jsx-runtime");
38
+ var import_link = __toESM(require("next/link"), 1);
39
+ var import_image = __toESM(require("next/image"), 1);
40
+ var import_lucide_react = require("lucide-react");
41
+ var import_cn = require("../utils/cn.js");
42
+ var import_ipfs = require("../utils/ipfs.js");
43
+ var import_coins = require("../data/coins.js");
44
+ 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"
47
+ };
48
+ function useTileModel({ collection, usePrice }) {
49
+ const { price, isLoading } = usePrice(collection);
50
+ const kind = (0, import_coins.coinKind)(collection.service);
51
+ const verified = collection.claimedBy != null || kind === "creator";
52
+ const logoUri = collection.profile?.image ?? collection.image;
53
+ const logo = logoUri ? (0, import_ipfs.ipfsToHttp)(logoUri) : null;
54
+ const initials = (collection.symbol ?? collection.name ?? "?").trim().slice(0, 2).toUpperCase();
55
+ const fdv = (0, import_coins.formatFdv)(price?.quotePerCoin, collection.totalSupply, price?.quoteSymbol ?? null);
56
+ const chain = (collection.chain ?? "").toString().toLowerCase();
57
+ return { price, isLoading, kind, verified, logo, initials, fdv, chain };
58
+ }
59
+ function PriceSkel({ wide }) {
60
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("inline-block h-4 rounded bg-muted-foreground/20 animate-pulse", wide ? "w-16" : "w-12") });
61
+ }
62
+ function CoinCard({ collection, usePrice, href }) {
63
+ const m = useTileModel({ collection, usePrice });
64
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href, className: "flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden transition-transform active:scale-[0.99]", children: [
65
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-3 p-4", children: [
66
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative h-12 w-12 shrink-0 rounded-full overflow-hidden bg-muted", children: m.logo ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_image.default, { src: m.logo, alt: "", fill: true, sizes: "48px", className: "object-cover", unoptimized: true }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-sm font-bold text-white", children: m.initials }) }),
67
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0 flex-1", children: [
68
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-1", children: [
69
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "truncate font-semibold", children: collection.name ?? "Untitled coin" }),
70
+ m.verified && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.BadgeCheck, { className: "h-4 w-4 shrink-0 text-primary", "aria-label": "Verified" })
71
+ ] }),
72
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm text-muted-foreground", children: collection.symbol ?? "\u2014" })
73
+ ] }),
74
+ /* @__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[m.kind]), children: m.kind === "creator" ? "Creator Coin" : "Memecoin" })
75
+ ] }),
76
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3 text-sm", children: [
77
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Stat, { label: "Price", children: m.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceSkel, {}) : m.price ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-semibold", children: (0, import_coins.formatCoinPrice)(m.price.quotePerCoin) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground", children: "\u2014" }) }),
78
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Stat, { label: "FDV", children: m.isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceSkel, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-semibold", children: m.fdv ?? "\u2014" }) }),
79
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Stat, { label: "Holders", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex items-center gap-1 font-semibold", children: [
80
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Users, { className: "h-3 w-3 text-muted-foreground" }),
81
+ collection.holderCount || "\u2014"
82
+ ] }) })
83
+ ] }),
84
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between border-t border-border/60 px-4 py-2.5 text-sm", children: [
85
+ m.chain ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: m.chain }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
86
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-medium text-primary", children: "Trade" })
87
+ ] })
88
+ ] });
89
+ }
90
+ function CoinRow({ collection, usePrice, href }) {
91
+ const m = useTileModel({ collection, usePrice });
92
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { 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", children: [
93
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted", children: m.logo ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_image.default, { src: m.logo, alt: "", fill: true, sizes: "36px", className: "object-cover", unoptimized: true }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-[11px] font-bold text-white", children: m.initials }) }),
94
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0 flex-1", children: [
95
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-1", children: [
96
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "truncate text-sm font-semibold", children: collection.name ?? "Untitled coin" }),
97
+ m.verified && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.BadgeCheck, { className: "h-3.5 w-3.5 shrink-0 text-primary", "aria-label": "Verified" })
98
+ ] }),
99
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-muted-foreground", children: collection.symbol ?? "\u2014" })
100
+ ] }),
101
+ /* @__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: m.kind === "creator" ? "Creator Coin" : "Memecoin" }),
102
+ /* @__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" }) }),
103
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "hidden shrink-0 text-sm font-medium text-primary sm:inline-block", children: "Trade" })
104
+ ] });
105
+ }
106
+ function Stat({ label, children }) {
107
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-0.5", children: [
108
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: label }),
109
+ children
110
+ ] });
111
+ }
112
+ function CoinCardSkeleton() {
113
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden", children: [
114
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-3 p-4", children: [
115
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-12 w-12 rounded-full bg-muted animate-pulse" }),
116
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex-1 space-y-1.5", children: [
117
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-4 w-24 rounded bg-muted-foreground/20 animate-pulse" }),
118
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-3 w-12 rounded bg-muted-foreground/20 animate-pulse" })
119
+ ] })
120
+ ] }),
121
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3", children: [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-8 w-full rounded bg-muted-foreground/10 animate-pulse" }, i)) }),
122
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-9 w-full bg-muted-foreground/10 animate-pulse" })
123
+ ] });
124
+ }
125
+ // Annotate the CommonJS export names for ESM import in node:
126
+ 0 && (module.exports = {
127
+ CoinCard,
128
+ CoinCardSkeleton,
129
+ CoinRow
130
+ });
131
+ //# sourceMappingURL=coin-card.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic 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. `chain` is shown as a badge.\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { BadgeCheck, Users } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport {\n coinKind, formatCoinPrice, formatFdv,\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\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const verified = collection.claimedBy != null || kind === \"creator\";\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 fdv = formatFdv(price?.quotePerCoin, collection.totalSupply, price?.quoteSymbol ?? null);\n const chain = (collection.chain ?? \"\").toString().toLowerCase();\n return { price, isLoading, kind, verified, logo, initials, fdv, chain };\n}\n\nfunction PriceSkel({ wide }: { wide?: boolean }) {\n return <span className={cn(\"inline-block h-4 rounded bg-muted-foreground/20 animate-pulse\", wide ? \"w-16\" : \"w-12\")} />;\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden transition-transform active:scale-[0.99]\">\n <div className=\"flex items-center gap-3 p-4\">\n <div className=\"relative h-12 w-12 shrink-0 rounded-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image src={m.logo} alt=\"\" fill sizes=\"48px\" className=\"object-cover\" unoptimized />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-sm font-bold text-white\">{m.initials}</div>\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"truncate font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n {m.verified && <BadgeCheck className=\"h-4 w-4 shrink-0 text-primary\" aria-label=\"Verified\" />}\n </div>\n <span className=\"text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium\", KIND_TAG[m.kind])}>\n {m.kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\n </div>\n <div className=\"grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3 text-sm\">\n <Stat label=\"Price\">\n {m.isLoading ? <PriceSkel /> : m.price ? <span className=\"font-semibold\">{formatCoinPrice(m.price.quotePerCoin)}</span> : <span className=\"text-muted-foreground\">—</span>}\n </Stat>\n <Stat label=\"FDV\">\n {m.isLoading ? <PriceSkel /> : <span className=\"font-semibold\">{m.fdv ?? \"—\"}</span>}\n </Stat>\n <Stat label=\"Holders\">\n <span className=\"inline-flex items-center gap-1 font-semibold\">\n <Users className=\"h-3 w-3 text-muted-foreground\" />{collection.holderCount || \"—\"}\n </span>\n </Stat>\n </div>\n <div className=\"flex items-center justify-between border-t border-border/60 px-4 py-2.5 text-sm\">\n {m.chain ? <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">{m.chain}</span> : <span />}\n <span className=\"font-medium text-primary\">Trade</span>\n </div>\n </Link>\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\">\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=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-[11px] font-bold text-white\">{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n {m.verified && <BadgeCheck className=\"h-3.5 w-3.5 shrink-0 text-primary\" aria-label=\"Verified\" />}\n </div>\n <span className=\"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 {m.kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\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\nfunction Stat({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-0.5\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">{label}</span>\n {children}\n </div>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden\">\n <div className=\"flex items-center gap-3 p-4\">\n <div className=\"h-12 w-12 rounded-full bg-muted animate-pulse\" />\n <div className=\"flex-1 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>\n <div className=\"grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3\">\n {[0, 1, 2].map((i) => <div key={i} className=\"h-8 w-full rounded bg-muted-foreground/10 animate-pulse\" />)}\n </div>\n <div className=\"h-9 w-full bg-muted-foreground/10 animate-pulse\" />\n </div>\n );\n}\n"],"mappings":"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","names":["Link","Image"]}
@@ -0,0 +1,19 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { CoinCollectionLike, CoinPriceLike } from '../data/coins.cjs';
3
+
4
+ /** Injected per-chain spot-price read. `price` is null while loading/unknown. */
5
+ type UseCoinPrice = (collection: CoinCollectionLike) => {
6
+ price: CoinPriceLike | null;
7
+ isLoading: boolean;
8
+ };
9
+ interface CoinTileProps {
10
+ collection: CoinCollectionLike;
11
+ usePrice: UseCoinPrice;
12
+ /** Link target — internal coin page or the per-chain trading app. */
13
+ href: string;
14
+ }
15
+ declare function CoinCard({ collection, usePrice, href }: CoinTileProps): react_jsx_runtime.JSX.Element;
16
+ declare function CoinRow({ collection, usePrice, href }: CoinTileProps): react_jsx_runtime.JSX.Element;
17
+ declare function CoinCardSkeleton(): react_jsx_runtime.JSX.Element;
18
+
19
+ export { CoinCard, CoinCardSkeleton, CoinRow, type CoinTileProps, type UseCoinPrice };
@@ -0,0 +1,19 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { CoinCollectionLike, CoinPriceLike } from '../data/coins.js';
3
+
4
+ /** Injected per-chain spot-price read. `price` is null while loading/unknown. */
5
+ type UseCoinPrice = (collection: CoinCollectionLike) => {
6
+ price: CoinPriceLike | null;
7
+ isLoading: boolean;
8
+ };
9
+ interface CoinTileProps {
10
+ collection: CoinCollectionLike;
11
+ usePrice: UseCoinPrice;
12
+ /** Link target — internal coin page or the per-chain trading app. */
13
+ href: string;
14
+ }
15
+ declare function CoinCard({ collection, usePrice, href }: CoinTileProps): react_jsx_runtime.JSX.Element;
16
+ declare function CoinRow({ collection, usePrice, href }: CoinTileProps): react_jsx_runtime.JSX.Element;
17
+ declare function CoinCardSkeleton(): react_jsx_runtime.JSX.Element;
18
+
19
+ export { CoinCard, CoinCardSkeleton, CoinRow, type CoinTileProps, type UseCoinPrice };
@@ -0,0 +1,99 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import Link from "next/link";
4
+ import Image from "next/image";
5
+ import { BadgeCheck, Users } from "lucide-react";
6
+ import { cn } from "../utils/cn.js";
7
+ import { ipfsToHttp } from "../utils/ipfs.js";
8
+ import {
9
+ coinKind,
10
+ formatCoinPrice,
11
+ formatFdv
12
+ } from "../data/coins.js";
13
+ const KIND_TAG = {
14
+ creator: "border-brand-purple/30 bg-brand-purple/10 text-brand-purple",
15
+ memecoin: "border-brand-rose/30 bg-brand-rose/10 text-brand-rose"
16
+ };
17
+ function useTileModel({ collection, usePrice }) {
18
+ const { price, isLoading } = usePrice(collection);
19
+ const kind = coinKind(collection.service);
20
+ const verified = collection.claimedBy != null || kind === "creator";
21
+ const logoUri = collection.profile?.image ?? collection.image;
22
+ const logo = logoUri ? ipfsToHttp(logoUri) : null;
23
+ const initials = (collection.symbol ?? collection.name ?? "?").trim().slice(0, 2).toUpperCase();
24
+ const fdv = formatFdv(price?.quotePerCoin, collection.totalSupply, price?.quoteSymbol ?? null);
25
+ const chain = (collection.chain ?? "").toString().toLowerCase();
26
+ return { price, isLoading, kind, verified, logo, initials, fdv, chain };
27
+ }
28
+ function PriceSkel({ wide }) {
29
+ return /* @__PURE__ */ jsx("span", { className: cn("inline-block h-4 rounded bg-muted-foreground/20 animate-pulse", wide ? "w-16" : "w-12") });
30
+ }
31
+ function CoinCard({ collection, usePrice, href }) {
32
+ const m = useTileModel({ collection, usePrice });
33
+ return /* @__PURE__ */ jsxs(Link, { href, className: "flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden transition-transform active:scale-[0.99]", children: [
34
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-4", children: [
35
+ /* @__PURE__ */ jsx("div", { className: "relative h-12 w-12 shrink-0 rounded-full overflow-hidden bg-muted", children: m.logo ? /* @__PURE__ */ jsx(Image, { src: m.logo, alt: "", fill: true, sizes: "48px", className: "object-cover", unoptimized: true }) : /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-sm font-bold text-white", children: m.initials }) }),
36
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
37
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
38
+ /* @__PURE__ */ jsx("span", { className: "truncate font-semibold", children: collection.name ?? "Untitled coin" }),
39
+ m.verified && /* @__PURE__ */ jsx(BadgeCheck, { className: "h-4 w-4 shrink-0 text-primary", "aria-label": "Verified" })
40
+ ] }),
41
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: collection.symbol ?? "\u2014" })
42
+ ] }),
43
+ /* @__PURE__ */ jsx("span", { className: cn("shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium", KIND_TAG[m.kind]), children: m.kind === "creator" ? "Creator Coin" : "Memecoin" })
44
+ ] }),
45
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3 text-sm", children: [
46
+ /* @__PURE__ */ jsx(Stat, { label: "Price", children: m.isLoading ? /* @__PURE__ */ jsx(PriceSkel, {}) : m.price ? /* @__PURE__ */ jsx("span", { className: "font-semibold", children: formatCoinPrice(m.price.quotePerCoin) }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }),
47
+ /* @__PURE__ */ jsx(Stat, { label: "FDV", children: m.isLoading ? /* @__PURE__ */ jsx(PriceSkel, {}) : /* @__PURE__ */ jsx("span", { className: "font-semibold", children: m.fdv ?? "\u2014" }) }),
48
+ /* @__PURE__ */ jsx(Stat, { label: "Holders", children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 font-semibold", children: [
49
+ /* @__PURE__ */ jsx(Users, { className: "h-3 w-3 text-muted-foreground" }),
50
+ collection.holderCount || "\u2014"
51
+ ] }) })
52
+ ] }),
53
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t border-border/60 px-4 py-2.5 text-sm", children: [
54
+ m.chain ? /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: m.chain }) : /* @__PURE__ */ jsx("span", {}),
55
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-primary", children: "Trade" })
56
+ ] })
57
+ ] });
58
+ }
59
+ function CoinRow({ collection, usePrice, href }) {
60
+ const m = useTileModel({ collection, usePrice });
61
+ return /* @__PURE__ */ jsxs(Link, { 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", children: [
62
+ /* @__PURE__ */ jsx("div", { className: "relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted", children: m.logo ? /* @__PURE__ */ jsx(Image, { src: m.logo, alt: "", fill: true, sizes: "36px", className: "object-cover", unoptimized: true }) : /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-[11px] font-bold text-white", children: m.initials }) }),
63
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
64
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
65
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold", children: collection.name ?? "Untitled coin" }),
66
+ m.verified && /* @__PURE__ */ jsx(BadgeCheck, { className: "h-3.5 w-3.5 shrink-0 text-primary", "aria-label": "Verified" })
67
+ ] }),
68
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: collection.symbol ?? "\u2014" })
69
+ ] }),
70
+ /* @__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: m.kind === "creator" ? "Creator Coin" : "Memecoin" }),
71
+ /* @__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" }) }),
72
+ /* @__PURE__ */ jsx("span", { className: "hidden shrink-0 text-sm font-medium text-primary sm:inline-block", children: "Trade" })
73
+ ] });
74
+ }
75
+ function Stat({ label, children }) {
76
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
77
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: label }),
78
+ children
79
+ ] });
80
+ }
81
+ function CoinCardSkeleton() {
82
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden", children: [
83
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-4", children: [
84
+ /* @__PURE__ */ jsx("div", { className: "h-12 w-12 rounded-full bg-muted animate-pulse" }),
85
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-1.5", children: [
86
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-24 rounded bg-muted-foreground/20 animate-pulse" }),
87
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-12 rounded bg-muted-foreground/20 animate-pulse" })
88
+ ] })
89
+ ] }),
90
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx("div", { className: "h-8 w-full rounded bg-muted-foreground/10 animate-pulse" }, i)) }),
91
+ /* @__PURE__ */ jsx("div", { className: "h-9 w-full bg-muted-foreground/10 animate-pulse" })
92
+ ] });
93
+ }
94
+ export {
95
+ CoinCard,
96
+ CoinCardSkeleton,
97
+ CoinRow
98
+ };
99
+ //# sourceMappingURL=coin-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic 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. `chain` is shown as a badge.\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { BadgeCheck, Users } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport {\n coinKind, formatCoinPrice, formatFdv,\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\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const verified = collection.claimedBy != null || kind === \"creator\";\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 fdv = formatFdv(price?.quotePerCoin, collection.totalSupply, price?.quoteSymbol ?? null);\n const chain = (collection.chain ?? \"\").toString().toLowerCase();\n return { price, isLoading, kind, verified, logo, initials, fdv, chain };\n}\n\nfunction PriceSkel({ wide }: { wide?: boolean }) {\n return <span className={cn(\"inline-block h-4 rounded bg-muted-foreground/20 animate-pulse\", wide ? \"w-16\" : \"w-12\")} />;\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden transition-transform active:scale-[0.99]\">\n <div className=\"flex items-center gap-3 p-4\">\n <div className=\"relative h-12 w-12 shrink-0 rounded-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image src={m.logo} alt=\"\" fill sizes=\"48px\" className=\"object-cover\" unoptimized />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-sm font-bold text-white\">{m.initials}</div>\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"truncate font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n {m.verified && <BadgeCheck className=\"h-4 w-4 shrink-0 text-primary\" aria-label=\"Verified\" />}\n </div>\n <span className=\"text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium\", KIND_TAG[m.kind])}>\n {m.kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\n </div>\n <div className=\"grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3 text-sm\">\n <Stat label=\"Price\">\n {m.isLoading ? <PriceSkel /> : m.price ? <span className=\"font-semibold\">{formatCoinPrice(m.price.quotePerCoin)}</span> : <span className=\"text-muted-foreground\">—</span>}\n </Stat>\n <Stat label=\"FDV\">\n {m.isLoading ? <PriceSkel /> : <span className=\"font-semibold\">{m.fdv ?? \"—\"}</span>}\n </Stat>\n <Stat label=\"Holders\">\n <span className=\"inline-flex items-center gap-1 font-semibold\">\n <Users className=\"h-3 w-3 text-muted-foreground\" />{collection.holderCount || \"—\"}\n </span>\n </Stat>\n </div>\n <div className=\"flex items-center justify-between border-t border-border/60 px-4 py-2.5 text-sm\">\n {m.chain ? <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">{m.chain}</span> : <span />}\n <span className=\"font-medium text-primary\">Trade</span>\n </div>\n </Link>\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\">\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=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-[11px] font-bold text-white\">{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n {m.verified && <BadgeCheck className=\"h-3.5 w-3.5 shrink-0 text-primary\" aria-label=\"Verified\" />}\n </div>\n <span className=\"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 {m.kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\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\nfunction Stat({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-0.5\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">{label}</span>\n {children}\n </div>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden\">\n <div className=\"flex items-center gap-3 p-4\">\n <div className=\"h-12 w-12 rounded-full bg-muted animate-pulse\" />\n <div className=\"flex-1 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>\n <div className=\"grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3\">\n {[0, 1, 2].map((i) => <div key={i} className=\"h-8 w-full rounded bg-muted-foreground/10 animate-pulse\" />)}\n </div>\n <div className=\"h-9 w-full bg-muted-foreground/10 animate-pulse\" />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var coins_explorer_exports = {};
21
+ __export(coins_explorer_exports, {
22
+ CoinsExplorer: () => CoinsExplorer
23
+ });
24
+ module.exports = __toCommonJS(coins_explorer_exports);
25
+ var import_jsx_runtime = require("react/jsx-runtime");
26
+ var import_react = require("react");
27
+ var import_lucide_react = require("lucide-react");
28
+ var import_cn = require("../utils/cn.js");
29
+ var import_coin_card = require("./coin-card.js");
30
+ const FILTER_TABS = [
31
+ { label: "All", value: "all" },
32
+ { label: "Creator Coins", value: "creator" },
33
+ { label: "Memecoins", value: "memecoin" }
34
+ ];
35
+ const SORT_OPTIONS = [
36
+ { label: "Recently launched", value: "recent" },
37
+ { label: "Name", value: "name" }
38
+ ];
39
+ function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
40
+ const [filter, setFilter] = (0, import_react.useState)("all");
41
+ const [sort, setSort] = (0, import_react.useState)("recent");
42
+ const [view, setView] = (0, import_react.useState)("grid");
43
+ const [query, setQuery] = (0, import_react.useState)("");
44
+ const { collections, isLoading } = useCoins({ filter, sort });
45
+ const items = (0, import_react.useMemo)(() => {
46
+ const q = query.trim().toLowerCase();
47
+ if (!q) return collections;
48
+ return collections.filter(
49
+ (c) => (c.name ?? "").toLowerCase().includes(q) || (c.symbol ?? "").toLowerCase().includes(q)
50
+ );
51
+ }, [collections, query]);
52
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-6", children: [
53
+ heading && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-2", children: [
54
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2 text-primary", children: [
55
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Coins, { className: "h-5 w-5" }),
56
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "Tokens" })
57
+ ] }),
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." })
60
+ ] }),
61
+ /* @__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: [
76
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
77
+ "select",
78
+ {
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))
83
+ }
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)(
86
+ "button",
87
+ {
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",
101
+ {
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"
106
+ }
107
+ )
108
+ ] })
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}`)) })
111
+ ] });
112
+ }
113
+ // Annotate the CommonJS export names for ESM import in node:
114
+ 0 && (module.exports = {
115
+ CoinsExplorer
116
+ });
117
+ //# sourceMappingURL=coins-explorer.cjs.map
@@ -0,0 +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":[]}
@@ -0,0 +1,23 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { UseCoinPrice } from './coin-card.cjs';
3
+ import { CoinCollectionLike } from '../data/coins.cjs';
4
+
5
+ type CoinFilter = "all" | "creator" | "memecoin";
6
+ type CoinSort = "recent" | "name";
7
+ type UseCoins = (opts: {
8
+ filter: CoinFilter;
9
+ sort: CoinSort;
10
+ }) => {
11
+ collections: CoinCollectionLike[];
12
+ isLoading: boolean;
13
+ };
14
+ interface CoinsExplorerProps {
15
+ useCoins: UseCoins;
16
+ usePrice: UseCoinPrice;
17
+ /** Build the link target for a coin (internal or per-chain trading app). */
18
+ coinHref: (collection: CoinCollectionLike) => string;
19
+ heading?: boolean;
20
+ }
21
+ declare function CoinsExplorer({ useCoins, usePrice, coinHref, heading }: CoinsExplorerProps): react_jsx_runtime.JSX.Element;
22
+
23
+ export { type CoinFilter, type CoinSort, CoinsExplorer, type CoinsExplorerProps, type UseCoins };
@@ -0,0 +1,23 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { UseCoinPrice } from './coin-card.js';
3
+ import { CoinCollectionLike } from '../data/coins.js';
4
+
5
+ type CoinFilter = "all" | "creator" | "memecoin";
6
+ type CoinSort = "recent" | "name";
7
+ type UseCoins = (opts: {
8
+ filter: CoinFilter;
9
+ sort: CoinSort;
10
+ }) => {
11
+ collections: CoinCollectionLike[];
12
+ isLoading: boolean;
13
+ };
14
+ interface CoinsExplorerProps {
15
+ useCoins: UseCoins;
16
+ usePrice: UseCoinPrice;
17
+ /** Build the link target for a coin (internal or per-chain trading app). */
18
+ coinHref: (collection: CoinCollectionLike) => string;
19
+ heading?: boolean;
20
+ }
21
+ declare function CoinsExplorer({ useCoins, usePrice, coinHref, heading }: CoinsExplorerProps): react_jsx_runtime.JSX.Element;
22
+
23
+ export { type CoinFilter, type CoinSort, CoinsExplorer, type CoinsExplorerProps, type UseCoins };