@medialane/ui 0.86.0 → 0.88.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.
- package/README.md +2 -0
- package/dist/components/coins-explorer.cjs +2 -2
- package/dist/components/coins-explorer.cjs.map +1 -1
- package/dist/components/coins-explorer.js +2 -2
- package/dist/components/coins-explorer.js.map +1 -1
- package/dist/components/discover-creators-strip.cjs +1 -1
- package/dist/components/discover-creators-strip.cjs.map +1 -1
- package/dist/components/discover-creators-strip.js +1 -1
- package/dist/components/discover-creators-strip.js.map +1 -1
- package/dist/components/nav-shell.cjs +2 -2
- package/dist/components/nav-shell.cjs.map +1 -1
- package/dist/components/nav-shell.d.cts +13 -3
- package/dist/components/nav-shell.d.ts +13 -3
- package/dist/components/nav-shell.js +2 -2
- package/dist/components/nav-shell.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -233,6 +233,8 @@ The package uses [tsup](https://tsup.egoist.dev/) and outputs ESM + CJS + type d
|
|
|
233
233
|
|
|
234
234
|
| Version | Added |
|
|
235
235
|
|---|---|
|
|
236
|
+
| **v0.87.0** | `NavWalletTrigger` gains optional `disconnectedIcon` — overrides the default `Wallet` glyph in the disconnected-state ring, for apps whose real connect entry point isn't a wallet (e.g. a Google mark for an email/social-login app). Omitted = unchanged `Wallet` default |
|
|
237
|
+
| **v0.86.0** | `NavWalletTrigger`'s disconnected state shows a plain `Wallet` glyph inside the rotating ring instead of an empty center — the bare ring wasn't being noticed as a connect entry point. Connected state unchanged |
|
|
236
238
|
| **v0.85.0** | `NavCommandMenu` gains two optional props: `showKeyboardHints` (default `true` — hide the "↑↓ Navigate / ↵ Open" footer hint) and `brandSlot` (replace the static "medialane ⌘K" footer brandmark entirely, e.g. with a "Connect" button). Both default to the prior behavior — non-breaking |
|
|
237
239
|
| **v0.84.0** | gol_starknet living-render support: `useIntersectionActive` + `AnimatedTokenMedia` swap a token's static image for a sandboxed iframe of its on-chain `animation_url` once visible, wired into `TokenCard`/`AssetCard`; SDK peer floor `>=0.73.0`. **Also replaces `AssetMediaColumn`** with the borderless/real-aspect-ratio/click-to-zoom design both apps had already forked locally |
|
|
238
240
|
| **v0.83.0** | `NavWalletTrigger` gains optional `iconSrc` — renders the connected wallet's own icon instead of a generic glyph; omitted = unchanged behavior |
|
|
@@ -63,9 +63,9 @@ function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
|
|
|
63
63
|
heading && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-2", children: [
|
|
64
64
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2 text-primary", children: [
|
|
65
65
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Coins, { className: "h-5 w-5" }),
|
|
66
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "
|
|
66
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "Coins" })
|
|
67
67
|
] }),
|
|
68
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-3xl
|
|
68
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-3xl", children: "Creator coins & memecoins" })
|
|
69
69
|
] }),
|
|
70
70
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-3 border-b border-border/60 pb-3", children: [
|
|
71
71
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
@@ -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 * 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 & 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
|
+
{"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\">Coins</span>\n </div>\n <h1 className=\"text-3xl\">Creator coins & 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":[]}
|
|
@@ -40,9 +40,9 @@ function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
|
|
|
40
40
|
heading && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
41
41
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-primary", children: [
|
|
42
42
|
/* @__PURE__ */ jsx(Coins, { className: "h-5 w-5" }),
|
|
43
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "
|
|
43
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "Coins" })
|
|
44
44
|
] }),
|
|
45
|
-
/* @__PURE__ */ jsx("h1", { className: "text-3xl
|
|
45
|
+
/* @__PURE__ */ jsx("h1", { className: "text-3xl", children: "Creator coins & memecoins" })
|
|
46
46
|
] }),
|
|
47
47
|
/* @__PURE__ */ jsxs("div", { className: "space-y-3 border-b border-border/60 pb-3", children: [
|
|
48
48
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
@@ -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 * 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 & 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
|
+
{"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\">Coins</span>\n </div>\n <h1 className=\"text-3xl\">Creator coins & 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":[]}
|
|
@@ -35,7 +35,7 @@ function CreatorChip({
|
|
|
35
35
|
href
|
|
36
36
|
}) {
|
|
37
37
|
const [bannerError, setBannerError] = (0, import_react.useState)(false);
|
|
38
|
-
const rawSrc = creator.
|
|
38
|
+
const rawSrc = creator.avatarImage || creator.collectionImage || null;
|
|
39
39
|
const bannerUrl = rawSrc && !bannerError ? (0, import_ipfs.ipfsToHttp)(rawSrc) : null;
|
|
40
40
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
41
41
|
"a",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/discover-creators-strip.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { Users, ArrowRight } from \"lucide-react\";\nimport { FadeIn } from \"./motion-primitives.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport type { ApiCreatorProfile } from \"@medialane/sdk\";\n\nexport interface DiscoverCreatorsStripProps {\n creators: ApiCreatorProfile[];\n isLoading: boolean;\n getHref: (creator: ApiCreatorProfile) => string;\n allCreatorsHref?: string;\n sectionLabel?: string;\n title?: string;\n}\n\nfunction CreatorChipSkeleton() {\n return (\n <div className=\"shrink-0 w-64 aspect-[3/4] rounded-xl bg-muted animate-pulse\" />\n );\n}\n\nfunction CreatorChip({\n creator,\n href,\n}: {\n creator: ApiCreatorProfile;\n href: string;\n}) {\n const [bannerError, setBannerError] = useState(false);\n\n //
|
|
1
|
+
{"version":3,"sources":["../../src/components/discover-creators-strip.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { Users, ArrowRight } from \"lucide-react\";\nimport { FadeIn } from \"./motion-primitives.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport type { ApiCreatorProfile } from \"@medialane/sdk\";\n\nexport interface DiscoverCreatorsStripProps {\n creators: ApiCreatorProfile[];\n isLoading: boolean;\n getHref: (creator: ApiCreatorProfile) => string;\n allCreatorsHref?: string;\n sectionLabel?: string;\n title?: string;\n}\n\nfunction CreatorChipSkeleton() {\n return (\n <div className=\"shrink-0 w-64 aspect-[3/4] rounded-xl bg-muted animate-pulse\" />\n );\n}\n\nfunction CreatorChip({\n creator,\n href,\n}: {\n creator: ApiCreatorProfile;\n href: string;\n}) {\n const [bannerError, setBannerError] = useState(false);\n\n // avatarImage (the creator's chosen NFT) with collectionImage fallback — io-approved big-username design\n const rawSrc = creator.avatarImage || creator.collectionImage || null;\n const bannerUrl = rawSrc && !bannerError ? ipfsToHttp(rawSrc) : null;\n\n return (\n <a\n href={href}\n className=\"block shrink-0 w-64 snap-start active:scale-[0.97] transition-transform duration-150 select-none\"\n >\n <div className=\"relative aspect-[3/4] rounded-xl overflow-hidden bg-muted\">\n {bannerUrl && (\n <img\n src={bannerUrl}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n className=\"absolute inset-0 w-full h-full object-cover\"\n onError={() => setBannerError(true)}\n />\n )}\n <div className=\"absolute bottom-0 inset-x-0 px-3 py-3\">\n <p className=\"font-bold text-2xl text-white truncate\">{creator.username}</p>\n </div>\n </div>\n </a>\n );\n}\n\nexport function DiscoverCreatorsStrip({\n creators,\n isLoading,\n getHref,\n allCreatorsHref = \"/creators\",\n sectionLabel = \"Explore\",\n title = \"Creators\",\n}: DiscoverCreatorsStripProps) {\n if (!isLoading && creators.length === 0) return null;\n\n return (\n <FadeIn>\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"section-label\">{sectionLabel}</p>\n <div className=\"flex items-center gap-2 mt-0.5\">\n <Users className=\"h-4 w-4 text-brand-purple\" />\n <h2 className=\"text-lg font-bold\">{title}</h2>\n </div>\n </div>\n <a\n href={allCreatorsHref}\n className=\"inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent transition-colors\"\n >\n View all <ArrowRight className=\"h-3.5 w-3.5\" />\n </a>\n </div>\n <div className=\"overflow-x-auto scrollbar-hide snap-x snap-mandatory -mx-4 px-4 md:mx-0 md:px-0\">\n <div className=\"flex gap-5 sm:gap-6 w-max pb-2\">\n {isLoading\n ? Array.from({ length: 6 }).map((_, i) => <CreatorChipSkeleton key={i} />)\n : creators.map((c) => (\n <CreatorChip key={c.walletAddress} creator={c} href={getHref(c)} />\n ))}\n </div>\n </div>\n </div>\n </FadeIn>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBI;AAjBJ,mBAAyB;AACzB,0BAAkC;AAClC,+BAAuB;AACvB,kBAA2B;AAY3B,SAAS,sBAAsB;AAC7B,SACE,4CAAC,SAAI,WAAU,gEAA+D;AAElF;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AACF,GAGG;AACD,QAAM,CAAC,aAAa,cAAc,QAAI,uBAAS,KAAK;AAGpD,QAAM,SAAS,QAAQ,eAAe,QAAQ,mBAAmB;AACjE,QAAM,YAAY,UAAU,CAAC,kBAAc,wBAAW,MAAM,IAAI;AAEhE,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MAEV,uDAAC,SAAI,WAAU,6DACZ;AAAA,qBACC;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,KAAI;AAAA,YACJ,eAAW;AAAA,YACX,SAAQ;AAAA,YACR,WAAU;AAAA,YACV,SAAS,MAAM,eAAe,IAAI;AAAA;AAAA,QACpC;AAAA,QAEF,4CAAC,SAAI,WAAU,yCACb,sDAAC,OAAE,WAAU,0CAA0C,kBAAQ,UAAS,GAC1E;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;AAEO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,QAAQ;AACV,GAA+B;AAC7B,MAAI,CAAC,aAAa,SAAS,WAAW,EAAG,QAAO;AAEhD,SACE,4CAAC,mCACC,uDAAC,SAAI,WAAU,aACb;AAAA,iDAAC,SAAI,WAAU,qCACb;AAAA,mDAAC,SACC;AAAA,oDAAC,OAAE,WAAU,iBAAiB,wBAAa;AAAA,QAC3C,6CAAC,SAAI,WAAU,kCACb;AAAA,sDAAC,6BAAM,WAAU,6BAA4B;AAAA,UAC7C,4CAAC,QAAG,WAAU,qBAAqB,iBAAM;AAAA,WAC3C;AAAA,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,WAAU;AAAA,UACX;AAAA;AAAA,YACU,4CAAC,kCAAW,WAAU,eAAc;AAAA;AAAA;AAAA,MAC/C;AAAA,OACF;AAAA,IACA,4CAAC,SAAI,WAAU,mFACb,sDAAC,SAAI,WAAU,kCACZ,sBACG,MAAM,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,MAAM,4CAAC,yBAAyB,CAAG,CAAE,IACvE,SAAS,IAAI,CAAC,MACZ,4CAAC,eAAkC,SAAS,GAAG,MAAM,QAAQ,CAAC,KAA5C,EAAE,aAA6C,CAClE,GACP,GACF;AAAA,KACF,GACF;AAEJ;","names":[]}
|
|
@@ -12,7 +12,7 @@ function CreatorChip({
|
|
|
12
12
|
href
|
|
13
13
|
}) {
|
|
14
14
|
const [bannerError, setBannerError] = useState(false);
|
|
15
|
-
const rawSrc = creator.
|
|
15
|
+
const rawSrc = creator.avatarImage || creator.collectionImage || null;
|
|
16
16
|
const bannerUrl = rawSrc && !bannerError ? ipfsToHttp(rawSrc) : null;
|
|
17
17
|
return /* @__PURE__ */ jsx(
|
|
18
18
|
"a",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/discover-creators-strip.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { Users, ArrowRight } from \"lucide-react\";\nimport { FadeIn } from \"./motion-primitives.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport type { ApiCreatorProfile } from \"@medialane/sdk\";\n\nexport interface DiscoverCreatorsStripProps {\n creators: ApiCreatorProfile[];\n isLoading: boolean;\n getHref: (creator: ApiCreatorProfile) => string;\n allCreatorsHref?: string;\n sectionLabel?: string;\n title?: string;\n}\n\nfunction CreatorChipSkeleton() {\n return (\n <div className=\"shrink-0 w-64 aspect-[3/4] rounded-xl bg-muted animate-pulse\" />\n );\n}\n\nfunction CreatorChip({\n creator,\n href,\n}: {\n creator: ApiCreatorProfile;\n href: string;\n}) {\n const [bannerError, setBannerError] = useState(false);\n\n //
|
|
1
|
+
{"version":3,"sources":["../../src/components/discover-creators-strip.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { Users, ArrowRight } from \"lucide-react\";\nimport { FadeIn } from \"./motion-primitives.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport type { ApiCreatorProfile } from \"@medialane/sdk\";\n\nexport interface DiscoverCreatorsStripProps {\n creators: ApiCreatorProfile[];\n isLoading: boolean;\n getHref: (creator: ApiCreatorProfile) => string;\n allCreatorsHref?: string;\n sectionLabel?: string;\n title?: string;\n}\n\nfunction CreatorChipSkeleton() {\n return (\n <div className=\"shrink-0 w-64 aspect-[3/4] rounded-xl bg-muted animate-pulse\" />\n );\n}\n\nfunction CreatorChip({\n creator,\n href,\n}: {\n creator: ApiCreatorProfile;\n href: string;\n}) {\n const [bannerError, setBannerError] = useState(false);\n\n // avatarImage (the creator's chosen NFT) with collectionImage fallback — io-approved big-username design\n const rawSrc = creator.avatarImage || creator.collectionImage || null;\n const bannerUrl = rawSrc && !bannerError ? ipfsToHttp(rawSrc) : null;\n\n return (\n <a\n href={href}\n className=\"block shrink-0 w-64 snap-start active:scale-[0.97] transition-transform duration-150 select-none\"\n >\n <div className=\"relative aspect-[3/4] rounded-xl overflow-hidden bg-muted\">\n {bannerUrl && (\n <img\n src={bannerUrl}\n alt=\"\"\n aria-hidden\n loading=\"lazy\"\n className=\"absolute inset-0 w-full h-full object-cover\"\n onError={() => setBannerError(true)}\n />\n )}\n <div className=\"absolute bottom-0 inset-x-0 px-3 py-3\">\n <p className=\"font-bold text-2xl text-white truncate\">{creator.username}</p>\n </div>\n </div>\n </a>\n );\n}\n\nexport function DiscoverCreatorsStrip({\n creators,\n isLoading,\n getHref,\n allCreatorsHref = \"/creators\",\n sectionLabel = \"Explore\",\n title = \"Creators\",\n}: DiscoverCreatorsStripProps) {\n if (!isLoading && creators.length === 0) return null;\n\n return (\n <FadeIn>\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div>\n <p className=\"section-label\">{sectionLabel}</p>\n <div className=\"flex items-center gap-2 mt-0.5\">\n <Users className=\"h-4 w-4 text-brand-purple\" />\n <h2 className=\"text-lg font-bold\">{title}</h2>\n </div>\n </div>\n <a\n href={allCreatorsHref}\n className=\"inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground px-3 py-1.5 rounded-md hover:bg-accent transition-colors\"\n >\n View all <ArrowRight className=\"h-3.5 w-3.5\" />\n </a>\n </div>\n <div className=\"overflow-x-auto scrollbar-hide snap-x snap-mandatory -mx-4 px-4 md:mx-0 md:px-0\">\n <div className=\"flex gap-5 sm:gap-6 w-max pb-2\">\n {isLoading\n ? Array.from({ length: 6 }).map((_, i) => <CreatorChipSkeleton key={i} />)\n : creators.map((c) => (\n <CreatorChip key={c.walletAddress} creator={c} href={getHref(c)} />\n ))}\n </div>\n </div>\n </div>\n </FadeIn>\n );\n}\n"],"mappings":";AAmBI,cAsBE,YAtBF;AAjBJ,SAAS,gBAAgB;AACzB,SAAS,OAAO,kBAAkB;AAClC,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAY3B,SAAS,sBAAsB;AAC7B,SACE,oBAAC,SAAI,WAAU,gEAA+D;AAElF;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AACF,GAGG;AACD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAGpD,QAAM,SAAS,QAAQ,eAAe,QAAQ,mBAAmB;AACjE,QAAM,YAAY,UAAU,CAAC,cAAc,WAAW,MAAM,IAAI;AAEhE,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAU;AAAA,MAEV,+BAAC,SAAI,WAAU,6DACZ;AAAA,qBACC;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,KAAI;AAAA,YACJ,eAAW;AAAA,YACX,SAAQ;AAAA,YACR,WAAU;AAAA,YACV,SAAS,MAAM,eAAe,IAAI;AAAA;AAAA,QACpC;AAAA,QAEF,oBAAC,SAAI,WAAU,yCACb,8BAAC,OAAE,WAAU,0CAA0C,kBAAQ,UAAS,GAC1E;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;AAEO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,QAAQ;AACV,GAA+B;AAC7B,MAAI,CAAC,aAAa,SAAS,WAAW,EAAG,QAAO;AAEhD,SACE,oBAAC,UACC,+BAAC,SAAI,WAAU,aACb;AAAA,yBAAC,SAAI,WAAU,qCACb;AAAA,2BAAC,SACC;AAAA,4BAAC,OAAE,WAAU,iBAAiB,wBAAa;AAAA,QAC3C,qBAAC,SAAI,WAAU,kCACb;AAAA,8BAAC,SAAM,WAAU,6BAA4B;AAAA,UAC7C,oBAAC,QAAG,WAAU,qBAAqB,iBAAM;AAAA,WAC3C;AAAA,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,WAAU;AAAA,UACX;AAAA;AAAA,YACU,oBAAC,cAAW,WAAU,eAAc;AAAA;AAAA;AAAA,MAC/C;AAAA,OACF;AAAA,IACA,oBAAC,SAAI,WAAU,mFACb,8BAAC,SAAI,WAAU,kCACZ,sBACG,MAAM,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,MAAM,oBAAC,yBAAyB,CAAG,CAAE,IACvE,SAAS,IAAI,CAAC,MACZ,oBAAC,eAAkC,SAAS,GAAG,MAAM,QAAQ,CAAC,KAA5C,EAAE,aAA6C,CAClE,GACP,GACF;AAAA,KACF,GACF;AAEJ;","names":[]}
|
|
@@ -87,7 +87,7 @@ function NavIconButton({ onClick, className, indicator, children, ...rest }) {
|
|
|
87
87
|
);
|
|
88
88
|
}
|
|
89
89
|
const NavWalletTrigger = React.forwardRef(
|
|
90
|
-
function NavWalletTrigger2({ onClick, className, connected = false, iconSrc, ...rest }, ref) {
|
|
90
|
+
function NavWalletTrigger2({ onClick, className, connected = false, iconSrc, disconnectedIcon, ...rest }, ref) {
|
|
91
91
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
92
92
|
"button",
|
|
93
93
|
{
|
|
@@ -105,7 +105,7 @@ const NavWalletTrigger = React.forwardRef(
|
|
|
105
105
|
),
|
|
106
106
|
children: [
|
|
107
107
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ml-nav-wallet-ring", "aria-hidden": "true" }),
|
|
108
|
-
connected ? iconSrc ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: iconSrc, alt: "", width: 16, height: 16, className: "h-4 w-4 shrink-0 rounded-full" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.User, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Wallet, { className: "h-3.5 w-3.5" })
|
|
108
|
+
connected ? iconSrc ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: iconSrc, alt: "", width: 16, height: 16, className: "h-4 w-4 shrink-0 rounded-full" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.User, { className: "h-3.5 w-3.5" }) : disconnectedIcon ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Wallet, { className: "h-3.5 w-3.5" })
|
|
109
109
|
]
|
|
110
110
|
}
|
|
111
111
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/nav-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { User, Wallet } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\n\n// ── Header buttons ────────────────────────────────────────────────────────────\n\nexport interface NavBrandButtonProps {\n /** Defaults to opening the nav command menu (`ml:nav-open`). */\n onClick?: () => void;\n className?: string;\n \"aria-label\"?: string;\n /** Path to the brand icon image (the app's real icon asset). */\n iconSrc?: string;\n}\n\n/**\n * The main header trigger: brand mark + menu glyph in one quiet glass pill.\n * Mobile-first — a single ≥44px tap target instead of two small adjacent icons.\n * With no `onClick`, it opens the NavCommandMenu mounted elsewhere in the app.\n */\nexport function NavBrandButton({ onClick, className, iconSrc = \"/icon.png\", ...rest }: NavBrandButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick ?? (() => document.dispatchEvent(new CustomEvent(ML_NAV_OPEN)))}\n aria-label={rest[\"aria-label\"] ?? \"Open navigation\"}\n className={cn(\n \"group flex h-11 items-center gap-2 rounded-full\",\n \"bg-background/10 pl-2.5 pr-3 backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={iconSrc} alt=\"Medialane\" width={30} height={30} className=\"h-[30px] w-[30px] shrink-0\" />\n <span className=\"flex flex-col gap-[5px]\" aria-hidden=\"true\">\n <span className=\"h-[1.5px] w-4 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n <span className=\"h-[1.5px] w-2.5 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n </span>\n </button>\n );\n}\n\nexport interface NavIconButtonProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\": string;\n /** Show a small brand-colored status dot (e.g. wallet connected). */\n indicator?: boolean;\n children: React.ReactNode;\n}\n\n/**\n * Circular glass header button — the right-side counterpart to NavBrandButton\n * (wallet / account trigger).\n */\nexport function NavIconButton({ onClick, className, indicator, children, ...rest }: NavIconButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"]}\n className={cn(\n \"relative flex h-11 w-11 items-center justify-center rounded-full\",\n \"bg-background/10 text-muted-foreground backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 hover:text-foreground active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {children}\n {indicator && (\n <span className=\"absolute right-[3px] top-[3px] h-2.5 w-2.5 rounded-full border-2 border-background bg-brand-blue\" />\n )}\n </button>\n );\n}\n\nexport interface NavWalletTriggerProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\"?: string;\n /** Whether a wallet is currently connected. */\n connected?: boolean;\n /**\n * The connected wallet's own icon (e.g. Argent/Braavos/Cartridge). Optional —\n * apps with no single \"connected wallet\" identity to show (e.g. an\n * email/social-login app) simply omit it and get the plain glyph below.\n */\n iconSrc?: string;\n}\n\n/**\n * The right-side counterpart to `NavBrandButton` for a wallet/account entry\n * point: the same glass pill material, with a thin brand-gradient ring\n * traced around the rim instead of a static icon. Not connected — the ring\n * rotates slowly (an ambient \"something opens here\" cue) around a plain\n * `Wallet` glyph, so the trigger reads as \"connect your wallet\" rather than\n * disappearing into the header (users weren't noticing the bare ring).\n * Connected — the ring settles to a static accent and shows the connected\n * wallet's own icon via `iconSrc` when the caller has one; otherwise a plain\n * `User` glyph (never a fabricated generic avatar).\n */\nexport const NavWalletTrigger = React.forwardRef<HTMLButtonElement, NavWalletTriggerProps>(\n function NavWalletTrigger({ onClick, className, connected = false, iconSrc, ...rest }, ref) {\n return (\n <button\n ref={ref}\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"] ?? (connected ? \"Account\" : \"Connect wallet\")}\n className={cn(\n \"ml-nav-wallet-trigger relative flex h-8 w-8 items-center justify-center rounded-full\",\n \"bg-background/10 text-muted-foreground backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 hover:text-foreground active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n connected && \"ml-nav-wallet-trigger--connected\",\n className\n )}\n >\n <span className=\"ml-nav-wallet-ring\" aria-hidden=\"true\" />\n {connected\n ? (iconSrc\n ? <img src={iconSrc} alt=\"\" width={16} height={16} className=\"h-4 w-4 shrink-0 rounded-full\" />\n : <User className=\"h-3.5 w-3.5\" />)\n : <Wallet className=\"h-3.5 w-3.5\" />}\n </button>\n );\n }\n);\n\n// ── Account sheet ─────────────────────────────────────────────────────────────\n\nconst ML_ACCOUNT_OPEN = \"ml:account-open\";\nconst ML_ACCOUNT_CLOSE = \"ml:account-close\";\nconst ML_NAV_OPEN = \"ml:nav-open\";\nconst ML_NAV_CLOSE = \"ml:nav-close\";\n\nexport function useNavAccountSheet() {\n return {\n open: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_OPEN)),\n close: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_CLOSE)),\n };\n}\n\nexport interface NavAccountSheetProps {\n /** The app's account/wallet panel content — identity, network, disconnect, etc. */\n children: React.ReactNode;\n}\n\n/**\n * The wallet-side surface: a centered, backdrop-blurred panel — the exact\n * same overlay + card treatment as `NavCommandMenu` (`.nav-canvas-overlay`,\n * `bg-background/90 backdrop-blur-2xl backdrop-saturate-150`), so every\n * global panel in the header reads as one system. No built-in title — this\n * is an account/wallet card, not a menu; content is entirely app-owned.\n *\n * Opens via `useNavAccountSheet().open()`. Mutually exclusive with the\n * command menu: opening one closes the other, and the shared `ml:nav-close`\n * event (fired by `useNavCommandMenu().close()`) closes this too.\n */\nexport function NavAccountSheet({ children }: NavAccountSheetProps) {\n const [open, setOpen] = React.useState(false);\n\n React.useEffect(() => {\n const onOpen = () => {\n document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE));\n setOpen(true);\n };\n const onClose = () => setOpen(false);\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.addEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.addEventListener(ML_NAV_OPEN, onClose);\n document.addEventListener(ML_NAV_CLOSE, onClose);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.removeEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.removeEventListener(ML_NAV_OPEN, onClose);\n document.removeEventListener(ML_NAV_CLOSE, onClose);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, []);\n\n return (\n <AnimatePresence>\n {open && (\n <>\n {/* Backdrop blur — identical to NavCommandMenu's */}\n <motion.div\n className=\"nav-canvas-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.15 }}\n onClick={() => setOpen(false)}\n />\n\n {/* Panel — centered on desktop, bottom sheet on mobile, same as NavCommandMenu */}\n <motion.div\n className=\"fixed inset-0 z-[101] flex items-end justify-center p-3 pb-4 sm:items-center sm:p-4\"\n initial={{ opacity: 0, y: 24 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 24 }}\n transition={{ duration: 0.18, ease: \"easeOut\" }}\n onClick={() => setOpen(false)}\n >\n <div\n className=\"relative w-full max-w-[380px] overflow-hidden rounded-[20px] border border-border/40 bg-background/90 shadow-2xl backdrop-blur-2xl backdrop-saturate-150\"\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"flex justify-center pt-2.5 sm:hidden\" aria-hidden=\"true\">\n <span className=\"h-1 w-9 rounded-full bg-muted-foreground/30\" />\n </div>\n <button\n onClick={() => setOpen(false)}\n className=\"absolute right-3 top-3 rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-muted/50 hover:text-foreground\"\n aria-label=\"Close\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n </button>\n <div className=\"px-4 pb-4 pt-5 sm:pt-6\">{children}</div>\n </div>\n </motion.div>\n </>\n )}\n </AnimatePresence>\n );\n}\n"],"mappings":"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","names":["NavWalletTrigger"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/nav-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { User, Wallet } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\n\n// ── Header buttons ────────────────────────────────────────────────────────────\n\nexport interface NavBrandButtonProps {\n /** Defaults to opening the nav command menu (`ml:nav-open`). */\n onClick?: () => void;\n className?: string;\n \"aria-label\"?: string;\n /** Path to the brand icon image (the app's real icon asset). */\n iconSrc?: string;\n}\n\n/**\n * The main header trigger: brand mark + menu glyph in one quiet glass pill.\n * Mobile-first — a single ≥44px tap target instead of two small adjacent icons.\n * With no `onClick`, it opens the NavCommandMenu mounted elsewhere in the app.\n */\nexport function NavBrandButton({ onClick, className, iconSrc = \"/icon.png\", ...rest }: NavBrandButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick ?? (() => document.dispatchEvent(new CustomEvent(ML_NAV_OPEN)))}\n aria-label={rest[\"aria-label\"] ?? \"Open navigation\"}\n className={cn(\n \"group flex h-11 items-center gap-2 rounded-full\",\n \"bg-background/10 pl-2.5 pr-3 backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={iconSrc} alt=\"Medialane\" width={30} height={30} className=\"h-[30px] w-[30px] shrink-0\" />\n <span className=\"flex flex-col gap-[5px]\" aria-hidden=\"true\">\n <span className=\"h-[1.5px] w-4 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n <span className=\"h-[1.5px] w-2.5 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n </span>\n </button>\n );\n}\n\nexport interface NavIconButtonProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\": string;\n /** Show a small brand-colored status dot (e.g. wallet connected). */\n indicator?: boolean;\n children: React.ReactNode;\n}\n\n/**\n * Circular glass header button — the right-side counterpart to NavBrandButton\n * (wallet / account trigger).\n */\nexport function NavIconButton({ onClick, className, indicator, children, ...rest }: NavIconButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"]}\n className={cn(\n \"relative flex h-11 w-11 items-center justify-center rounded-full\",\n \"bg-background/10 text-muted-foreground backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 hover:text-foreground active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {children}\n {indicator && (\n <span className=\"absolute right-[3px] top-[3px] h-2.5 w-2.5 rounded-full border-2 border-background bg-brand-blue\" />\n )}\n </button>\n );\n}\n\nexport interface NavWalletTriggerProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\"?: string;\n /** Whether a wallet is currently connected. */\n connected?: boolean;\n /**\n * The connected wallet's own icon (e.g. Argent/Braavos/Cartridge). Optional —\n * apps with no single \"connected wallet\" identity to show (e.g. an\n * email/social-login app) simply omit it and get the plain glyph below.\n */\n iconSrc?: string;\n /**\n * Override for the disconnected-state glyph (default: a plain `Wallet`\n * icon). Apps whose actual connect entry point isn't a wallet — e.g. an\n * email/social-login app whose users primarily sign in with Google — can\n * pass a more accurate icon here so the ring points at what will really\n * happen on click.\n */\n disconnectedIcon?: React.ReactNode;\n}\n\n/**\n * The right-side counterpart to `NavBrandButton` for a wallet/account entry\n * point: the same glass pill material, with a thin brand-gradient ring\n * traced around the rim instead of a static icon. Not connected — the ring\n * rotates slowly (an ambient \"something opens here\" cue) around a plain\n * `Wallet` glyph (override via `disconnectedIcon` — e.g. a Google mark for an\n * email/social-login app), so the trigger reads as an actionable connect\n * entry point rather than disappearing into the header (users weren't\n * noticing the bare ring). Connected — the ring settles to a static accent\n * and shows the connected\n * wallet's own icon via `iconSrc` when the caller has one; otherwise a plain\n * `User` glyph (never a fabricated generic avatar).\n */\nexport const NavWalletTrigger = React.forwardRef<HTMLButtonElement, NavWalletTriggerProps>(\n function NavWalletTrigger({ onClick, className, connected = false, iconSrc, disconnectedIcon, ...rest }, ref) {\n return (\n <button\n ref={ref}\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"] ?? (connected ? \"Account\" : \"Connect wallet\")}\n className={cn(\n \"ml-nav-wallet-trigger relative flex h-8 w-8 items-center justify-center rounded-full\",\n \"bg-background/10 text-muted-foreground backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 hover:text-foreground active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n connected && \"ml-nav-wallet-trigger--connected\",\n className\n )}\n >\n <span className=\"ml-nav-wallet-ring\" aria-hidden=\"true\" />\n {connected\n ? (iconSrc\n ? <img src={iconSrc} alt=\"\" width={16} height={16} className=\"h-4 w-4 shrink-0 rounded-full\" />\n : <User className=\"h-3.5 w-3.5\" />)\n : (disconnectedIcon ?? <Wallet className=\"h-3.5 w-3.5\" />)}\n </button>\n );\n }\n);\n\n// ── Account sheet ─────────────────────────────────────────────────────────────\n\nconst ML_ACCOUNT_OPEN = \"ml:account-open\";\nconst ML_ACCOUNT_CLOSE = \"ml:account-close\";\nconst ML_NAV_OPEN = \"ml:nav-open\";\nconst ML_NAV_CLOSE = \"ml:nav-close\";\n\nexport function useNavAccountSheet() {\n return {\n open: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_OPEN)),\n close: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_CLOSE)),\n };\n}\n\nexport interface NavAccountSheetProps {\n /** The app's account/wallet panel content — identity, network, disconnect, etc. */\n children: React.ReactNode;\n}\n\n/**\n * The wallet-side surface: a centered, backdrop-blurred panel — the exact\n * same overlay + card treatment as `NavCommandMenu` (`.nav-canvas-overlay`,\n * `bg-background/90 backdrop-blur-2xl backdrop-saturate-150`), so every\n * global panel in the header reads as one system. No built-in title — this\n * is an account/wallet card, not a menu; content is entirely app-owned.\n *\n * Opens via `useNavAccountSheet().open()`. Mutually exclusive with the\n * command menu: opening one closes the other, and the shared `ml:nav-close`\n * event (fired by `useNavCommandMenu().close()`) closes this too.\n */\nexport function NavAccountSheet({ children }: NavAccountSheetProps) {\n const [open, setOpen] = React.useState(false);\n\n React.useEffect(() => {\n const onOpen = () => {\n document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE));\n setOpen(true);\n };\n const onClose = () => setOpen(false);\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.addEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.addEventListener(ML_NAV_OPEN, onClose);\n document.addEventListener(ML_NAV_CLOSE, onClose);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.removeEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.removeEventListener(ML_NAV_OPEN, onClose);\n document.removeEventListener(ML_NAV_CLOSE, onClose);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, []);\n\n return (\n <AnimatePresence>\n {open && (\n <>\n {/* Backdrop blur — identical to NavCommandMenu's */}\n <motion.div\n className=\"nav-canvas-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.15 }}\n onClick={() => setOpen(false)}\n />\n\n {/* Panel — centered on desktop, bottom sheet on mobile, same as NavCommandMenu */}\n <motion.div\n className=\"fixed inset-0 z-[101] flex items-end justify-center p-3 pb-4 sm:items-center sm:p-4\"\n initial={{ opacity: 0, y: 24 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 24 }}\n transition={{ duration: 0.18, ease: \"easeOut\" }}\n onClick={() => setOpen(false)}\n >\n <div\n className=\"relative w-full max-w-[380px] overflow-hidden rounded-[20px] border border-border/40 bg-background/90 shadow-2xl backdrop-blur-2xl backdrop-saturate-150\"\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"flex justify-center pt-2.5 sm:hidden\" aria-hidden=\"true\">\n <span className=\"h-1 w-9 rounded-full bg-muted-foreground/30\" />\n </div>\n <button\n onClick={() => setOpen(false)}\n className=\"absolute right-3 top-3 rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-muted/50 hover:text-foreground\"\n aria-label=\"Close\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n </button>\n <div className=\"px-4 pb-4 pt-5 sm:pt-6\">{children}</div>\n </div>\n </motion.div>\n </>\n )}\n </AnimatePresence>\n );\n}\n"],"mappings":"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","names":["NavWalletTrigger"]}
|
|
@@ -40,15 +40,25 @@ interface NavWalletTriggerProps {
|
|
|
40
40
|
* email/social-login app) simply omit it and get the plain glyph below.
|
|
41
41
|
*/
|
|
42
42
|
iconSrc?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Override for the disconnected-state glyph (default: a plain `Wallet`
|
|
45
|
+
* icon). Apps whose actual connect entry point isn't a wallet — e.g. an
|
|
46
|
+
* email/social-login app whose users primarily sign in with Google — can
|
|
47
|
+
* pass a more accurate icon here so the ring points at what will really
|
|
48
|
+
* happen on click.
|
|
49
|
+
*/
|
|
50
|
+
disconnectedIcon?: React.ReactNode;
|
|
43
51
|
}
|
|
44
52
|
/**
|
|
45
53
|
* The right-side counterpart to `NavBrandButton` for a wallet/account entry
|
|
46
54
|
* point: the same glass pill material, with a thin brand-gradient ring
|
|
47
55
|
* traced around the rim instead of a static icon. Not connected — the ring
|
|
48
56
|
* rotates slowly (an ambient "something opens here" cue) around a plain
|
|
49
|
-
* `Wallet` glyph
|
|
50
|
-
*
|
|
51
|
-
*
|
|
57
|
+
* `Wallet` glyph (override via `disconnectedIcon` — e.g. a Google mark for an
|
|
58
|
+
* email/social-login app), so the trigger reads as an actionable connect
|
|
59
|
+
* entry point rather than disappearing into the header (users weren't
|
|
60
|
+
* noticing the bare ring). Connected — the ring settles to a static accent
|
|
61
|
+
* and shows the connected
|
|
52
62
|
* wallet's own icon via `iconSrc` when the caller has one; otherwise a plain
|
|
53
63
|
* `User` glyph (never a fabricated generic avatar).
|
|
54
64
|
*/
|
|
@@ -40,15 +40,25 @@ interface NavWalletTriggerProps {
|
|
|
40
40
|
* email/social-login app) simply omit it and get the plain glyph below.
|
|
41
41
|
*/
|
|
42
42
|
iconSrc?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Override for the disconnected-state glyph (default: a plain `Wallet`
|
|
45
|
+
* icon). Apps whose actual connect entry point isn't a wallet — e.g. an
|
|
46
|
+
* email/social-login app whose users primarily sign in with Google — can
|
|
47
|
+
* pass a more accurate icon here so the ring points at what will really
|
|
48
|
+
* happen on click.
|
|
49
|
+
*/
|
|
50
|
+
disconnectedIcon?: React.ReactNode;
|
|
43
51
|
}
|
|
44
52
|
/**
|
|
45
53
|
* The right-side counterpart to `NavBrandButton` for a wallet/account entry
|
|
46
54
|
* point: the same glass pill material, with a thin brand-gradient ring
|
|
47
55
|
* traced around the rim instead of a static icon. Not connected — the ring
|
|
48
56
|
* rotates slowly (an ambient "something opens here" cue) around a plain
|
|
49
|
-
* `Wallet` glyph
|
|
50
|
-
*
|
|
51
|
-
*
|
|
57
|
+
* `Wallet` glyph (override via `disconnectedIcon` — e.g. a Google mark for an
|
|
58
|
+
* email/social-login app), so the trigger reads as an actionable connect
|
|
59
|
+
* entry point rather than disappearing into the header (users weren't
|
|
60
|
+
* noticing the bare ring). Connected — the ring settles to a static accent
|
|
61
|
+
* and shows the connected
|
|
52
62
|
* wallet's own icon via `iconSrc` when the caller has one; otherwise a plain
|
|
53
63
|
* `User` glyph (never a fabricated generic avatar).
|
|
54
64
|
*/
|
|
@@ -50,7 +50,7 @@ function NavIconButton({ onClick, className, indicator, children, ...rest }) {
|
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
const NavWalletTrigger = React.forwardRef(
|
|
53
|
-
function NavWalletTrigger2({ onClick, className, connected = false, iconSrc, ...rest }, ref) {
|
|
53
|
+
function NavWalletTrigger2({ onClick, className, connected = false, iconSrc, disconnectedIcon, ...rest }, ref) {
|
|
54
54
|
return /* @__PURE__ */ jsxs(
|
|
55
55
|
"button",
|
|
56
56
|
{
|
|
@@ -68,7 +68,7 @@ const NavWalletTrigger = React.forwardRef(
|
|
|
68
68
|
),
|
|
69
69
|
children: [
|
|
70
70
|
/* @__PURE__ */ jsx("span", { className: "ml-nav-wallet-ring", "aria-hidden": "true" }),
|
|
71
|
-
connected ? iconSrc ? /* @__PURE__ */ jsx("img", { src: iconSrc, alt: "", width: 16, height: 16, className: "h-4 w-4 shrink-0 rounded-full" }) : /* @__PURE__ */ jsx(User, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Wallet, { className: "h-3.5 w-3.5" })
|
|
71
|
+
connected ? iconSrc ? /* @__PURE__ */ jsx("img", { src: iconSrc, alt: "", width: 16, height: 16, className: "h-4 w-4 shrink-0 rounded-full" }) : /* @__PURE__ */ jsx(User, { className: "h-3.5 w-3.5" }) : disconnectedIcon ?? /* @__PURE__ */ jsx(Wallet, { className: "h-3.5 w-3.5" })
|
|
72
72
|
]
|
|
73
73
|
}
|
|
74
74
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/nav-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { User, Wallet } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\n\n// ── Header buttons ────────────────────────────────────────────────────────────\n\nexport interface NavBrandButtonProps {\n /** Defaults to opening the nav command menu (`ml:nav-open`). */\n onClick?: () => void;\n className?: string;\n \"aria-label\"?: string;\n /** Path to the brand icon image (the app's real icon asset). */\n iconSrc?: string;\n}\n\n/**\n * The main header trigger: brand mark + menu glyph in one quiet glass pill.\n * Mobile-first — a single ≥44px tap target instead of two small adjacent icons.\n * With no `onClick`, it opens the NavCommandMenu mounted elsewhere in the app.\n */\nexport function NavBrandButton({ onClick, className, iconSrc = \"/icon.png\", ...rest }: NavBrandButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick ?? (() => document.dispatchEvent(new CustomEvent(ML_NAV_OPEN)))}\n aria-label={rest[\"aria-label\"] ?? \"Open navigation\"}\n className={cn(\n \"group flex h-11 items-center gap-2 rounded-full\",\n \"bg-background/10 pl-2.5 pr-3 backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={iconSrc} alt=\"Medialane\" width={30} height={30} className=\"h-[30px] w-[30px] shrink-0\" />\n <span className=\"flex flex-col gap-[5px]\" aria-hidden=\"true\">\n <span className=\"h-[1.5px] w-4 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n <span className=\"h-[1.5px] w-2.5 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n </span>\n </button>\n );\n}\n\nexport interface NavIconButtonProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\": string;\n /** Show a small brand-colored status dot (e.g. wallet connected). */\n indicator?: boolean;\n children: React.ReactNode;\n}\n\n/**\n * Circular glass header button — the right-side counterpart to NavBrandButton\n * (wallet / account trigger).\n */\nexport function NavIconButton({ onClick, className, indicator, children, ...rest }: NavIconButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"]}\n className={cn(\n \"relative flex h-11 w-11 items-center justify-center rounded-full\",\n \"bg-background/10 text-muted-foreground backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 hover:text-foreground active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {children}\n {indicator && (\n <span className=\"absolute right-[3px] top-[3px] h-2.5 w-2.5 rounded-full border-2 border-background bg-brand-blue\" />\n )}\n </button>\n );\n}\n\nexport interface NavWalletTriggerProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\"?: string;\n /** Whether a wallet is currently connected. */\n connected?: boolean;\n /**\n * The connected wallet's own icon (e.g. Argent/Braavos/Cartridge). Optional —\n * apps with no single \"connected wallet\" identity to show (e.g. an\n * email/social-login app) simply omit it and get the plain glyph below.\n */\n iconSrc?: string;\n}\n\n/**\n * The right-side counterpart to `NavBrandButton` for a wallet/account entry\n * point: the same glass pill material, with a thin brand-gradient ring\n * traced around the rim instead of a static icon. Not connected — the ring\n * rotates slowly (an ambient \"something opens here\" cue) around a plain\n * `Wallet` glyph, so the trigger reads as \"connect your wallet\" rather than\n * disappearing into the header (users weren't noticing the bare ring).\n * Connected — the ring settles to a static accent and shows the connected\n * wallet's own icon via `iconSrc` when the caller has one; otherwise a plain\n * `User` glyph (never a fabricated generic avatar).\n */\nexport const NavWalletTrigger = React.forwardRef<HTMLButtonElement, NavWalletTriggerProps>(\n function NavWalletTrigger({ onClick, className, connected = false, iconSrc, ...rest }, ref) {\n return (\n <button\n ref={ref}\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"] ?? (connected ? \"Account\" : \"Connect wallet\")}\n className={cn(\n \"ml-nav-wallet-trigger relative flex h-8 w-8 items-center justify-center rounded-full\",\n \"bg-background/10 text-muted-foreground backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 hover:text-foreground active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n connected && \"ml-nav-wallet-trigger--connected\",\n className\n )}\n >\n <span className=\"ml-nav-wallet-ring\" aria-hidden=\"true\" />\n {connected\n ? (iconSrc\n ? <img src={iconSrc} alt=\"\" width={16} height={16} className=\"h-4 w-4 shrink-0 rounded-full\" />\n : <User className=\"h-3.5 w-3.5\" />)\n : <Wallet className=\"h-3.5 w-3.5\" />}\n </button>\n );\n }\n);\n\n// ── Account sheet ─────────────────────────────────────────────────────────────\n\nconst ML_ACCOUNT_OPEN = \"ml:account-open\";\nconst ML_ACCOUNT_CLOSE = \"ml:account-close\";\nconst ML_NAV_OPEN = \"ml:nav-open\";\nconst ML_NAV_CLOSE = \"ml:nav-close\";\n\nexport function useNavAccountSheet() {\n return {\n open: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_OPEN)),\n close: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_CLOSE)),\n };\n}\n\nexport interface NavAccountSheetProps {\n /** The app's account/wallet panel content — identity, network, disconnect, etc. */\n children: React.ReactNode;\n}\n\n/**\n * The wallet-side surface: a centered, backdrop-blurred panel — the exact\n * same overlay + card treatment as `NavCommandMenu` (`.nav-canvas-overlay`,\n * `bg-background/90 backdrop-blur-2xl backdrop-saturate-150`), so every\n * global panel in the header reads as one system. No built-in title — this\n * is an account/wallet card, not a menu; content is entirely app-owned.\n *\n * Opens via `useNavAccountSheet().open()`. Mutually exclusive with the\n * command menu: opening one closes the other, and the shared `ml:nav-close`\n * event (fired by `useNavCommandMenu().close()`) closes this too.\n */\nexport function NavAccountSheet({ children }: NavAccountSheetProps) {\n const [open, setOpen] = React.useState(false);\n\n React.useEffect(() => {\n const onOpen = () => {\n document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE));\n setOpen(true);\n };\n const onClose = () => setOpen(false);\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.addEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.addEventListener(ML_NAV_OPEN, onClose);\n document.addEventListener(ML_NAV_CLOSE, onClose);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.removeEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.removeEventListener(ML_NAV_OPEN, onClose);\n document.removeEventListener(ML_NAV_CLOSE, onClose);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, []);\n\n return (\n <AnimatePresence>\n {open && (\n <>\n {/* Backdrop blur — identical to NavCommandMenu's */}\n <motion.div\n className=\"nav-canvas-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.15 }}\n onClick={() => setOpen(false)}\n />\n\n {/* Panel — centered on desktop, bottom sheet on mobile, same as NavCommandMenu */}\n <motion.div\n className=\"fixed inset-0 z-[101] flex items-end justify-center p-3 pb-4 sm:items-center sm:p-4\"\n initial={{ opacity: 0, y: 24 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 24 }}\n transition={{ duration: 0.18, ease: \"easeOut\" }}\n onClick={() => setOpen(false)}\n >\n <div\n className=\"relative w-full max-w-[380px] overflow-hidden rounded-[20px] border border-border/40 bg-background/90 shadow-2xl backdrop-blur-2xl backdrop-saturate-150\"\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"flex justify-center pt-2.5 sm:hidden\" aria-hidden=\"true\">\n <span className=\"h-1 w-9 rounded-full bg-muted-foreground/30\" />\n </div>\n <button\n onClick={() => setOpen(false)}\n className=\"absolute right-3 top-3 rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-muted/50 hover:text-foreground\"\n aria-label=\"Close\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n </button>\n <div className=\"px-4 pb-4 pt-5 sm:pt-6\">{children}</div>\n </div>\n </motion.div>\n </>\n )}\n </AnimatePresence>\n );\n}\n"],"mappings":"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","names":["NavWalletTrigger"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/nav-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { User, Wallet } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\n\n// ── Header buttons ────────────────────────────────────────────────────────────\n\nexport interface NavBrandButtonProps {\n /** Defaults to opening the nav command menu (`ml:nav-open`). */\n onClick?: () => void;\n className?: string;\n \"aria-label\"?: string;\n /** Path to the brand icon image (the app's real icon asset). */\n iconSrc?: string;\n}\n\n/**\n * The main header trigger: brand mark + menu glyph in one quiet glass pill.\n * Mobile-first — a single ≥44px tap target instead of two small adjacent icons.\n * With no `onClick`, it opens the NavCommandMenu mounted elsewhere in the app.\n */\nexport function NavBrandButton({ onClick, className, iconSrc = \"/icon.png\", ...rest }: NavBrandButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick ?? (() => document.dispatchEvent(new CustomEvent(ML_NAV_OPEN)))}\n aria-label={rest[\"aria-label\"] ?? \"Open navigation\"}\n className={cn(\n \"group flex h-11 items-center gap-2 rounded-full\",\n \"bg-background/10 pl-2.5 pr-3 backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={iconSrc} alt=\"Medialane\" width={30} height={30} className=\"h-[30px] w-[30px] shrink-0\" />\n <span className=\"flex flex-col gap-[5px]\" aria-hidden=\"true\">\n <span className=\"h-[1.5px] w-4 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n <span className=\"h-[1.5px] w-2.5 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n </span>\n </button>\n );\n}\n\nexport interface NavIconButtonProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\": string;\n /** Show a small brand-colored status dot (e.g. wallet connected). */\n indicator?: boolean;\n children: React.ReactNode;\n}\n\n/**\n * Circular glass header button — the right-side counterpart to NavBrandButton\n * (wallet / account trigger).\n */\nexport function NavIconButton({ onClick, className, indicator, children, ...rest }: NavIconButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"]}\n className={cn(\n \"relative flex h-11 w-11 items-center justify-center rounded-full\",\n \"bg-background/10 text-muted-foreground backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 hover:text-foreground active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {children}\n {indicator && (\n <span className=\"absolute right-[3px] top-[3px] h-2.5 w-2.5 rounded-full border-2 border-background bg-brand-blue\" />\n )}\n </button>\n );\n}\n\nexport interface NavWalletTriggerProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\"?: string;\n /** Whether a wallet is currently connected. */\n connected?: boolean;\n /**\n * The connected wallet's own icon (e.g. Argent/Braavos/Cartridge). Optional —\n * apps with no single \"connected wallet\" identity to show (e.g. an\n * email/social-login app) simply omit it and get the plain glyph below.\n */\n iconSrc?: string;\n /**\n * Override for the disconnected-state glyph (default: a plain `Wallet`\n * icon). Apps whose actual connect entry point isn't a wallet — e.g. an\n * email/social-login app whose users primarily sign in with Google — can\n * pass a more accurate icon here so the ring points at what will really\n * happen on click.\n */\n disconnectedIcon?: React.ReactNode;\n}\n\n/**\n * The right-side counterpart to `NavBrandButton` for a wallet/account entry\n * point: the same glass pill material, with a thin brand-gradient ring\n * traced around the rim instead of a static icon. Not connected — the ring\n * rotates slowly (an ambient \"something opens here\" cue) around a plain\n * `Wallet` glyph (override via `disconnectedIcon` — e.g. a Google mark for an\n * email/social-login app), so the trigger reads as an actionable connect\n * entry point rather than disappearing into the header (users weren't\n * noticing the bare ring). Connected — the ring settles to a static accent\n * and shows the connected\n * wallet's own icon via `iconSrc` when the caller has one; otherwise a plain\n * `User` glyph (never a fabricated generic avatar).\n */\nexport const NavWalletTrigger = React.forwardRef<HTMLButtonElement, NavWalletTriggerProps>(\n function NavWalletTrigger({ onClick, className, connected = false, iconSrc, disconnectedIcon, ...rest }, ref) {\n return (\n <button\n ref={ref}\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"] ?? (connected ? \"Account\" : \"Connect wallet\")}\n className={cn(\n \"ml-nav-wallet-trigger relative flex h-8 w-8 items-center justify-center rounded-full\",\n \"bg-background/10 text-muted-foreground backdrop-blur-xl backdrop-saturate-150\",\n \"transition-colors hover:bg-background/20 hover:text-foreground active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n connected && \"ml-nav-wallet-trigger--connected\",\n className\n )}\n >\n <span className=\"ml-nav-wallet-ring\" aria-hidden=\"true\" />\n {connected\n ? (iconSrc\n ? <img src={iconSrc} alt=\"\" width={16} height={16} className=\"h-4 w-4 shrink-0 rounded-full\" />\n : <User className=\"h-3.5 w-3.5\" />)\n : (disconnectedIcon ?? <Wallet className=\"h-3.5 w-3.5\" />)}\n </button>\n );\n }\n);\n\n// ── Account sheet ─────────────────────────────────────────────────────────────\n\nconst ML_ACCOUNT_OPEN = \"ml:account-open\";\nconst ML_ACCOUNT_CLOSE = \"ml:account-close\";\nconst ML_NAV_OPEN = \"ml:nav-open\";\nconst ML_NAV_CLOSE = \"ml:nav-close\";\n\nexport function useNavAccountSheet() {\n return {\n open: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_OPEN)),\n close: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_CLOSE)),\n };\n}\n\nexport interface NavAccountSheetProps {\n /** The app's account/wallet panel content — identity, network, disconnect, etc. */\n children: React.ReactNode;\n}\n\n/**\n * The wallet-side surface: a centered, backdrop-blurred panel — the exact\n * same overlay + card treatment as `NavCommandMenu` (`.nav-canvas-overlay`,\n * `bg-background/90 backdrop-blur-2xl backdrop-saturate-150`), so every\n * global panel in the header reads as one system. No built-in title — this\n * is an account/wallet card, not a menu; content is entirely app-owned.\n *\n * Opens via `useNavAccountSheet().open()`. Mutually exclusive with the\n * command menu: opening one closes the other, and the shared `ml:nav-close`\n * event (fired by `useNavCommandMenu().close()`) closes this too.\n */\nexport function NavAccountSheet({ children }: NavAccountSheetProps) {\n const [open, setOpen] = React.useState(false);\n\n React.useEffect(() => {\n const onOpen = () => {\n document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE));\n setOpen(true);\n };\n const onClose = () => setOpen(false);\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.addEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.addEventListener(ML_NAV_OPEN, onClose);\n document.addEventListener(ML_NAV_CLOSE, onClose);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.removeEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.removeEventListener(ML_NAV_OPEN, onClose);\n document.removeEventListener(ML_NAV_CLOSE, onClose);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, []);\n\n return (\n <AnimatePresence>\n {open && (\n <>\n {/* Backdrop blur — identical to NavCommandMenu's */}\n <motion.div\n className=\"nav-canvas-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.15 }}\n onClick={() => setOpen(false)}\n />\n\n {/* Panel — centered on desktop, bottom sheet on mobile, same as NavCommandMenu */}\n <motion.div\n className=\"fixed inset-0 z-[101] flex items-end justify-center p-3 pb-4 sm:items-center sm:p-4\"\n initial={{ opacity: 0, y: 24 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 24 }}\n transition={{ duration: 0.18, ease: \"easeOut\" }}\n onClick={() => setOpen(false)}\n >\n <div\n className=\"relative w-full max-w-[380px] overflow-hidden rounded-[20px] border border-border/40 bg-background/90 shadow-2xl backdrop-blur-2xl backdrop-saturate-150\"\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"flex justify-center pt-2.5 sm:hidden\" aria-hidden=\"true\">\n <span className=\"h-1 w-9 rounded-full bg-muted-foreground/30\" />\n </div>\n <button\n onClick={() => setOpen(false)}\n className=\"absolute right-3 top-3 rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-muted/50 hover:text-foreground\"\n aria-label=\"Close\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n </button>\n <div className=\"px-4 pb-4 pt-5 sm:pt-6\">{children}</div>\n </div>\n </motion.div>\n </>\n )}\n </AnimatePresence>\n );\n}\n"],"mappings":"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","names":["NavWalletTrigger"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medialane/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.88.0",
|
|
4
4
|
"description": "Shared UI components for Medialane apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"tailwind-merge": ">=2.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@medialane/sdk": "0.
|
|
49
|
+
"@medialane/sdk": "0.74.0",
|
|
50
50
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
51
51
|
"@types/bun": "^1.3.14",
|
|
52
52
|
"@types/react": "^19.0.0",
|