@medialane/ui 0.94.2 → 0.94.4
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/dist/components/asset-overview-content.cjs +1 -1
- package/dist/components/asset-overview-content.cjs.map +1 -1
- package/dist/components/asset-overview-content.js +1 -1
- package/dist/components/asset-overview-content.js.map +1 -1
- 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/ip-type-display.cjs +4 -4
- package/dist/components/ip-type-display.cjs.map +1 -1
- package/dist/components/ip-type-display.js +4 -4
- package/dist/components/ip-type-display.js.map +1 -1
- package/dist/components/portfolio-bento-tile.cjs +3 -3
- package/dist/components/portfolio-bento-tile.cjs.map +1 -1
- package/dist/components/portfolio-bento-tile.d.cts +3 -3
- package/dist/components/portfolio-bento-tile.d.ts +3 -3
- package/dist/components/portfolio-bento-tile.js +3 -3
- package/dist/components/portfolio-bento-tile.js.map +1 -1
- package/dist/components/portfolio-chip-filter.cjs +3 -3
- package/dist/components/portfolio-chip-filter.cjs.map +1 -1
- package/dist/components/portfolio-chip-filter.js +3 -3
- package/dist/components/portfolio-chip-filter.js.map +1 -1
- package/dist/components/portfolio-header.cjs +1 -1
- package/dist/components/portfolio-header.cjs.map +1 -1
- package/dist/components/portfolio-header.js +1 -1
- package/dist/components/portfolio-header.js.map +1 -1
- package/dist/components/portfolio-overview.cjs +4 -1
- package/dist/components/portfolio-overview.cjs.map +1 -1
- package/dist/components/portfolio-overview.d.cts +3 -1
- package/dist/components/portfolio-overview.d.ts +3 -1
- package/dist/components/portfolio-overview.js +4 -1
- package/dist/components/portfolio-overview.js.map +1 -1
- package/package.json +1 -1
|
@@ -80,7 +80,7 @@ function AssetOverviewContent({
|
|
|
80
80
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-sm font-semibold text-muted-foreground", children: "Rights" }),
|
|
81
81
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative grid grid-cols-2 sm:grid-cols-4 gap-3 [grid-auto-flow:dense] auto-rows-[minmax(6rem,auto)]", children: [
|
|
82
82
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "col-span-2 sm:row-span-2 flex flex-col justify-between gap-4 rounded-2xl bg-gradient-to-br from-primary/10 via-primary/5 to-accent/10 ring-1 ring-primary/15 p-5", children: [
|
|
83
|
-
licenseType ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pill-badge self-start text-[10px]
|
|
83
|
+
licenseType ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pill-badge self-start text-[10px]", children: licenseType }) : null,
|
|
84
84
|
summary ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-base sm:text-lg font-semibold leading-snug text-foreground", children: summary }) : null
|
|
85
85
|
] }),
|
|
86
86
|
facts.map(({ icon, label, value }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Cell, { icon, label, value }, label))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/asset-overview-content.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Bot, Calendar, DollarSign, GitBranch, Globe, Percent, UserCheck } from \"lucide-react\";\nimport { IPTypeDisplay } from \"./ip-type-display.js\";\nimport { AddressDisplay } from \"./address-display.js\";\nimport { licenseSummary } from \"../utils/license-summary.js\";\n\ninterface AssetAttribute {\n trait_type?: string;\n value?: string;\n}\n\ninterface AssetOverviewContentProps {\n attributes: AssetAttribute[];\n hasTemplateData: boolean;\n isDisplayAttr: (attribute: AssetAttribute) => boolean;\n}\n\n// Attribute values can be numbers (OpenSea allows numeric trait `value`s), so\n// guard against non-strings — `(123).trim()` would throw and crash the page.\nconst isAddressLike = (v?: unknown): boolean =>\n typeof v === \"string\" && /^0x[0-9a-fA-F]{16,}$/.test(v.trim());\n\n/** A standard bento square: a brand-tinted icon, a plain label, a bold value.\n * `wide` stretches it across two columns for an emphasized detail. */\nfunction Cell({\n icon,\n label,\n value,\n wide,\n}: {\n icon?: ReactNode;\n label: string;\n value: ReactNode;\n wide?: boolean;\n}) {\n return (\n <div\n className={`flex flex-col justify-between gap-2 rounded-xl bg-muted/25 ring-1 ring-border/40 p-3.5 ${\n wide ? \"col-span-2\" : \"\"\n }`}\n >\n {icon ? <span className=\"text-primary/70\">{icon}</span> : null}\n <div className=\"space-y-0.5\">\n <p className=\"text-[10px] font-medium text-muted-foreground truncate\" title={label}>\n {label}\n </p>\n <div className=\"text-sm font-bold text-foreground truncate\">{value}</div>\n </div>\n </div>\n );\n}\n\n/**\n * Asset Overview tab, as a Bento 2.0 grid: an asymmetrical lattice of rounded\n * compartments on a soft brand light-leak, uniform gaps throughout. The license\n * summary is the emphasized cell (stretched 2×2, brand-gradient wash + license\n * stamp + emerald trust seal); each right is a standard square; Details follow.\n * Uses only design-system tokens (brand-* / primary / aurora / emerald-for-trust).\n */\nexport function AssetOverviewContent({\n attributes,\n hasTemplateData,\n isDisplayAttr,\n}: AssetOverviewContentProps) {\n const attr = (trait: string) => attributes.find((attribute) => attribute.trait_type === trait)?.value;\n const licenseType = attr(\"License\");\n const commercialUse = attr(\"Commercial Use\");\n const derivatives = attr(\"Derivatives\");\n const attribution = attr(\"Attribution\");\n const territory = attr(\"Territory\");\n const aiPolicy = attr(\"AI Policy\");\n const royalty = attr(\"Royalty\");\n const registration = attr(\"Registration\");\n const summary = licenseSummary(attributes);\n const hasLicenseData = !!(licenseType || commercialUse || derivatives || attribution);\n const displayAttributes = attributes.filter((attribute) => isDisplayAttr(attribute));\n\n const facts = [\n { icon: <DollarSign className=\"h-4 w-4\" />, label: \"Commercial use\", value: commercialUse },\n { icon: <GitBranch className=\"h-4 w-4\" />, label: \"Derivatives\", value: derivatives },\n { icon: <UserCheck className=\"h-4 w-4\" />, label: \"Attribution\", value: attribution },\n { icon: <Globe className=\"h-4 w-4\" />, label: \"Territory\", value: territory },\n { icon: <Bot className=\"h-4 w-4\" />, label: \"AI & data mining\", value: aiPolicy },\n { icon: <Percent className=\"h-4 w-4\" />, label: \"Royalty\", value: royalty },\n { icon: <Calendar className=\"h-4 w-4\" />, label: \"Registered\", value: registration },\n ].filter((row) => !!row.value);\n\n return (\n <div className=\"mt-4 space-y-7\">\n {hasTemplateData ? <IPTypeDisplay attributes={attributes} /> : null}\n\n {hasLicenseData ? (\n <section className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-muted-foreground\">Rights</h3>\n <div className=\"relative\">\n\n <div className=\"relative grid grid-cols-2 sm:grid-cols-4 gap-3 [grid-auto-flow:dense] auto-rows-[minmax(6rem,auto)]\">\n {/* emphasized cell — the license summary, stretched 2×2 */}\n <div className=\"col-span-2 sm:row-span-2 flex flex-col justify-between gap-4 rounded-2xl bg-gradient-to-br from-primary/10 via-primary/5 to-accent/10 ring-1 ring-primary/15 p-5\">\n {licenseType ? (\n <span className=\"pill-badge self-start text-[10px]
|
|
1
|
+
{"version":3,"sources":["../../src/components/asset-overview-content.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Bot, Calendar, DollarSign, GitBranch, Globe, Percent, UserCheck } from \"lucide-react\";\nimport { IPTypeDisplay } from \"./ip-type-display.js\";\nimport { AddressDisplay } from \"./address-display.js\";\nimport { licenseSummary } from \"../utils/license-summary.js\";\n\ninterface AssetAttribute {\n trait_type?: string;\n value?: string;\n}\n\ninterface AssetOverviewContentProps {\n attributes: AssetAttribute[];\n hasTemplateData: boolean;\n isDisplayAttr: (attribute: AssetAttribute) => boolean;\n}\n\n// Attribute values can be numbers (OpenSea allows numeric trait `value`s), so\n// guard against non-strings — `(123).trim()` would throw and crash the page.\nconst isAddressLike = (v?: unknown): boolean =>\n typeof v === \"string\" && /^0x[0-9a-fA-F]{16,}$/.test(v.trim());\n\n/** A standard bento square: a brand-tinted icon, a plain label, a bold value.\n * `wide` stretches it across two columns for an emphasized detail. */\nfunction Cell({\n icon,\n label,\n value,\n wide,\n}: {\n icon?: ReactNode;\n label: string;\n value: ReactNode;\n wide?: boolean;\n}) {\n return (\n <div\n className={`flex flex-col justify-between gap-2 rounded-xl bg-muted/25 ring-1 ring-border/40 p-3.5 ${\n wide ? \"col-span-2\" : \"\"\n }`}\n >\n {icon ? <span className=\"text-primary/70\">{icon}</span> : null}\n <div className=\"space-y-0.5\">\n <p className=\"text-[10px] font-medium text-muted-foreground truncate\" title={label}>\n {label}\n </p>\n <div className=\"text-sm font-bold text-foreground truncate\">{value}</div>\n </div>\n </div>\n );\n}\n\n/**\n * Asset Overview tab, as a Bento 2.0 grid: an asymmetrical lattice of rounded\n * compartments on a soft brand light-leak, uniform gaps throughout. The license\n * summary is the emphasized cell (stretched 2×2, brand-gradient wash + license\n * stamp + emerald trust seal); each right is a standard square; Details follow.\n * Uses only design-system tokens (brand-* / primary / aurora / emerald-for-trust).\n */\nexport function AssetOverviewContent({\n attributes,\n hasTemplateData,\n isDisplayAttr,\n}: AssetOverviewContentProps) {\n const attr = (trait: string) => attributes.find((attribute) => attribute.trait_type === trait)?.value;\n const licenseType = attr(\"License\");\n const commercialUse = attr(\"Commercial Use\");\n const derivatives = attr(\"Derivatives\");\n const attribution = attr(\"Attribution\");\n const territory = attr(\"Territory\");\n const aiPolicy = attr(\"AI Policy\");\n const royalty = attr(\"Royalty\");\n const registration = attr(\"Registration\");\n const summary = licenseSummary(attributes);\n const hasLicenseData = !!(licenseType || commercialUse || derivatives || attribution);\n const displayAttributes = attributes.filter((attribute) => isDisplayAttr(attribute));\n\n const facts = [\n { icon: <DollarSign className=\"h-4 w-4\" />, label: \"Commercial use\", value: commercialUse },\n { icon: <GitBranch className=\"h-4 w-4\" />, label: \"Derivatives\", value: derivatives },\n { icon: <UserCheck className=\"h-4 w-4\" />, label: \"Attribution\", value: attribution },\n { icon: <Globe className=\"h-4 w-4\" />, label: \"Territory\", value: territory },\n { icon: <Bot className=\"h-4 w-4\" />, label: \"AI & data mining\", value: aiPolicy },\n { icon: <Percent className=\"h-4 w-4\" />, label: \"Royalty\", value: royalty },\n { icon: <Calendar className=\"h-4 w-4\" />, label: \"Registered\", value: registration },\n ].filter((row) => !!row.value);\n\n return (\n <div className=\"mt-4 space-y-7\">\n {hasTemplateData ? <IPTypeDisplay attributes={attributes} /> : null}\n\n {hasLicenseData ? (\n <section className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-muted-foreground\">Rights</h3>\n <div className=\"relative\">\n\n <div className=\"relative grid grid-cols-2 sm:grid-cols-4 gap-3 [grid-auto-flow:dense] auto-rows-[minmax(6rem,auto)]\">\n {/* emphasized cell — the license summary, stretched 2×2 */}\n <div className=\"col-span-2 sm:row-span-2 flex flex-col justify-between gap-4 rounded-2xl bg-gradient-to-br from-primary/10 via-primary/5 to-accent/10 ring-1 ring-primary/15 p-5\">\n {licenseType ? (\n <span className=\"pill-badge self-start text-[10px]\">{licenseType}</span>\n ) : null}\n {summary ? (\n <p className=\"text-base sm:text-lg font-semibold leading-snug text-foreground\">{summary}</p>\n ) : null}\n </div>\n\n {facts.map(({ icon, label, value }) => (\n <Cell key={label} icon={icon} label={label} value={value} />\n ))}\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground/70\">\n Kept in permanent, tamper-proof storage · recognized under international copyright law\n </p>\n </section>\n ) : null}\n\n {displayAttributes.length > 0 ? (\n <section className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-muted-foreground\">Details</h3>\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3 [grid-auto-flow:dense] auto-rows-[minmax(6rem,auto)]\">\n {displayAttributes.map((attribute, index) => (\n <Cell\n key={index}\n label={attribute.trait_type ?? \"Trait\"}\n wide={isAddressLike(attribute.value)}\n value={\n isAddressLike(attribute.value) ? (\n <AddressDisplay address={attribute.value!} chars={4} className=\"text-sm font-bold\" />\n ) : (\n attribute.value ?? \"—\"\n )\n }\n />\n ))}\n </div>\n </section>\n ) : null}\n\n {!hasTemplateData && !hasLicenseData && displayAttributes.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">No additional details available.</p>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -57,7 +57,7 @@ function AssetOverviewContent({
|
|
|
57
57
|
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-muted-foreground", children: "Rights" }),
|
|
58
58
|
/* @__PURE__ */ jsx("div", { className: "relative", children: /* @__PURE__ */ jsxs("div", { className: "relative grid grid-cols-2 sm:grid-cols-4 gap-3 [grid-auto-flow:dense] auto-rows-[minmax(6rem,auto)]", children: [
|
|
59
59
|
/* @__PURE__ */ jsxs("div", { className: "col-span-2 sm:row-span-2 flex flex-col justify-between gap-4 rounded-2xl bg-gradient-to-br from-primary/10 via-primary/5 to-accent/10 ring-1 ring-primary/15 p-5", children: [
|
|
60
|
-
licenseType ? /* @__PURE__ */ jsx("span", { className: "pill-badge self-start text-[10px]
|
|
60
|
+
licenseType ? /* @__PURE__ */ jsx("span", { className: "pill-badge self-start text-[10px]", children: licenseType }) : null,
|
|
61
61
|
summary ? /* @__PURE__ */ jsx("p", { className: "text-base sm:text-lg font-semibold leading-snug text-foreground", children: summary }) : null
|
|
62
62
|
] }),
|
|
63
63
|
facts.map(({ icon, label, value }) => /* @__PURE__ */ jsx(Cell, { icon, label, value }, label))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/asset-overview-content.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Bot, Calendar, DollarSign, GitBranch, Globe, Percent, UserCheck } from \"lucide-react\";\nimport { IPTypeDisplay } from \"./ip-type-display.js\";\nimport { AddressDisplay } from \"./address-display.js\";\nimport { licenseSummary } from \"../utils/license-summary.js\";\n\ninterface AssetAttribute {\n trait_type?: string;\n value?: string;\n}\n\ninterface AssetOverviewContentProps {\n attributes: AssetAttribute[];\n hasTemplateData: boolean;\n isDisplayAttr: (attribute: AssetAttribute) => boolean;\n}\n\n// Attribute values can be numbers (OpenSea allows numeric trait `value`s), so\n// guard against non-strings — `(123).trim()` would throw and crash the page.\nconst isAddressLike = (v?: unknown): boolean =>\n typeof v === \"string\" && /^0x[0-9a-fA-F]{16,}$/.test(v.trim());\n\n/** A standard bento square: a brand-tinted icon, a plain label, a bold value.\n * `wide` stretches it across two columns for an emphasized detail. */\nfunction Cell({\n icon,\n label,\n value,\n wide,\n}: {\n icon?: ReactNode;\n label: string;\n value: ReactNode;\n wide?: boolean;\n}) {\n return (\n <div\n className={`flex flex-col justify-between gap-2 rounded-xl bg-muted/25 ring-1 ring-border/40 p-3.5 ${\n wide ? \"col-span-2\" : \"\"\n }`}\n >\n {icon ? <span className=\"text-primary/70\">{icon}</span> : null}\n <div className=\"space-y-0.5\">\n <p className=\"text-[10px] font-medium text-muted-foreground truncate\" title={label}>\n {label}\n </p>\n <div className=\"text-sm font-bold text-foreground truncate\">{value}</div>\n </div>\n </div>\n );\n}\n\n/**\n * Asset Overview tab, as a Bento 2.0 grid: an asymmetrical lattice of rounded\n * compartments on a soft brand light-leak, uniform gaps throughout. The license\n * summary is the emphasized cell (stretched 2×2, brand-gradient wash + license\n * stamp + emerald trust seal); each right is a standard square; Details follow.\n * Uses only design-system tokens (brand-* / primary / aurora / emerald-for-trust).\n */\nexport function AssetOverviewContent({\n attributes,\n hasTemplateData,\n isDisplayAttr,\n}: AssetOverviewContentProps) {\n const attr = (trait: string) => attributes.find((attribute) => attribute.trait_type === trait)?.value;\n const licenseType = attr(\"License\");\n const commercialUse = attr(\"Commercial Use\");\n const derivatives = attr(\"Derivatives\");\n const attribution = attr(\"Attribution\");\n const territory = attr(\"Territory\");\n const aiPolicy = attr(\"AI Policy\");\n const royalty = attr(\"Royalty\");\n const registration = attr(\"Registration\");\n const summary = licenseSummary(attributes);\n const hasLicenseData = !!(licenseType || commercialUse || derivatives || attribution);\n const displayAttributes = attributes.filter((attribute) => isDisplayAttr(attribute));\n\n const facts = [\n { icon: <DollarSign className=\"h-4 w-4\" />, label: \"Commercial use\", value: commercialUse },\n { icon: <GitBranch className=\"h-4 w-4\" />, label: \"Derivatives\", value: derivatives },\n { icon: <UserCheck className=\"h-4 w-4\" />, label: \"Attribution\", value: attribution },\n { icon: <Globe className=\"h-4 w-4\" />, label: \"Territory\", value: territory },\n { icon: <Bot className=\"h-4 w-4\" />, label: \"AI & data mining\", value: aiPolicy },\n { icon: <Percent className=\"h-4 w-4\" />, label: \"Royalty\", value: royalty },\n { icon: <Calendar className=\"h-4 w-4\" />, label: \"Registered\", value: registration },\n ].filter((row) => !!row.value);\n\n return (\n <div className=\"mt-4 space-y-7\">\n {hasTemplateData ? <IPTypeDisplay attributes={attributes} /> : null}\n\n {hasLicenseData ? (\n <section className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-muted-foreground\">Rights</h3>\n <div className=\"relative\">\n\n <div className=\"relative grid grid-cols-2 sm:grid-cols-4 gap-3 [grid-auto-flow:dense] auto-rows-[minmax(6rem,auto)]\">\n {/* emphasized cell — the license summary, stretched 2×2 */}\n <div className=\"col-span-2 sm:row-span-2 flex flex-col justify-between gap-4 rounded-2xl bg-gradient-to-br from-primary/10 via-primary/5 to-accent/10 ring-1 ring-primary/15 p-5\">\n {licenseType ? (\n <span className=\"pill-badge self-start text-[10px]
|
|
1
|
+
{"version":3,"sources":["../../src/components/asset-overview-content.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Bot, Calendar, DollarSign, GitBranch, Globe, Percent, UserCheck } from \"lucide-react\";\nimport { IPTypeDisplay } from \"./ip-type-display.js\";\nimport { AddressDisplay } from \"./address-display.js\";\nimport { licenseSummary } from \"../utils/license-summary.js\";\n\ninterface AssetAttribute {\n trait_type?: string;\n value?: string;\n}\n\ninterface AssetOverviewContentProps {\n attributes: AssetAttribute[];\n hasTemplateData: boolean;\n isDisplayAttr: (attribute: AssetAttribute) => boolean;\n}\n\n// Attribute values can be numbers (OpenSea allows numeric trait `value`s), so\n// guard against non-strings — `(123).trim()` would throw and crash the page.\nconst isAddressLike = (v?: unknown): boolean =>\n typeof v === \"string\" && /^0x[0-9a-fA-F]{16,}$/.test(v.trim());\n\n/** A standard bento square: a brand-tinted icon, a plain label, a bold value.\n * `wide` stretches it across two columns for an emphasized detail. */\nfunction Cell({\n icon,\n label,\n value,\n wide,\n}: {\n icon?: ReactNode;\n label: string;\n value: ReactNode;\n wide?: boolean;\n}) {\n return (\n <div\n className={`flex flex-col justify-between gap-2 rounded-xl bg-muted/25 ring-1 ring-border/40 p-3.5 ${\n wide ? \"col-span-2\" : \"\"\n }`}\n >\n {icon ? <span className=\"text-primary/70\">{icon}</span> : null}\n <div className=\"space-y-0.5\">\n <p className=\"text-[10px] font-medium text-muted-foreground truncate\" title={label}>\n {label}\n </p>\n <div className=\"text-sm font-bold text-foreground truncate\">{value}</div>\n </div>\n </div>\n );\n}\n\n/**\n * Asset Overview tab, as a Bento 2.0 grid: an asymmetrical lattice of rounded\n * compartments on a soft brand light-leak, uniform gaps throughout. The license\n * summary is the emphasized cell (stretched 2×2, brand-gradient wash + license\n * stamp + emerald trust seal); each right is a standard square; Details follow.\n * Uses only design-system tokens (brand-* / primary / aurora / emerald-for-trust).\n */\nexport function AssetOverviewContent({\n attributes,\n hasTemplateData,\n isDisplayAttr,\n}: AssetOverviewContentProps) {\n const attr = (trait: string) => attributes.find((attribute) => attribute.trait_type === trait)?.value;\n const licenseType = attr(\"License\");\n const commercialUse = attr(\"Commercial Use\");\n const derivatives = attr(\"Derivatives\");\n const attribution = attr(\"Attribution\");\n const territory = attr(\"Territory\");\n const aiPolicy = attr(\"AI Policy\");\n const royalty = attr(\"Royalty\");\n const registration = attr(\"Registration\");\n const summary = licenseSummary(attributes);\n const hasLicenseData = !!(licenseType || commercialUse || derivatives || attribution);\n const displayAttributes = attributes.filter((attribute) => isDisplayAttr(attribute));\n\n const facts = [\n { icon: <DollarSign className=\"h-4 w-4\" />, label: \"Commercial use\", value: commercialUse },\n { icon: <GitBranch className=\"h-4 w-4\" />, label: \"Derivatives\", value: derivatives },\n { icon: <UserCheck className=\"h-4 w-4\" />, label: \"Attribution\", value: attribution },\n { icon: <Globe className=\"h-4 w-4\" />, label: \"Territory\", value: territory },\n { icon: <Bot className=\"h-4 w-4\" />, label: \"AI & data mining\", value: aiPolicy },\n { icon: <Percent className=\"h-4 w-4\" />, label: \"Royalty\", value: royalty },\n { icon: <Calendar className=\"h-4 w-4\" />, label: \"Registered\", value: registration },\n ].filter((row) => !!row.value);\n\n return (\n <div className=\"mt-4 space-y-7\">\n {hasTemplateData ? <IPTypeDisplay attributes={attributes} /> : null}\n\n {hasLicenseData ? (\n <section className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-muted-foreground\">Rights</h3>\n <div className=\"relative\">\n\n <div className=\"relative grid grid-cols-2 sm:grid-cols-4 gap-3 [grid-auto-flow:dense] auto-rows-[minmax(6rem,auto)]\">\n {/* emphasized cell — the license summary, stretched 2×2 */}\n <div className=\"col-span-2 sm:row-span-2 flex flex-col justify-between gap-4 rounded-2xl bg-gradient-to-br from-primary/10 via-primary/5 to-accent/10 ring-1 ring-primary/15 p-5\">\n {licenseType ? (\n <span className=\"pill-badge self-start text-[10px]\">{licenseType}</span>\n ) : null}\n {summary ? (\n <p className=\"text-base sm:text-lg font-semibold leading-snug text-foreground\">{summary}</p>\n ) : null}\n </div>\n\n {facts.map(({ icon, label, value }) => (\n <Cell key={label} icon={icon} label={label} value={value} />\n ))}\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground/70\">\n Kept in permanent, tamper-proof storage · recognized under international copyright law\n </p>\n </section>\n ) : null}\n\n {displayAttributes.length > 0 ? (\n <section className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-muted-foreground\">Details</h3>\n <div className=\"grid grid-cols-2 sm:grid-cols-4 gap-3 [grid-auto-flow:dense] auto-rows-[minmax(6rem,auto)]\">\n {displayAttributes.map((attribute, index) => (\n <Cell\n key={index}\n label={attribute.trait_type ?? \"Trait\"}\n wide={isAddressLike(attribute.value)}\n value={\n isAddressLike(attribute.value) ? (\n <AddressDisplay address={attribute.value!} chars={4} className=\"text-sm font-bold\" />\n ) : (\n attribute.value ?? \"—\"\n )\n }\n />\n ))}\n </div>\n </section>\n ) : null}\n\n {!hasTemplateData && !hasLicenseData && displayAttributes.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">No additional details available.</p>\n ) : null}\n </div>\n );\n}\n"],"mappings":";AA2Cc,cACR,YADQ;AAxCd,SAAS,KAAK,UAAU,YAAY,WAAW,OAAO,SAAS,iBAAiB;AAChF,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,sBAAsB;AAe/B,MAAM,gBAAgB,CAAC,MACrB,OAAO,MAAM,YAAY,uBAAuB,KAAK,EAAE,KAAK,CAAC;AAI/D,SAAS,KAAK;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,0FACT,OAAO,eAAe,EACxB;AAAA,MAEC;AAAA,eAAO,oBAAC,UAAK,WAAU,mBAAmB,gBAAK,IAAU;AAAA,QAC1D,qBAAC,SAAI,WAAU,eACb;AAAA,8BAAC,OAAE,WAAU,0DAAyD,OAAO,OAC1E,iBACH;AAAA,UACA,oBAAC,SAAI,WAAU,8CAA8C,iBAAM;AAAA,WACrE;AAAA;AAAA;AAAA,EACF;AAEJ;AASO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AACF,GAA8B;AAC5B,QAAM,OAAO,CAAC,UAAkB,WAAW,KAAK,CAAC,cAAc,UAAU,eAAe,KAAK,GAAG;AAChG,QAAM,cAAc,KAAK,SAAS;AAClC,QAAM,gBAAgB,KAAK,gBAAgB;AAC3C,QAAM,cAAc,KAAK,aAAa;AACtC,QAAM,cAAc,KAAK,aAAa;AACtC,QAAM,YAAY,KAAK,WAAW;AAClC,QAAM,WAAW,KAAK,WAAW;AACjC,QAAM,UAAU,KAAK,SAAS;AAC9B,QAAM,eAAe,KAAK,cAAc;AACxC,QAAM,UAAU,eAAe,UAAU;AACzC,QAAM,iBAAiB,CAAC,EAAE,eAAe,iBAAiB,eAAe;AACzE,QAAM,oBAAoB,WAAW,OAAO,CAAC,cAAc,cAAc,SAAS,CAAC;AAEnF,QAAM,QAAQ;AAAA,IACZ,EAAE,MAAM,oBAAC,cAAW,WAAU,WAAU,GAAI,OAAO,kBAAkB,OAAO,cAAc;AAAA,IAC1F,EAAE,MAAM,oBAAC,aAAU,WAAU,WAAU,GAAI,OAAO,eAAe,OAAO,YAAY;AAAA,IACpF,EAAE,MAAM,oBAAC,aAAU,WAAU,WAAU,GAAI,OAAO,eAAe,OAAO,YAAY;AAAA,IACpF,EAAE,MAAM,oBAAC,SAAM,WAAU,WAAU,GAAI,OAAO,aAAa,OAAO,UAAU;AAAA,IAC5E,EAAE,MAAM,oBAAC,OAAI,WAAU,WAAU,GAAI,OAAO,oBAAoB,OAAO,SAAS;AAAA,IAChF,EAAE,MAAM,oBAAC,WAAQ,WAAU,WAAU,GAAI,OAAO,WAAW,OAAO,QAAQ;AAAA,IAC1E,EAAE,MAAM,oBAAC,YAAS,WAAU,WAAU,GAAI,OAAO,cAAc,OAAO,aAAa;AAAA,EACrF,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,KAAK;AAE7B,SACE,qBAAC,SAAI,WAAU,kBACZ;AAAA,sBAAkB,oBAAC,iBAAc,YAAwB,IAAK;AAAA,IAE9D,iBACC,qBAAC,aAAQ,WAAU,aACjB;AAAA,0BAAC,QAAG,WAAU,+CAA8C,oBAAM;AAAA,MAClE,oBAAC,SAAI,WAAU,YAEb,+BAAC,SAAI,WAAU,uGAEb;AAAA,6BAAC,SAAI,WAAU,oKACZ;AAAA,wBACC,oBAAC,UAAK,WAAU,qCAAqC,uBAAY,IAC/D;AAAA,UACH,UACC,oBAAC,OAAE,WAAU,mEAAmE,mBAAQ,IACtF;AAAA,WACN;AAAA,QAEC,MAAM,IAAI,CAAC,EAAE,MAAM,OAAO,MAAM,MAC/B,oBAAC,QAAiB,MAAY,OAAc,SAAjC,KAA+C,CAC3D;AAAA,SACH,GACF;AAAA,MACA,oBAAC,OAAE,WAAU,oCAAmC,uGAEhD;AAAA,OACF,IACE;AAAA,IAEH,kBAAkB,SAAS,IAC1B,qBAAC,aAAQ,WAAU,aACjB;AAAA,0BAAC,QAAG,WAAU,+CAA8C,qBAAO;AAAA,MACnE,oBAAC,SAAI,WAAU,8FACZ,4BAAkB,IAAI,CAAC,WAAW,UACjC;AAAA,QAAC;AAAA;AAAA,UAEC,OAAO,UAAU,cAAc;AAAA,UAC/B,MAAM,cAAc,UAAU,KAAK;AAAA,UACnC,OACE,cAAc,UAAU,KAAK,IAC3B,oBAAC,kBAAe,SAAS,UAAU,OAAQ,OAAO,GAAG,WAAU,qBAAoB,IAEnF,UAAU,SAAS;AAAA;AAAA,QAPlB;AAAA,MAUP,CACD,GACH;AAAA,OACF,IACE;AAAA,IAEH,CAAC,mBAAmB,CAAC,kBAAkB,kBAAkB,WAAW,IACnE,oBAAC,OAAE,WAAU,iCAAgC,8CAAgC,IAC3E;AAAA,KACN;AAEJ;","names":[]}
|
|
@@ -63,7 +63,7 @@ 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
|
|
66
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm font-semibold", children: "Coins" })
|
|
67
67
|
] }),
|
|
68
68
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-3xl", children: "Creator coins & memecoins" })
|
|
69
69
|
] }),
|
|
@@ -151,7 +151,7 @@ function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
|
|
|
151
151
|
}
|
|
152
152
|
function FilterGroup({ label, children }) {
|
|
153
153
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-2", children: [
|
|
154
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-[10px] font-
|
|
154
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-[10px] font-medium text-muted-foreground", children: label }),
|
|
155
155
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-1.5", children })
|
|
156
156
|
] });
|
|
157
157
|
}
|
|
@@ -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\">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":[]}
|
|
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\">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-medium 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,7 +40,7 @@ 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
|
|
43
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: "Coins" })
|
|
44
44
|
] }),
|
|
45
45
|
/* @__PURE__ */ jsx("h1", { className: "text-3xl", children: "Creator coins & memecoins" })
|
|
46
46
|
] }),
|
|
@@ -128,7 +128,7 @@ function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
|
|
|
128
128
|
}
|
|
129
129
|
function FilterGroup({ label, children }) {
|
|
130
130
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
131
|
-
/* @__PURE__ */ jsx("p", { className: "text-[10px] font-
|
|
131
|
+
/* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium text-muted-foreground", children: label }),
|
|
132
132
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children })
|
|
133
133
|
] });
|
|
134
134
|
}
|
|
@@ -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\">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":[]}
|
|
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\">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-medium 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":[]}
|
|
@@ -130,7 +130,7 @@ function IPTypeDisplay({ attributes }) {
|
|
|
130
130
|
if (embeds.length === 0 && socials.length === 0 && !docUri) return null;
|
|
131
131
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-5", children: [
|
|
132
132
|
docUri && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1.5", children: [
|
|
133
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-
|
|
133
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-medium text-muted-foreground", children: "Document" }),
|
|
134
134
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
135
135
|
"a",
|
|
136
136
|
{
|
|
@@ -156,7 +156,7 @@ function IPTypeDisplay({ attributes }) {
|
|
|
156
156
|
const src = getEmbedSrc(platform, value);
|
|
157
157
|
if (src) {
|
|
158
158
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1.5", children: [
|
|
159
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-
|
|
159
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-medium text-muted-foreground", children: meta.label }),
|
|
160
160
|
COMPACT[platform] ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
161
161
|
"iframe",
|
|
162
162
|
{
|
|
@@ -181,7 +181,7 @@ function IPTypeDisplay({ attributes }) {
|
|
|
181
181
|
] }, platform);
|
|
182
182
|
}
|
|
183
183
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
184
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-
|
|
184
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-medium text-muted-foreground mb-1", children: meta.label }),
|
|
185
185
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
186
186
|
"a",
|
|
187
187
|
{
|
|
@@ -198,7 +198,7 @@ function IPTypeDisplay({ attributes }) {
|
|
|
198
198
|
] }, platform);
|
|
199
199
|
}),
|
|
200
200
|
socials.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1.5", children: [
|
|
201
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-
|
|
201
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-medium text-muted-foreground", children: "Links" }),
|
|
202
202
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-2", children: socials.map(({ platform, meta, value }) => {
|
|
203
203
|
const SIcon = meta.icon;
|
|
204
204
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/ip-type-display.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IPType } from \"../data/ip.js\";\nimport {\n IP_TEMPLATES,\n EMBED_PLATFORM_META,\n SOCIAL_PLATFORM_META,\n type EmbedPlatform,\n} from \"../data/ip-templates.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { ExternalLink, FileText, ShieldCheck } from \"lucide-react\";\n\ninterface Attr {\n trait_type?: string | null;\n value?: string | null;\n}\n\ninterface IPTypeDisplayProps {\n attributes: Attr[] | null | undefined;\n}\n\n// ── Embed URL parsers ────────────────────────────────────────────────────────\n\nfunction parseYouTubeEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n let id: string | null = null;\n if (u.hostname.includes(\"youtu.be\")) {\n id = u.pathname.slice(1);\n } else if (u.hostname.includes(\"youtube.com\")) {\n id = u.searchParams.get(\"v\");\n }\n if (!id) return null;\n return `https://www.youtube.com/embed/${id}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSpotifyEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"spotify.com\")) return null;\n // Spotify's embed endpoint only accepts /embed/{type}/{id}. Isolate the\n // resource type + id so locale prefixes (e.g. /intl-pt) and trailing query\n // params don't leak through — `/embed/intl-pt/album/…` 404s on Spotify.\n const match = u.pathname.match(\n /(track|album|playlist|episode|show|artist)\\/([A-Za-z0-9]+)/\n );\n if (!match) return null;\n return `https://open.spotify.com/embed/${match[1]}/${match[2]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSoundCloudEmbed(url: string): string | null {\n try {\n new URL(url); // validate\n if (!url.includes(\"soundcloud.com\")) return null;\n const encoded = encodeURIComponent(url);\n return `https://w.soundcloud.com/player/?url=${encoded}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false`;\n } catch {\n return null;\n }\n}\n\nfunction parseTikTokEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"tiktok.com\")) return null;\n const match = u.pathname.match(/\\/video\\/(\\d+)/);\n if (!match) return null;\n return `https://www.tiktok.com/embed/v2/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseVimeoEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"vimeo.com\")) return null;\n const match = u.pathname.match(/(\\d+)/);\n if (!match) return null;\n return `https://player.vimeo.com/video/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction getEmbedSrc(platform: EmbedPlatform, value: string): string | null {\n switch (platform) {\n case \"youtube\": return parseYouTubeEmbed(value);\n case \"spotify\": return parseSpotifyEmbed(value);\n case \"soundcloud\": return parseSoundCloudEmbed(value);\n case \"tiktok\": return parseTikTokEmbed(value);\n case \"vimeo\": return parseVimeoEmbed(value);\n }\n}\n\n// Compact iframe (fixed height) vs 16:9 video frame.\nconst COMPACT: Record<EmbedPlatform, boolean> = {\n spotify: true,\n soundcloud: true,\n youtube: false,\n tiktok: false,\n vimeo: false,\n};\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function IPTypeDisplay({ attributes }: IPTypeDisplayProps) {\n const attrs = attributes ?? [];\n\n const ipType = attrs.find(\n (a) => a.trait_type?.toLowerCase() === \"ip type\"\n )?.value as IPType | undefined;\n if (!ipType) return null;\n\n const template = IP_TEMPLATES[ipType];\n if (!template) return null;\n\n const getAttr = (key: string) =>\n attrs.find((a) => a.trait_type === key)?.value ?? null;\n\n const embeds = (template.embeds ?? []).flatMap((platform) => {\n const meta = EMBED_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const socials = (template.socials ?? []).flatMap((platform) => {\n const meta = SOCIAL_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const docUri = template.docUpload ? getAttr(template.docUpload.traitType) : null;\n\n if (embeds.length === 0 && socials.length === 0 && !docUri) return null;\n\n return (\n <div className=\"space-y-5\">\n {/* Document pinned to IPFS — immutable, timestamped copy of the work */}\n {docUri && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Document\n </p>\n <a\n href={docUri.startsWith(\"ipfs://\") ? ipfsToHttp(docUri) : docUri}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-3 transition-colors hover:border-primary/40 group\"\n >\n <FileText className=\"h-5 w-5 text-primary shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold group-hover:text-primary transition-colors\">\n View document\n </p>\n <p className=\"text-xs text-muted-foreground flex items-center gap-1 mt-0.5\">\n <ShieldCheck className=\"h-3 w-3 shrink-0\" />\n Stored on IPFS — immutable, timestamped copy of the original\n </p>\n </div>\n <ExternalLink className=\"h-3.5 w-3.5 text-muted-foreground shrink-0\" />\n </a>\n </div>\n )}\n {embeds.map(({ platform, meta, value }) => {\n const src = getEmbedSrc(platform, value);\n if (src) {\n return (\n <div key={platform} className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n {meta.label}\n </p>\n {COMPACT[platform] ? (\n <iframe\n src={src}\n className=\"w-full rounded-xl border-0\"\n height={166}\n allow=\"autoplay\"\n loading=\"lazy\"\n title={meta.label}\n />\n ) : (\n <div className=\"relative w-full aspect-video rounded-xl overflow-hidden bg-muted/20\">\n <iframe\n src={src}\n className=\"absolute inset-0 w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n title={meta.label}\n />\n </div>\n )}\n </div>\n );\n }\n // Fallback: plain external link if URL parsing failed\n return (\n <div key={platform}>\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1\">\n {meta.label}\n </p>\n <a\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 text-sm text-primary hover:underline\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n Open link\n </a>\n </div>\n );\n })}\n\n {socials.length > 0 && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Links\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {socials.map(({ platform, meta, value }) => {\n const SIcon = meta.icon;\n return (\n <a\n key={platform}\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground\"\n >\n <SIcon className=\"h-3.5 w-3.5\" />\n {meta.label}\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/ip-type-display.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IPType } from \"../data/ip.js\";\nimport {\n IP_TEMPLATES,\n EMBED_PLATFORM_META,\n SOCIAL_PLATFORM_META,\n type EmbedPlatform,\n} from \"../data/ip-templates.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { ExternalLink, FileText, ShieldCheck } from \"lucide-react\";\n\ninterface Attr {\n trait_type?: string | null;\n value?: string | null;\n}\n\ninterface IPTypeDisplayProps {\n attributes: Attr[] | null | undefined;\n}\n\n// ── Embed URL parsers ────────────────────────────────────────────────────────\n\nfunction parseYouTubeEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n let id: string | null = null;\n if (u.hostname.includes(\"youtu.be\")) {\n id = u.pathname.slice(1);\n } else if (u.hostname.includes(\"youtube.com\")) {\n id = u.searchParams.get(\"v\");\n }\n if (!id) return null;\n return `https://www.youtube.com/embed/${id}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSpotifyEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"spotify.com\")) return null;\n // Spotify's embed endpoint only accepts /embed/{type}/{id}. Isolate the\n // resource type + id so locale prefixes (e.g. /intl-pt) and trailing query\n // params don't leak through — `/embed/intl-pt/album/…` 404s on Spotify.\n const match = u.pathname.match(\n /(track|album|playlist|episode|show|artist)\\/([A-Za-z0-9]+)/\n );\n if (!match) return null;\n return `https://open.spotify.com/embed/${match[1]}/${match[2]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSoundCloudEmbed(url: string): string | null {\n try {\n new URL(url); // validate\n if (!url.includes(\"soundcloud.com\")) return null;\n const encoded = encodeURIComponent(url);\n return `https://w.soundcloud.com/player/?url=${encoded}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false`;\n } catch {\n return null;\n }\n}\n\nfunction parseTikTokEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"tiktok.com\")) return null;\n const match = u.pathname.match(/\\/video\\/(\\d+)/);\n if (!match) return null;\n return `https://www.tiktok.com/embed/v2/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseVimeoEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"vimeo.com\")) return null;\n const match = u.pathname.match(/(\\d+)/);\n if (!match) return null;\n return `https://player.vimeo.com/video/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction getEmbedSrc(platform: EmbedPlatform, value: string): string | null {\n switch (platform) {\n case \"youtube\": return parseYouTubeEmbed(value);\n case \"spotify\": return parseSpotifyEmbed(value);\n case \"soundcloud\": return parseSoundCloudEmbed(value);\n case \"tiktok\": return parseTikTokEmbed(value);\n case \"vimeo\": return parseVimeoEmbed(value);\n }\n}\n\n// Compact iframe (fixed height) vs 16:9 video frame.\nconst COMPACT: Record<EmbedPlatform, boolean> = {\n spotify: true,\n soundcloud: true,\n youtube: false,\n tiktok: false,\n vimeo: false,\n};\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function IPTypeDisplay({ attributes }: IPTypeDisplayProps) {\n const attrs = attributes ?? [];\n\n const ipType = attrs.find(\n (a) => a.trait_type?.toLowerCase() === \"ip type\"\n )?.value as IPType | undefined;\n if (!ipType) return null;\n\n const template = IP_TEMPLATES[ipType];\n if (!template) return null;\n\n const getAttr = (key: string) =>\n attrs.find((a) => a.trait_type === key)?.value ?? null;\n\n const embeds = (template.embeds ?? []).flatMap((platform) => {\n const meta = EMBED_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const socials = (template.socials ?? []).flatMap((platform) => {\n const meta = SOCIAL_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const docUri = template.docUpload ? getAttr(template.docUpload.traitType) : null;\n\n if (embeds.length === 0 && socials.length === 0 && !docUri) return null;\n\n return (\n <div className=\"space-y-5\">\n {/* Document pinned to IPFS — immutable, timestamped copy of the work */}\n {docUri && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n Document\n </p>\n <a\n href={docUri.startsWith(\"ipfs://\") ? ipfsToHttp(docUri) : docUri}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-3 transition-colors hover:border-primary/40 group\"\n >\n <FileText className=\"h-5 w-5 text-primary shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold group-hover:text-primary transition-colors\">\n View document\n </p>\n <p className=\"text-xs text-muted-foreground flex items-center gap-1 mt-0.5\">\n <ShieldCheck className=\"h-3 w-3 shrink-0\" />\n Stored on IPFS — immutable, timestamped copy of the original\n </p>\n </div>\n <ExternalLink className=\"h-3.5 w-3.5 text-muted-foreground shrink-0\" />\n </a>\n </div>\n )}\n {embeds.map(({ platform, meta, value }) => {\n const src = getEmbedSrc(platform, value);\n if (src) {\n return (\n <div key={platform} className=\"space-y-1.5\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n {meta.label}\n </p>\n {COMPACT[platform] ? (\n <iframe\n src={src}\n className=\"w-full rounded-xl border-0\"\n height={166}\n allow=\"autoplay\"\n loading=\"lazy\"\n title={meta.label}\n />\n ) : (\n <div className=\"relative w-full aspect-video rounded-xl overflow-hidden bg-muted/20\">\n <iframe\n src={src}\n className=\"absolute inset-0 w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n title={meta.label}\n />\n </div>\n )}\n </div>\n );\n }\n // Fallback: plain external link if URL parsing failed\n return (\n <div key={platform}>\n <p className=\"text-xs font-medium text-muted-foreground mb-1\">\n {meta.label}\n </p>\n <a\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 text-sm text-primary hover:underline\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n Open link\n </a>\n </div>\n );\n })}\n\n {socials.length > 0 && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n Links\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {socials.map(({ platform, meta, value }) => {\n const SIcon = meta.icon;\n return (\n <a\n key={platform}\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground\"\n >\n <SIcon className=\"h-3.5 w-3.5\" />\n {meta.label}\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -111,7 +111,7 @@ function IPTypeDisplay({ attributes }) {
|
|
|
111
111
|
if (embeds.length === 0 && socials.length === 0 && !docUri) return null;
|
|
112
112
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
113
113
|
docUri && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
114
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs font-
|
|
114
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "Document" }),
|
|
115
115
|
/* @__PURE__ */ jsxs(
|
|
116
116
|
"a",
|
|
117
117
|
{
|
|
@@ -137,7 +137,7 @@ function IPTypeDisplay({ attributes }) {
|
|
|
137
137
|
const src = getEmbedSrc(platform, value);
|
|
138
138
|
if (src) {
|
|
139
139
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
140
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs font-
|
|
140
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: meta.label }),
|
|
141
141
|
COMPACT[platform] ? /* @__PURE__ */ jsx(
|
|
142
142
|
"iframe",
|
|
143
143
|
{
|
|
@@ -162,7 +162,7 @@ function IPTypeDisplay({ attributes }) {
|
|
|
162
162
|
] }, platform);
|
|
163
163
|
}
|
|
164
164
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
165
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs font-
|
|
165
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1", children: meta.label }),
|
|
166
166
|
/* @__PURE__ */ jsxs(
|
|
167
167
|
"a",
|
|
168
168
|
{
|
|
@@ -179,7 +179,7 @@ function IPTypeDisplay({ attributes }) {
|
|
|
179
179
|
] }, platform);
|
|
180
180
|
}),
|
|
181
181
|
socials.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
182
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs font-
|
|
182
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "Links" }),
|
|
183
183
|
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: socials.map(({ platform, meta, value }) => {
|
|
184
184
|
const SIcon = meta.icon;
|
|
185
185
|
return /* @__PURE__ */ jsxs(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/ip-type-display.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IPType } from \"../data/ip.js\";\nimport {\n IP_TEMPLATES,\n EMBED_PLATFORM_META,\n SOCIAL_PLATFORM_META,\n type EmbedPlatform,\n} from \"../data/ip-templates.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { ExternalLink, FileText, ShieldCheck } from \"lucide-react\";\n\ninterface Attr {\n trait_type?: string | null;\n value?: string | null;\n}\n\ninterface IPTypeDisplayProps {\n attributes: Attr[] | null | undefined;\n}\n\n// ── Embed URL parsers ────────────────────────────────────────────────────────\n\nfunction parseYouTubeEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n let id: string | null = null;\n if (u.hostname.includes(\"youtu.be\")) {\n id = u.pathname.slice(1);\n } else if (u.hostname.includes(\"youtube.com\")) {\n id = u.searchParams.get(\"v\");\n }\n if (!id) return null;\n return `https://www.youtube.com/embed/${id}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSpotifyEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"spotify.com\")) return null;\n // Spotify's embed endpoint only accepts /embed/{type}/{id}. Isolate the\n // resource type + id so locale prefixes (e.g. /intl-pt) and trailing query\n // params don't leak through — `/embed/intl-pt/album/…` 404s on Spotify.\n const match = u.pathname.match(\n /(track|album|playlist|episode|show|artist)\\/([A-Za-z0-9]+)/\n );\n if (!match) return null;\n return `https://open.spotify.com/embed/${match[1]}/${match[2]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSoundCloudEmbed(url: string): string | null {\n try {\n new URL(url); // validate\n if (!url.includes(\"soundcloud.com\")) return null;\n const encoded = encodeURIComponent(url);\n return `https://w.soundcloud.com/player/?url=${encoded}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false`;\n } catch {\n return null;\n }\n}\n\nfunction parseTikTokEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"tiktok.com\")) return null;\n const match = u.pathname.match(/\\/video\\/(\\d+)/);\n if (!match) return null;\n return `https://www.tiktok.com/embed/v2/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseVimeoEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"vimeo.com\")) return null;\n const match = u.pathname.match(/(\\d+)/);\n if (!match) return null;\n return `https://player.vimeo.com/video/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction getEmbedSrc(platform: EmbedPlatform, value: string): string | null {\n switch (platform) {\n case \"youtube\": return parseYouTubeEmbed(value);\n case \"spotify\": return parseSpotifyEmbed(value);\n case \"soundcloud\": return parseSoundCloudEmbed(value);\n case \"tiktok\": return parseTikTokEmbed(value);\n case \"vimeo\": return parseVimeoEmbed(value);\n }\n}\n\n// Compact iframe (fixed height) vs 16:9 video frame.\nconst COMPACT: Record<EmbedPlatform, boolean> = {\n spotify: true,\n soundcloud: true,\n youtube: false,\n tiktok: false,\n vimeo: false,\n};\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function IPTypeDisplay({ attributes }: IPTypeDisplayProps) {\n const attrs = attributes ?? [];\n\n const ipType = attrs.find(\n (a) => a.trait_type?.toLowerCase() === \"ip type\"\n )?.value as IPType | undefined;\n if (!ipType) return null;\n\n const template = IP_TEMPLATES[ipType];\n if (!template) return null;\n\n const getAttr = (key: string) =>\n attrs.find((a) => a.trait_type === key)?.value ?? null;\n\n const embeds = (template.embeds ?? []).flatMap((platform) => {\n const meta = EMBED_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const socials = (template.socials ?? []).flatMap((platform) => {\n const meta = SOCIAL_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const docUri = template.docUpload ? getAttr(template.docUpload.traitType) : null;\n\n if (embeds.length === 0 && socials.length === 0 && !docUri) return null;\n\n return (\n <div className=\"space-y-5\">\n {/* Document pinned to IPFS — immutable, timestamped copy of the work */}\n {docUri && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Document\n </p>\n <a\n href={docUri.startsWith(\"ipfs://\") ? ipfsToHttp(docUri) : docUri}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-3 transition-colors hover:border-primary/40 group\"\n >\n <FileText className=\"h-5 w-5 text-primary shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold group-hover:text-primary transition-colors\">\n View document\n </p>\n <p className=\"text-xs text-muted-foreground flex items-center gap-1 mt-0.5\">\n <ShieldCheck className=\"h-3 w-3 shrink-0\" />\n Stored on IPFS — immutable, timestamped copy of the original\n </p>\n </div>\n <ExternalLink className=\"h-3.5 w-3.5 text-muted-foreground shrink-0\" />\n </a>\n </div>\n )}\n {embeds.map(({ platform, meta, value }) => {\n const src = getEmbedSrc(platform, value);\n if (src) {\n return (\n <div key={platform} className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n {meta.label}\n </p>\n {COMPACT[platform] ? (\n <iframe\n src={src}\n className=\"w-full rounded-xl border-0\"\n height={166}\n allow=\"autoplay\"\n loading=\"lazy\"\n title={meta.label}\n />\n ) : (\n <div className=\"relative w-full aspect-video rounded-xl overflow-hidden bg-muted/20\">\n <iframe\n src={src}\n className=\"absolute inset-0 w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n title={meta.label}\n />\n </div>\n )}\n </div>\n );\n }\n // Fallback: plain external link if URL parsing failed\n return (\n <div key={platform}>\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1\">\n {meta.label}\n </p>\n <a\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 text-sm text-primary hover:underline\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n Open link\n </a>\n </div>\n );\n })}\n\n {socials.length > 0 && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Links\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {socials.map(({ platform, meta, value }) => {\n const SIcon = meta.icon;\n return (\n <a\n key={platform}\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground\"\n >\n <SIcon className=\"h-3.5 w-3.5\" />\n {meta.label}\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/ip-type-display.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IPType } from \"../data/ip.js\";\nimport {\n IP_TEMPLATES,\n EMBED_PLATFORM_META,\n SOCIAL_PLATFORM_META,\n type EmbedPlatform,\n} from \"../data/ip-templates.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { ExternalLink, FileText, ShieldCheck } from \"lucide-react\";\n\ninterface Attr {\n trait_type?: string | null;\n value?: string | null;\n}\n\ninterface IPTypeDisplayProps {\n attributes: Attr[] | null | undefined;\n}\n\n// ── Embed URL parsers ────────────────────────────────────────────────────────\n\nfunction parseYouTubeEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n let id: string | null = null;\n if (u.hostname.includes(\"youtu.be\")) {\n id = u.pathname.slice(1);\n } else if (u.hostname.includes(\"youtube.com\")) {\n id = u.searchParams.get(\"v\");\n }\n if (!id) return null;\n return `https://www.youtube.com/embed/${id}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSpotifyEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"spotify.com\")) return null;\n // Spotify's embed endpoint only accepts /embed/{type}/{id}. Isolate the\n // resource type + id so locale prefixes (e.g. /intl-pt) and trailing query\n // params don't leak through — `/embed/intl-pt/album/…` 404s on Spotify.\n const match = u.pathname.match(\n /(track|album|playlist|episode|show|artist)\\/([A-Za-z0-9]+)/\n );\n if (!match) return null;\n return `https://open.spotify.com/embed/${match[1]}/${match[2]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSoundCloudEmbed(url: string): string | null {\n try {\n new URL(url); // validate\n if (!url.includes(\"soundcloud.com\")) return null;\n const encoded = encodeURIComponent(url);\n return `https://w.soundcloud.com/player/?url=${encoded}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false`;\n } catch {\n return null;\n }\n}\n\nfunction parseTikTokEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"tiktok.com\")) return null;\n const match = u.pathname.match(/\\/video\\/(\\d+)/);\n if (!match) return null;\n return `https://www.tiktok.com/embed/v2/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseVimeoEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"vimeo.com\")) return null;\n const match = u.pathname.match(/(\\d+)/);\n if (!match) return null;\n return `https://player.vimeo.com/video/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction getEmbedSrc(platform: EmbedPlatform, value: string): string | null {\n switch (platform) {\n case \"youtube\": return parseYouTubeEmbed(value);\n case \"spotify\": return parseSpotifyEmbed(value);\n case \"soundcloud\": return parseSoundCloudEmbed(value);\n case \"tiktok\": return parseTikTokEmbed(value);\n case \"vimeo\": return parseVimeoEmbed(value);\n }\n}\n\n// Compact iframe (fixed height) vs 16:9 video frame.\nconst COMPACT: Record<EmbedPlatform, boolean> = {\n spotify: true,\n soundcloud: true,\n youtube: false,\n tiktok: false,\n vimeo: false,\n};\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function IPTypeDisplay({ attributes }: IPTypeDisplayProps) {\n const attrs = attributes ?? [];\n\n const ipType = attrs.find(\n (a) => a.trait_type?.toLowerCase() === \"ip type\"\n )?.value as IPType | undefined;\n if (!ipType) return null;\n\n const template = IP_TEMPLATES[ipType];\n if (!template) return null;\n\n const getAttr = (key: string) =>\n attrs.find((a) => a.trait_type === key)?.value ?? null;\n\n const embeds = (template.embeds ?? []).flatMap((platform) => {\n const meta = EMBED_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const socials = (template.socials ?? []).flatMap((platform) => {\n const meta = SOCIAL_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const docUri = template.docUpload ? getAttr(template.docUpload.traitType) : null;\n\n if (embeds.length === 0 && socials.length === 0 && !docUri) return null;\n\n return (\n <div className=\"space-y-5\">\n {/* Document pinned to IPFS — immutable, timestamped copy of the work */}\n {docUri && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n Document\n </p>\n <a\n href={docUri.startsWith(\"ipfs://\") ? ipfsToHttp(docUri) : docUri}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-3 transition-colors hover:border-primary/40 group\"\n >\n <FileText className=\"h-5 w-5 text-primary shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold group-hover:text-primary transition-colors\">\n View document\n </p>\n <p className=\"text-xs text-muted-foreground flex items-center gap-1 mt-0.5\">\n <ShieldCheck className=\"h-3 w-3 shrink-0\" />\n Stored on IPFS — immutable, timestamped copy of the original\n </p>\n </div>\n <ExternalLink className=\"h-3.5 w-3.5 text-muted-foreground shrink-0\" />\n </a>\n </div>\n )}\n {embeds.map(({ platform, meta, value }) => {\n const src = getEmbedSrc(platform, value);\n if (src) {\n return (\n <div key={platform} className=\"space-y-1.5\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n {meta.label}\n </p>\n {COMPACT[platform] ? (\n <iframe\n src={src}\n className=\"w-full rounded-xl border-0\"\n height={166}\n allow=\"autoplay\"\n loading=\"lazy\"\n title={meta.label}\n />\n ) : (\n <div className=\"relative w-full aspect-video rounded-xl overflow-hidden bg-muted/20\">\n <iframe\n src={src}\n className=\"absolute inset-0 w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n title={meta.label}\n />\n </div>\n )}\n </div>\n );\n }\n // Fallback: plain external link if URL parsing failed\n return (\n <div key={platform}>\n <p className=\"text-xs font-medium text-muted-foreground mb-1\">\n {meta.label}\n </p>\n <a\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 text-sm text-primary hover:underline\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n Open link\n </a>\n </div>\n );\n })}\n\n {socials.length > 0 && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n Links\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {socials.map(({ platform, meta, value }) => {\n const SIcon = meta.icon;\n return (\n <a\n key={platform}\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground\"\n >\n <SIcon className=\"h-3.5 w-3.5\" />\n {meta.label}\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -47,13 +47,13 @@ function PortfolioBentoTile({
|
|
|
47
47
|
"section",
|
|
48
48
|
{
|
|
49
49
|
className: (0, import_cn.cn)(
|
|
50
|
-
"rounded-2xl border border-border/50 bg-card/60 min-w-0",
|
|
51
|
-
size === "wide" && "col-span-
|
|
50
|
+
"rounded-2xl border border-border/50 bg-card/60 min-w-0 transition-colors hover:border-border",
|
|
51
|
+
size === "wide" && "md:col-span-2",
|
|
52
52
|
className
|
|
53
53
|
),
|
|
54
54
|
children: [
|
|
55
55
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between gap-3 px-4 pt-4 pb-3", children: [
|
|
56
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-
|
|
56
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-xs font-medium text-muted-foreground", children: title }),
|
|
57
57
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
58
58
|
import_link.default,
|
|
59
59
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/portfolio-bento-tile.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { ArrowRight } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\n\nexport interface PortfolioBentoTileProps {\n /** Tile heading, e.g. \"Collections\". */\n title: string;\n /** \"See all\" link target. */\n href: string;\n /**\n * \"wide\" spans
|
|
1
|
+
{"version":3,"sources":["../../src/components/portfolio-bento-tile.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { ArrowRight } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\n\nexport interface PortfolioBentoTileProps {\n /** Tile heading, e.g. \"Collections\". */\n title: string;\n /** \"See all\" link target. */\n href: string;\n /**\n * \"wide\" spans 2 grid tracks (for content that reads better a bit wider,\n * like Activity rows) while still sharing a row with other tiles when\n * there's room. Default sits in a single track.\n */\n size?: \"wide\" | \"default\";\n children: React.ReactNode;\n className?: string;\n}\n\n/**\n * A single cell on the portfolio landing page: a titled, media-forward\n * preview of one section, expanding into its full subpage via \"See all\".\n * Pure presentation — content is supplied by the caller.\n */\nexport function PortfolioBentoTile({\n title,\n href,\n size = \"default\",\n children,\n className,\n}: PortfolioBentoTileProps) {\n return (\n <section\n className={cn(\n \"rounded-2xl border border-border/50 bg-card/60 min-w-0 transition-colors hover:border-border\",\n size === \"wide\" && \"md:col-span-2\",\n className,\n )}\n >\n <div className=\"flex items-center justify-between gap-3 px-4 pt-4 pb-3\">\n <h2 className=\"text-xs font-medium text-muted-foreground\">\n {title}\n </h2>\n <Link\n href={href}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors\"\n >\n See all\n <ArrowRight className=\"h-3 w-3\" />\n </Link>\n </div>\n <div className=\"px-4 pb-4\">{children}</div>\n </section>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA0CQ;AAxCR,kBAAiB;AACjB,0BAA2B;AAC3B,gBAAmB;AAsBZ,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AACF,GAA4B;AAC1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,QACT;AAAA,QACA,SAAS,UAAU;AAAA,QACnB;AAAA,MACF;AAAA,MAEA;AAAA,qDAAC,SAAI,WAAU,0DACb;AAAA,sDAAC,QAAG,WAAU,6CACX,iBACH;AAAA,UACA;AAAA,YAAC,YAAAA;AAAA,YAAA;AAAA,cACC;AAAA,cACA,WAAU;AAAA,cACX;AAAA;AAAA,gBAEC,4CAAC,kCAAW,WAAU,WAAU;AAAA;AAAA;AAAA,UAClC;AAAA,WACF;AAAA,QACA,4CAAC,SAAI,WAAU,aAAa,UAAS;AAAA;AAAA;AAAA,EACvC;AAEJ;","names":["Link"]}
|
|
@@ -6,9 +6,9 @@ interface PortfolioBentoTileProps {
|
|
|
6
6
|
/** "See all" link target. */
|
|
7
7
|
href: string;
|
|
8
8
|
/**
|
|
9
|
-
* "wide" spans
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* "wide" spans 2 grid tracks (for content that reads better a bit wider,
|
|
10
|
+
* like Activity rows) while still sharing a row with other tiles when
|
|
11
|
+
* there's room. Default sits in a single track.
|
|
12
12
|
*/
|
|
13
13
|
size?: "wide" | "default";
|
|
14
14
|
children: React.ReactNode;
|
|
@@ -6,9 +6,9 @@ interface PortfolioBentoTileProps {
|
|
|
6
6
|
/** "See all" link target. */
|
|
7
7
|
href: string;
|
|
8
8
|
/**
|
|
9
|
-
* "wide" spans
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* "wide" spans 2 grid tracks (for content that reads better a bit wider,
|
|
10
|
+
* like Activity rows) while still sharing a row with other tiles when
|
|
11
|
+
* there's room. Default sits in a single track.
|
|
12
12
|
*/
|
|
13
13
|
size?: "wide" | "default";
|
|
14
14
|
children: React.ReactNode;
|
|
@@ -14,13 +14,13 @@ function PortfolioBentoTile({
|
|
|
14
14
|
"section",
|
|
15
15
|
{
|
|
16
16
|
className: cn(
|
|
17
|
-
"rounded-2xl border border-border/50 bg-card/60 min-w-0",
|
|
18
|
-
size === "wide" && "col-span-
|
|
17
|
+
"rounded-2xl border border-border/50 bg-card/60 min-w-0 transition-colors hover:border-border",
|
|
18
|
+
size === "wide" && "md:col-span-2",
|
|
19
19
|
className
|
|
20
20
|
),
|
|
21
21
|
children: [
|
|
22
22
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 px-4 pt-4 pb-3", children: [
|
|
23
|
-
/* @__PURE__ */ jsx("h2", { className: "text-
|
|
23
|
+
/* @__PURE__ */ jsx("h2", { className: "text-xs font-medium text-muted-foreground", children: title }),
|
|
24
24
|
/* @__PURE__ */ jsxs(
|
|
25
25
|
Link,
|
|
26
26
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/portfolio-bento-tile.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { ArrowRight } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\n\nexport interface PortfolioBentoTileProps {\n /** Tile heading, e.g. \"Collections\". */\n title: string;\n /** \"See all\" link target. */\n href: string;\n /**\n * \"wide\" spans
|
|
1
|
+
{"version":3,"sources":["../../src/components/portfolio-bento-tile.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { ArrowRight } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\n\nexport interface PortfolioBentoTileProps {\n /** Tile heading, e.g. \"Collections\". */\n title: string;\n /** \"See all\" link target. */\n href: string;\n /**\n * \"wide\" spans 2 grid tracks (for content that reads better a bit wider,\n * like Activity rows) while still sharing a row with other tiles when\n * there's room. Default sits in a single track.\n */\n size?: \"wide\" | \"default\";\n children: React.ReactNode;\n className?: string;\n}\n\n/**\n * A single cell on the portfolio landing page: a titled, media-forward\n * preview of one section, expanding into its full subpage via \"See all\".\n * Pure presentation — content is supplied by the caller.\n */\nexport function PortfolioBentoTile({\n title,\n href,\n size = \"default\",\n children,\n className,\n}: PortfolioBentoTileProps) {\n return (\n <section\n className={cn(\n \"rounded-2xl border border-border/50 bg-card/60 min-w-0 transition-colors hover:border-border\",\n size === \"wide\" && \"md:col-span-2\",\n className,\n )}\n >\n <div className=\"flex items-center justify-between gap-3 px-4 pt-4 pb-3\">\n <h2 className=\"text-xs font-medium text-muted-foreground\">\n {title}\n </h2>\n <Link\n href={href}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors\"\n >\n See all\n <ArrowRight className=\"h-3 w-3\" />\n </Link>\n </div>\n <div className=\"px-4 pb-4\">{children}</div>\n </section>\n );\n}\n"],"mappings":";AA0CQ,cAGA,YAHA;AAxCR,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,UAAU;AAsBZ,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AACF,GAA4B;AAC1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,SAAS,UAAU;AAAA,QACnB;AAAA,MACF;AAAA,MAEA;AAAA,6BAAC,SAAI,WAAU,0DACb;AAAA,8BAAC,QAAG,WAAU,6CACX,iBACH;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,WAAU;AAAA,cACX;AAAA;AAAA,gBAEC,oBAAC,cAAW,WAAU,WAAU;AAAA;AAAA;AAAA,UAClC;AAAA,WACF;AAAA,QACA,oBAAC,SAAI,WAAU,aAAa,UAAS;AAAA;AAAA;AAAA,EACvC;AAEJ;","names":[]}
|
|
@@ -35,9 +35,9 @@ module.exports = __toCommonJS(portfolio_chip_filter_exports);
|
|
|
35
35
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
36
36
|
var import_link = __toESM(require("next/link"), 1);
|
|
37
37
|
var import_cn = require("../utils/cn.js");
|
|
38
|
-
const CHIP_CLASS = "shrink-0 rounded-full px-3.5 py-1.5 text-[13px] whitespace-nowrap transition-colors";
|
|
39
|
-
const INACTIVE_CLASS = "bg-muted text-muted-foreground hover:text-foreground";
|
|
40
|
-
const ACTIVE_CLASS = "bg-primary text-primary-foreground font-medium";
|
|
38
|
+
const CHIP_CLASS = "shrink-0 rounded-full px-3.5 py-1.5 text-[13px] whitespace-nowrap transition-colors border";
|
|
39
|
+
const INACTIVE_CLASS = "bg-muted border-border/60 text-muted-foreground hover:text-foreground hover:border-border";
|
|
40
|
+
const ACTIVE_CLASS = "bg-primary border-primary text-primary-foreground font-medium";
|
|
41
41
|
function PortfolioChipFilter({
|
|
42
42
|
options,
|
|
43
43
|
value,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/portfolio-chip-filter.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { cn } from \"../utils/cn.js\";\n\nexport interface PortfolioChipFilterOption {\n key: string;\n label: string;\n /**\n * When set, this chip is a direct navigation link rather than an in-page\n * filter. It's marked active when `href` equals the current `value`\n * (pass the current pathname as `value` for a location-aware nav bar).\n */\n href?: string;\n}\n\nexport interface PortfolioChipFilterProps {\n options: PortfolioChipFilterOption[];\n /** Selected filter key, or the current pathname when used for navigation. */\n value: string;\n onChange: (key: string) => void;\n /** Suppress the auto-injected \"All\" chip — for pure navigation bars where every option already has an href. Default true. */\n showAll?: boolean;\n className?: string;\n}\n\nconst CHIP_CLASS =\n \"shrink-0 rounded-full px-3.5 py-1.5 text-[13px] whitespace-nowrap transition-colors\";\nconst INACTIVE_CLASS = \"bg-muted text-muted-foreground hover:text-foreground\";\nconst ACTIVE_CLASS = \"bg-primary text-primary-foreground font-medium\";\n\n/**\n * Horizontally-scrollable chip row — the portfolio section's sole\n * navigation mechanism (Instagram/YouTube-style chips), used both as an\n * in-page filter (chips with no `href`, toggled via `onChange`) and as\n * cross-page navigation (chips with `href`, highlighted active by matching\n * `value` against the current pathname).\n */\nexport function PortfolioChipFilter({\n options,\n value,\n onChange,\n showAll = true,\n className,\n}: PortfolioChipFilterProps) {\n const items = showAll ? [{ key: \"all\", label: \"All\" }, ...options] : options;\n\n return (\n <div\n className={cn(\n \"flex items-center gap-1.5 overflow-x-auto scrollbar-hide -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8\",\n className,\n )}\n >\n {items.map((item) =>\n item.href ? (\n <Link\n key={item.key}\n href={item.href}\n className={cn(CHIP_CLASS, item.href === value ? ACTIVE_CLASS : INACTIVE_CLASS)}\n >\n {item.label}\n </Link>\n ) : (\n <button\n key={item.key}\n type=\"button\"\n onClick={() => onChange(item.key)}\n className={cn(CHIP_CLASS, item.key === value ? ACTIVE_CLASS : INACTIVE_CLASS)}\n >\n {item.label}\n </button>\n ),\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwDU;AAtDV,kBAAiB;AACjB,gBAAmB;AAuBnB,MAAM,aACJ;AACF,MAAM,iBAAiB;AACvB,MAAM,eAAe;AASd,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AACF,GAA6B;AAC3B,QAAM,QAAQ,UAAU,CAAC,EAAE,KAAK,OAAO,OAAO,MAAM,GAAG,GAAG,OAAO,IAAI;AAErE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC,gBAAM;AAAA,QAAI,CAAC,SACV,KAAK,OACH;AAAA,UAAC,YAAAA;AAAA,UAAA;AAAA,YAEC,MAAM,KAAK;AAAA,YACX,eAAW,cAAG,YAAY,KAAK,SAAS,QAAQ,eAAe,cAAc;AAAA,YAE5E,eAAK;AAAA;AAAA,UAJD,KAAK;AAAA,QAKZ,IAEA;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAS,MAAM,SAAS,KAAK,GAAG;AAAA,YAChC,eAAW,cAAG,YAAY,KAAK,QAAQ,QAAQ,eAAe,cAAc;AAAA,YAE3E,eAAK;AAAA;AAAA,UALD,KAAK;AAAA,QAMZ;AAAA,MAEJ;AAAA;AAAA,EACF;AAEJ;","names":["Link"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/portfolio-chip-filter.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { cn } from \"../utils/cn.js\";\n\nexport interface PortfolioChipFilterOption {\n key: string;\n label: string;\n /**\n * When set, this chip is a direct navigation link rather than an in-page\n * filter. It's marked active when `href` equals the current `value`\n * (pass the current pathname as `value` for a location-aware nav bar).\n */\n href?: string;\n}\n\nexport interface PortfolioChipFilterProps {\n options: PortfolioChipFilterOption[];\n /** Selected filter key, or the current pathname when used for navigation. */\n value: string;\n onChange: (key: string) => void;\n /** Suppress the auto-injected \"All\" chip — for pure navigation bars where every option already has an href. Default true. */\n showAll?: boolean;\n className?: string;\n}\n\nconst CHIP_CLASS =\n \"shrink-0 rounded-full px-3.5 py-1.5 text-[13px] whitespace-nowrap transition-colors border\";\nconst INACTIVE_CLASS = \"bg-muted border-border/60 text-muted-foreground hover:text-foreground hover:border-border\";\nconst ACTIVE_CLASS = \"bg-primary border-primary text-primary-foreground font-medium\";\n\n/**\n * Horizontally-scrollable chip row — the portfolio section's sole\n * navigation mechanism (Instagram/YouTube-style chips), used both as an\n * in-page filter (chips with no `href`, toggled via `onChange`) and as\n * cross-page navigation (chips with `href`, highlighted active by matching\n * `value` against the current pathname).\n */\nexport function PortfolioChipFilter({\n options,\n value,\n onChange,\n showAll = true,\n className,\n}: PortfolioChipFilterProps) {\n const items = showAll ? [{ key: \"all\", label: \"All\" }, ...options] : options;\n\n return (\n <div\n className={cn(\n \"flex items-center gap-1.5 overflow-x-auto scrollbar-hide -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8\",\n className,\n )}\n >\n {items.map((item) =>\n item.href ? (\n <Link\n key={item.key}\n href={item.href}\n className={cn(CHIP_CLASS, item.href === value ? ACTIVE_CLASS : INACTIVE_CLASS)}\n >\n {item.label}\n </Link>\n ) : (\n <button\n key={item.key}\n type=\"button\"\n onClick={() => onChange(item.key)}\n className={cn(CHIP_CLASS, item.key === value ? ACTIVE_CLASS : INACTIVE_CLASS)}\n >\n {item.label}\n </button>\n ),\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwDU;AAtDV,kBAAiB;AACjB,gBAAmB;AAuBnB,MAAM,aACJ;AACF,MAAM,iBAAiB;AACvB,MAAM,eAAe;AASd,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AACF,GAA6B;AAC3B,QAAM,QAAQ,UAAU,CAAC,EAAE,KAAK,OAAO,OAAO,MAAM,GAAG,GAAG,OAAO,IAAI;AAErE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC,gBAAM;AAAA,QAAI,CAAC,SACV,KAAK,OACH;AAAA,UAAC,YAAAA;AAAA,UAAA;AAAA,YAEC,MAAM,KAAK;AAAA,YACX,eAAW,cAAG,YAAY,KAAK,SAAS,QAAQ,eAAe,cAAc;AAAA,YAE5E,eAAK;AAAA;AAAA,UAJD,KAAK;AAAA,QAKZ,IAEA;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAS,MAAM,SAAS,KAAK,GAAG;AAAA,YAChC,eAAW,cAAG,YAAY,KAAK,QAAQ,QAAQ,eAAe,cAAc;AAAA,YAE3E,eAAK;AAAA;AAAA,UALD,KAAK;AAAA,QAMZ;AAAA,MAEJ;AAAA;AAAA,EACF;AAEJ;","names":["Link"]}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { cn } from "../utils/cn.js";
|
|
5
|
-
const CHIP_CLASS = "shrink-0 rounded-full px-3.5 py-1.5 text-[13px] whitespace-nowrap transition-colors";
|
|
6
|
-
const INACTIVE_CLASS = "bg-muted text-muted-foreground hover:text-foreground";
|
|
7
|
-
const ACTIVE_CLASS = "bg-primary text-primary-foreground font-medium";
|
|
5
|
+
const CHIP_CLASS = "shrink-0 rounded-full px-3.5 py-1.5 text-[13px] whitespace-nowrap transition-colors border";
|
|
6
|
+
const INACTIVE_CLASS = "bg-muted border-border/60 text-muted-foreground hover:text-foreground hover:border-border";
|
|
7
|
+
const ACTIVE_CLASS = "bg-primary border-primary text-primary-foreground font-medium";
|
|
8
8
|
function PortfolioChipFilter({
|
|
9
9
|
options,
|
|
10
10
|
value,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/portfolio-chip-filter.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { cn } from \"../utils/cn.js\";\n\nexport interface PortfolioChipFilterOption {\n key: string;\n label: string;\n /**\n * When set, this chip is a direct navigation link rather than an in-page\n * filter. It's marked active when `href` equals the current `value`\n * (pass the current pathname as `value` for a location-aware nav bar).\n */\n href?: string;\n}\n\nexport interface PortfolioChipFilterProps {\n options: PortfolioChipFilterOption[];\n /** Selected filter key, or the current pathname when used for navigation. */\n value: string;\n onChange: (key: string) => void;\n /** Suppress the auto-injected \"All\" chip — for pure navigation bars where every option already has an href. Default true. */\n showAll?: boolean;\n className?: string;\n}\n\nconst CHIP_CLASS =\n \"shrink-0 rounded-full px-3.5 py-1.5 text-[13px] whitespace-nowrap transition-colors\";\nconst INACTIVE_CLASS = \"bg-muted text-muted-foreground hover:text-foreground\";\nconst ACTIVE_CLASS = \"bg-primary text-primary-foreground font-medium\";\n\n/**\n * Horizontally-scrollable chip row — the portfolio section's sole\n * navigation mechanism (Instagram/YouTube-style chips), used both as an\n * in-page filter (chips with no `href`, toggled via `onChange`) and as\n * cross-page navigation (chips with `href`, highlighted active by matching\n * `value` against the current pathname).\n */\nexport function PortfolioChipFilter({\n options,\n value,\n onChange,\n showAll = true,\n className,\n}: PortfolioChipFilterProps) {\n const items = showAll ? [{ key: \"all\", label: \"All\" }, ...options] : options;\n\n return (\n <div\n className={cn(\n \"flex items-center gap-1.5 overflow-x-auto scrollbar-hide -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8\",\n className,\n )}\n >\n {items.map((item) =>\n item.href ? (\n <Link\n key={item.key}\n href={item.href}\n className={cn(CHIP_CLASS, item.href === value ? ACTIVE_CLASS : INACTIVE_CLASS)}\n >\n {item.label}\n </Link>\n ) : (\n <button\n key={item.key}\n type=\"button\"\n onClick={() => onChange(item.key)}\n className={cn(CHIP_CLASS, item.key === value ? ACTIVE_CLASS : INACTIVE_CLASS)}\n >\n {item.label}\n </button>\n ),\n )}\n </div>\n );\n}\n"],"mappings":";AAwDU;AAtDV,OAAO,UAAU;AACjB,SAAS,UAAU;AAuBnB,MAAM,aACJ;AACF,MAAM,iBAAiB;AACvB,MAAM,eAAe;AASd,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AACF,GAA6B;AAC3B,QAAM,QAAQ,UAAU,CAAC,EAAE,KAAK,OAAO,OAAO,MAAM,GAAG,GAAG,OAAO,IAAI;AAErE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC,gBAAM;AAAA,QAAI,CAAC,SACV,KAAK,OACH;AAAA,UAAC;AAAA;AAAA,YAEC,MAAM,KAAK;AAAA,YACX,WAAW,GAAG,YAAY,KAAK,SAAS,QAAQ,eAAe,cAAc;AAAA,YAE5E,eAAK;AAAA;AAAA,UAJD,KAAK;AAAA,QAKZ,IAEA;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAS,MAAM,SAAS,KAAK,GAAG;AAAA,YAChC,WAAW,GAAG,YAAY,KAAK,QAAQ,QAAQ,eAAe,cAAc;AAAA,YAE3E,eAAK;AAAA;AAAA,UALD,KAAK;AAAA,QAMZ;AAAA,MAEJ;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/portfolio-chip-filter.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { cn } from \"../utils/cn.js\";\n\nexport interface PortfolioChipFilterOption {\n key: string;\n label: string;\n /**\n * When set, this chip is a direct navigation link rather than an in-page\n * filter. It's marked active when `href` equals the current `value`\n * (pass the current pathname as `value` for a location-aware nav bar).\n */\n href?: string;\n}\n\nexport interface PortfolioChipFilterProps {\n options: PortfolioChipFilterOption[];\n /** Selected filter key, or the current pathname when used for navigation. */\n value: string;\n onChange: (key: string) => void;\n /** Suppress the auto-injected \"All\" chip — for pure navigation bars where every option already has an href. Default true. */\n showAll?: boolean;\n className?: string;\n}\n\nconst CHIP_CLASS =\n \"shrink-0 rounded-full px-3.5 py-1.5 text-[13px] whitespace-nowrap transition-colors border\";\nconst INACTIVE_CLASS = \"bg-muted border-border/60 text-muted-foreground hover:text-foreground hover:border-border\";\nconst ACTIVE_CLASS = \"bg-primary border-primary text-primary-foreground font-medium\";\n\n/**\n * Horizontally-scrollable chip row — the portfolio section's sole\n * navigation mechanism (Instagram/YouTube-style chips), used both as an\n * in-page filter (chips with no `href`, toggled via `onChange`) and as\n * cross-page navigation (chips with `href`, highlighted active by matching\n * `value` against the current pathname).\n */\nexport function PortfolioChipFilter({\n options,\n value,\n onChange,\n showAll = true,\n className,\n}: PortfolioChipFilterProps) {\n const items = showAll ? [{ key: \"all\", label: \"All\" }, ...options] : options;\n\n return (\n <div\n className={cn(\n \"flex items-center gap-1.5 overflow-x-auto scrollbar-hide -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8\",\n className,\n )}\n >\n {items.map((item) =>\n item.href ? (\n <Link\n key={item.key}\n href={item.href}\n className={cn(CHIP_CLASS, item.href === value ? ACTIVE_CLASS : INACTIVE_CLASS)}\n >\n {item.label}\n </Link>\n ) : (\n <button\n key={item.key}\n type=\"button\"\n onClick={() => onChange(item.key)}\n className={cn(CHIP_CLASS, item.key === value ? ACTIVE_CLASS : INACTIVE_CLASS)}\n >\n {item.label}\n </button>\n ),\n )}\n </div>\n );\n}\n"],"mappings":";AAwDU;AAtDV,OAAO,UAAU;AACjB,SAAS,UAAU;AAuBnB,MAAM,aACJ;AACF,MAAM,iBAAiB;AACvB,MAAM,eAAe;AASd,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AACF,GAA6B;AAC3B,QAAM,QAAQ,UAAU,CAAC,EAAE,KAAK,OAAO,OAAO,MAAM,GAAG,GAAG,OAAO,IAAI;AAErE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC,gBAAM;AAAA,QAAI,CAAC,SACV,KAAK,OACH;AAAA,UAAC;AAAA;AAAA,YAEC,MAAM,KAAK;AAAA,YACX,WAAW,GAAG,YAAY,KAAK,SAAS,QAAQ,eAAe,cAAc;AAAA,YAE5E,eAAK;AAAA;AAAA,UAJD,KAAK;AAAA,QAKZ,IAEA;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAS,MAAM,SAAS,KAAK,GAAG;AAAA,YAChC,WAAW,GAAG,YAAY,KAAK,QAAQ,QAAQ,eAAe,cAAc;AAAA,YAE3E,eAAK;AAAA;AAAA,UALD,KAAK;AAAA,QAMZ;AAAA,MAEJ;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -50,7 +50,7 @@ function PortfolioHeader({
|
|
|
50
50
|
] }) });
|
|
51
51
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: (0, import_cn.cn)("flex items-center justify-between gap-4", className), children: [
|
|
52
52
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0", children: [
|
|
53
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-
|
|
53
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-medium text-muted-foreground", children: "Portfolio" }),
|
|
54
54
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
55
55
|
import_address_display.AddressDisplay,
|
|
56
56
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/portfolio-header.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { cn } from \"../utils/cn.js\";\nimport { AddressDisplay } from \"./address-display.js\";\n\nexport interface PortfolioHeaderScore {\n /** Journey level name (\"Starter\", \"Explorer\", …) — no numeric level shown. */\n levelName: string;\n totalXp: number;\n /** Where the score chip links (\"/rewards\"). */\n href?: string;\n}\n\nexport interface PortfolioHeaderProps {\n address: string;\n score?: PortfolioHeaderScore | null;\n className?: string;\n}\n\n/**\n * Portfolio identity header: the address as a real page title (no generic\n * gradient/avatar element — there's no profile data to lead with, and a\n * placeholder gradient mark isn't the answer), with the rewards journey\n * chip alongside it.\n */\nexport function PortfolioHeader({\n address,\n score,\n className,\n}: PortfolioHeaderProps) {\n const scoreChip = score && (\n <div className=\"btn-border-animated p-[1.5px] rounded-full\">\n <div className=\"rounded-full bg-card px-3.5 py-1.5 flex items-baseline gap-2 whitespace-nowrap\">\n <span className=\"text-sm font-semibold text-foreground\">\n {score.levelName}\n </span>\n <span className=\"text-xs text-muted-foreground tabular-nums\">\n {score.totalXp.toLocaleString()} XP\n </span>\n </div>\n </div>\n );\n\n return (\n <div className={cn(\"flex items-center justify-between gap-4\", className)}>\n <div className=\"min-w-0\">\n <p className=\"text-
|
|
1
|
+
{"version":3,"sources":["../../src/components/portfolio-header.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { cn } from \"../utils/cn.js\";\nimport { AddressDisplay } from \"./address-display.js\";\n\nexport interface PortfolioHeaderScore {\n /** Journey level name (\"Starter\", \"Explorer\", …) — no numeric level shown. */\n levelName: string;\n totalXp: number;\n /** Where the score chip links (\"/rewards\"). */\n href?: string;\n}\n\nexport interface PortfolioHeaderProps {\n address: string;\n score?: PortfolioHeaderScore | null;\n className?: string;\n}\n\n/**\n * Portfolio identity header: the address as a real page title (no generic\n * gradient/avatar element — there's no profile data to lead with, and a\n * placeholder gradient mark isn't the answer), with the rewards journey\n * chip alongside it.\n */\nexport function PortfolioHeader({\n address,\n score,\n className,\n}: PortfolioHeaderProps) {\n const scoreChip = score && (\n <div className=\"btn-border-animated p-[1.5px] rounded-full\">\n <div className=\"rounded-full bg-card px-3.5 py-1.5 flex items-baseline gap-2 whitespace-nowrap\">\n <span className=\"text-sm font-semibold text-foreground\">\n {score.levelName}\n </span>\n <span className=\"text-xs text-muted-foreground tabular-nums\">\n {score.totalXp.toLocaleString()} XP\n </span>\n </div>\n </div>\n );\n\n return (\n <div className={cn(\"flex items-center justify-between gap-4\", className)}>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n Portfolio\n </p>\n <AddressDisplay\n address={address}\n chars={6}\n className=\"text-xl font-bold tracking-tight text-foreground\"\n />\n </div>\n {score?.href ? (\n <Link href={score.href} className=\"shrink-0 active:opacity-80\">\n {scoreChip}\n </Link>\n ) : (\n <div className=\"shrink-0\">{scoreChip}</div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkCQ;AAhCR,kBAAiB;AACjB,gBAAmB;AACnB,6BAA+B;AAsBxB,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,YAAY,SAChB,4CAAC,SAAI,WAAU,8CACb,uDAAC,SAAI,WAAU,kFACb;AAAA,gDAAC,UAAK,WAAU,yCACb,gBAAM,WACT;AAAA,IACA,6CAAC,UAAK,WAAU,8CACb;AAAA,YAAM,QAAQ,eAAe;AAAA,MAAE;AAAA,OAClC;AAAA,KACF,GACF;AAGF,SACE,6CAAC,SAAI,eAAW,cAAG,2CAA2C,SAAS,GACrE;AAAA,iDAAC,SAAI,WAAU,WACb;AAAA,kDAAC,OAAE,WAAU,6CAA4C,uBAEzD;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,OAAO;AAAA,UACP,WAAU;AAAA;AAAA,MACZ;AAAA,OACF;AAAA,IACC,OAAO,OACN,4CAAC,YAAAA,SAAA,EAAK,MAAM,MAAM,MAAM,WAAU,8BAC/B,qBACH,IAEA,4CAAC,SAAI,WAAU,YAAY,qBAAU;AAAA,KAEzC;AAEJ;","names":["Link"]}
|
|
@@ -17,7 +17,7 @@ function PortfolioHeader({
|
|
|
17
17
|
] }) });
|
|
18
18
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center justify-between gap-4", className), children: [
|
|
19
19
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
20
|
-
/* @__PURE__ */ jsx("p", { className: "text-
|
|
20
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground", children: "Portfolio" }),
|
|
21
21
|
/* @__PURE__ */ jsx(
|
|
22
22
|
AddressDisplay,
|
|
23
23
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/portfolio-header.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { cn } from \"../utils/cn.js\";\nimport { AddressDisplay } from \"./address-display.js\";\n\nexport interface PortfolioHeaderScore {\n /** Journey level name (\"Starter\", \"Explorer\", …) — no numeric level shown. */\n levelName: string;\n totalXp: number;\n /** Where the score chip links (\"/rewards\"). */\n href?: string;\n}\n\nexport interface PortfolioHeaderProps {\n address: string;\n score?: PortfolioHeaderScore | null;\n className?: string;\n}\n\n/**\n * Portfolio identity header: the address as a real page title (no generic\n * gradient/avatar element — there's no profile data to lead with, and a\n * placeholder gradient mark isn't the answer), with the rewards journey\n * chip alongside it.\n */\nexport function PortfolioHeader({\n address,\n score,\n className,\n}: PortfolioHeaderProps) {\n const scoreChip = score && (\n <div className=\"btn-border-animated p-[1.5px] rounded-full\">\n <div className=\"rounded-full bg-card px-3.5 py-1.5 flex items-baseline gap-2 whitespace-nowrap\">\n <span className=\"text-sm font-semibold text-foreground\">\n {score.levelName}\n </span>\n <span className=\"text-xs text-muted-foreground tabular-nums\">\n {score.totalXp.toLocaleString()} XP\n </span>\n </div>\n </div>\n );\n\n return (\n <div className={cn(\"flex items-center justify-between gap-4\", className)}>\n <div className=\"min-w-0\">\n <p className=\"text-
|
|
1
|
+
{"version":3,"sources":["../../src/components/portfolio-header.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { cn } from \"../utils/cn.js\";\nimport { AddressDisplay } from \"./address-display.js\";\n\nexport interface PortfolioHeaderScore {\n /** Journey level name (\"Starter\", \"Explorer\", …) — no numeric level shown. */\n levelName: string;\n totalXp: number;\n /** Where the score chip links (\"/rewards\"). */\n href?: string;\n}\n\nexport interface PortfolioHeaderProps {\n address: string;\n score?: PortfolioHeaderScore | null;\n className?: string;\n}\n\n/**\n * Portfolio identity header: the address as a real page title (no generic\n * gradient/avatar element — there's no profile data to lead with, and a\n * placeholder gradient mark isn't the answer), with the rewards journey\n * chip alongside it.\n */\nexport function PortfolioHeader({\n address,\n score,\n className,\n}: PortfolioHeaderProps) {\n const scoreChip = score && (\n <div className=\"btn-border-animated p-[1.5px] rounded-full\">\n <div className=\"rounded-full bg-card px-3.5 py-1.5 flex items-baseline gap-2 whitespace-nowrap\">\n <span className=\"text-sm font-semibold text-foreground\">\n {score.levelName}\n </span>\n <span className=\"text-xs text-muted-foreground tabular-nums\">\n {score.totalXp.toLocaleString()} XP\n </span>\n </div>\n </div>\n );\n\n return (\n <div className={cn(\"flex items-center justify-between gap-4\", className)}>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n Portfolio\n </p>\n <AddressDisplay\n address={address}\n chars={6}\n className=\"text-xl font-bold tracking-tight text-foreground\"\n />\n </div>\n {score?.href ? (\n <Link href={score.href} className=\"shrink-0 active:opacity-80\">\n {scoreChip}\n </Link>\n ) : (\n <div className=\"shrink-0\">{scoreChip}</div>\n )}\n </div>\n );\n}\n"],"mappings":";AAkCQ,cAGA,YAHA;AAhCR,OAAO,UAAU;AACjB,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAsBxB,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,YAAY,SAChB,oBAAC,SAAI,WAAU,8CACb,+BAAC,SAAI,WAAU,kFACb;AAAA,wBAAC,UAAK,WAAU,yCACb,gBAAM,WACT;AAAA,IACA,qBAAC,UAAK,WAAU,8CACb;AAAA,YAAM,QAAQ,eAAe;AAAA,MAAE;AAAA,OAClC;AAAA,KACF,GACF;AAGF,SACE,qBAAC,SAAI,WAAW,GAAG,2CAA2C,SAAS,GACrE;AAAA,yBAAC,SAAI,WAAU,WACb;AAAA,0BAAC,OAAE,WAAU,6CAA4C,uBAEzD;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA,OAAO;AAAA,UACP,WAAU;AAAA;AAAA,MACZ;AAAA,OACF;AAAA,IACC,OAAO,OACN,oBAAC,QAAK,MAAM,MAAM,MAAM,WAAU,8BAC/B,qBACH,IAEA,oBAAC,SAAI,WAAU,YAAY,qBAAU;AAAA,KAEzC;AAEJ;","names":[]}
|
|
@@ -38,7 +38,10 @@ function PortfolioOverview({
|
|
|
38
38
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
39
39
|
"div",
|
|
40
40
|
{
|
|
41
|
-
className: (0, import_cn.cn)(
|
|
41
|
+
className: (0, import_cn.cn)(
|
|
42
|
+
"grid gap-4 items-start grid-cols-[repeat(auto-fit,minmax(320px,1fr))]",
|
|
43
|
+
className
|
|
44
|
+
),
|
|
42
45
|
children: populatedTiles.map((tile) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
43
46
|
import_portfolio_bento_tile.PortfolioBentoTile,
|
|
44
47
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/portfolio-overview.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"../utils/cn.js\";\nimport { PortfolioBentoTile, type PortfolioBentoTileProps } from \"./portfolio-bento-tile.js\";\n\nexport interface PortfolioBentoTileConfig {\n key: string;\n title: string;\n href: string;\n size?: PortfolioBentoTileProps[\"size\"];\n /** Tiles with nothing to show are omitted entirely, not rendered empty. */\n isEmpty?: boolean;\n content: React.ReactNode;\n}\n\nexport interface PortfolioOverviewProps {\n tiles: PortfolioBentoTileConfig[];\n /** First-run state across the whole portfolio (zero holdings, zero activity). */\n isEmpty?: boolean;\n emptyState?: React.ReactNode;\n className?: string;\n}\n\n/**\n * Portfolio landing page: a content-first dashboard. Each tile is a fast\n * preview of one section, expanding into its full subpage via \"See all\".\n * Navigation across portfolio sections lives one level up, in the shared\n * portfolio chip bar rendered by the app's layout on every page — this\n * component is pure tile layout, nothing else.\n *\n * Uses a CSS grid with `auto-fit` tracks rather than multi-column masonry:\n * masonry's fixed column tracks leave dead blank space when few tiles are\n * populated (a real, reproduced failure mode with sparse content). `auto-fit`\n * collapses empty tracks and stretches present tiles to fill the freed\n * width, so the layout always uses the full screen regardless of how many\n * tiles are populated.\n */\nexport function PortfolioOverview({\n tiles,\n isEmpty,\n emptyState,\n className,\n}: PortfolioOverviewProps) {\n if (isEmpty) {\n return <div className={className}>{emptyState}</div>;\n }\n\n const populatedTiles = tiles.filter((t) => !t.isEmpty);\n\n return (\n <div\n className={cn(\"grid gap-4 grid-cols-[repeat(auto-fit,minmax(320px,1fr))]\"
|
|
1
|
+
{"version":3,"sources":["../../src/components/portfolio-overview.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"../utils/cn.js\";\nimport { PortfolioBentoTile, type PortfolioBentoTileProps } from \"./portfolio-bento-tile.js\";\n\nexport interface PortfolioBentoTileConfig {\n key: string;\n title: string;\n href: string;\n size?: PortfolioBentoTileProps[\"size\"];\n /** Tiles with nothing to show are omitted entirely, not rendered empty. */\n isEmpty?: boolean;\n content: React.ReactNode;\n}\n\nexport interface PortfolioOverviewProps {\n tiles: PortfolioBentoTileConfig[];\n /** First-run state across the whole portfolio (zero holdings, zero activity). */\n isEmpty?: boolean;\n emptyState?: React.ReactNode;\n className?: string;\n}\n\n/**\n * Portfolio landing page: a content-first dashboard. Each tile is a fast\n * preview of one section, expanding into its full subpage via \"See all\".\n * Navigation across portfolio sections lives one level up, in the shared\n * portfolio chip bar rendered by the app's layout on every page — this\n * component is pure tile layout, nothing else.\n *\n * Uses a CSS grid with `auto-fit` tracks rather than multi-column masonry:\n * masonry's fixed column tracks leave dead blank space when few tiles are\n * populated (a real, reproduced failure mode with sparse content). `auto-fit`\n * collapses empty tracks and stretches present tiles to fill the freed\n * width, so the layout always uses the full screen regardless of how many\n * tiles are populated. `items-start` overrides grid's default row-stretch —\n * without it, a short tile sharing a row with a tall one gets stretched to\n * match, leaving dead space inside its own card (also reproduced).\n */\nexport function PortfolioOverview({\n tiles,\n isEmpty,\n emptyState,\n className,\n}: PortfolioOverviewProps) {\n if (isEmpty) {\n return <div className={className}>{emptyState}</div>;\n }\n\n const populatedTiles = tiles.filter((t) => !t.isEmpty);\n\n return (\n <div\n className={cn(\n \"grid gap-4 items-start grid-cols-[repeat(auto-fit,minmax(320px,1fr))]\",\n className,\n )}\n >\n {populatedTiles.map((tile) => (\n <PortfolioBentoTile\n key={tile.key}\n title={tile.title}\n href={tile.href}\n size={tile.size}\n >\n {tile.content}\n </PortfolioBentoTile>\n ))}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA8CW;AA5CX,gBAAmB;AACnB,kCAAiE;AAoC1D,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AACzB,MAAI,SAAS;AACX,WAAO,4CAAC,SAAI,WAAuB,sBAAW;AAAA,EAChD;AAEA,QAAM,iBAAiB,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO;AAErD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC,yBAAe,IAAI,CAAC,SACnB;AAAA,QAAC;AAAA;AAAA,UAEC,OAAO,KAAK;AAAA,UACZ,MAAM,KAAK;AAAA,UACX,MAAM,KAAK;AAAA,UAEV,eAAK;AAAA;AAAA,QALD,KAAK;AAAA,MAMZ,CACD;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -29,7 +29,9 @@ interface PortfolioOverviewProps {
|
|
|
29
29
|
* populated (a real, reproduced failure mode with sparse content). `auto-fit`
|
|
30
30
|
* collapses empty tracks and stretches present tiles to fill the freed
|
|
31
31
|
* width, so the layout always uses the full screen regardless of how many
|
|
32
|
-
* tiles are populated.
|
|
32
|
+
* tiles are populated. `items-start` overrides grid's default row-stretch —
|
|
33
|
+
* without it, a short tile sharing a row with a tall one gets stretched to
|
|
34
|
+
* match, leaving dead space inside its own card (also reproduced).
|
|
33
35
|
*/
|
|
34
36
|
declare function PortfolioOverview({ tiles, isEmpty, emptyState, className, }: PortfolioOverviewProps): react_jsx_runtime.JSX.Element;
|
|
35
37
|
|
|
@@ -29,7 +29,9 @@ interface PortfolioOverviewProps {
|
|
|
29
29
|
* populated (a real, reproduced failure mode with sparse content). `auto-fit`
|
|
30
30
|
* collapses empty tracks and stretches present tiles to fill the freed
|
|
31
31
|
* width, so the layout always uses the full screen regardless of how many
|
|
32
|
-
* tiles are populated.
|
|
32
|
+
* tiles are populated. `items-start` overrides grid's default row-stretch —
|
|
33
|
+
* without it, a short tile sharing a row with a tall one gets stretched to
|
|
34
|
+
* match, leaving dead space inside its own card (also reproduced).
|
|
33
35
|
*/
|
|
34
36
|
declare function PortfolioOverview({ tiles, isEmpty, emptyState, className, }: PortfolioOverviewProps): react_jsx_runtime.JSX.Element;
|
|
35
37
|
|
|
@@ -15,7 +15,10 @@ function PortfolioOverview({
|
|
|
15
15
|
return /* @__PURE__ */ jsx(
|
|
16
16
|
"div",
|
|
17
17
|
{
|
|
18
|
-
className: cn(
|
|
18
|
+
className: cn(
|
|
19
|
+
"grid gap-4 items-start grid-cols-[repeat(auto-fit,minmax(320px,1fr))]",
|
|
20
|
+
className
|
|
21
|
+
),
|
|
19
22
|
children: populatedTiles.map((tile) => /* @__PURE__ */ jsx(
|
|
20
23
|
PortfolioBentoTile,
|
|
21
24
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/portfolio-overview.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"../utils/cn.js\";\nimport { PortfolioBentoTile, type PortfolioBentoTileProps } from \"./portfolio-bento-tile.js\";\n\nexport interface PortfolioBentoTileConfig {\n key: string;\n title: string;\n href: string;\n size?: PortfolioBentoTileProps[\"size\"];\n /** Tiles with nothing to show are omitted entirely, not rendered empty. */\n isEmpty?: boolean;\n content: React.ReactNode;\n}\n\nexport interface PortfolioOverviewProps {\n tiles: PortfolioBentoTileConfig[];\n /** First-run state across the whole portfolio (zero holdings, zero activity). */\n isEmpty?: boolean;\n emptyState?: React.ReactNode;\n className?: string;\n}\n\n/**\n * Portfolio landing page: a content-first dashboard. Each tile is a fast\n * preview of one section, expanding into its full subpage via \"See all\".\n * Navigation across portfolio sections lives one level up, in the shared\n * portfolio chip bar rendered by the app's layout on every page — this\n * component is pure tile layout, nothing else.\n *\n * Uses a CSS grid with `auto-fit` tracks rather than multi-column masonry:\n * masonry's fixed column tracks leave dead blank space when few tiles are\n * populated (a real, reproduced failure mode with sparse content). `auto-fit`\n * collapses empty tracks and stretches present tiles to fill the freed\n * width, so the layout always uses the full screen regardless of how many\n * tiles are populated.\n */\nexport function PortfolioOverview({\n tiles,\n isEmpty,\n emptyState,\n className,\n}: PortfolioOverviewProps) {\n if (isEmpty) {\n return <div className={className}>{emptyState}</div>;\n }\n\n const populatedTiles = tiles.filter((t) => !t.isEmpty);\n\n return (\n <div\n className={cn(\"grid gap-4 grid-cols-[repeat(auto-fit,minmax(320px,1fr))]\"
|
|
1
|
+
{"version":3,"sources":["../../src/components/portfolio-overview.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"../utils/cn.js\";\nimport { PortfolioBentoTile, type PortfolioBentoTileProps } from \"./portfolio-bento-tile.js\";\n\nexport interface PortfolioBentoTileConfig {\n key: string;\n title: string;\n href: string;\n size?: PortfolioBentoTileProps[\"size\"];\n /** Tiles with nothing to show are omitted entirely, not rendered empty. */\n isEmpty?: boolean;\n content: React.ReactNode;\n}\n\nexport interface PortfolioOverviewProps {\n tiles: PortfolioBentoTileConfig[];\n /** First-run state across the whole portfolio (zero holdings, zero activity). */\n isEmpty?: boolean;\n emptyState?: React.ReactNode;\n className?: string;\n}\n\n/**\n * Portfolio landing page: a content-first dashboard. Each tile is a fast\n * preview of one section, expanding into its full subpage via \"See all\".\n * Navigation across portfolio sections lives one level up, in the shared\n * portfolio chip bar rendered by the app's layout on every page — this\n * component is pure tile layout, nothing else.\n *\n * Uses a CSS grid with `auto-fit` tracks rather than multi-column masonry:\n * masonry's fixed column tracks leave dead blank space when few tiles are\n * populated (a real, reproduced failure mode with sparse content). `auto-fit`\n * collapses empty tracks and stretches present tiles to fill the freed\n * width, so the layout always uses the full screen regardless of how many\n * tiles are populated. `items-start` overrides grid's default row-stretch —\n * without it, a short tile sharing a row with a tall one gets stretched to\n * match, leaving dead space inside its own card (also reproduced).\n */\nexport function PortfolioOverview({\n tiles,\n isEmpty,\n emptyState,\n className,\n}: PortfolioOverviewProps) {\n if (isEmpty) {\n return <div className={className}>{emptyState}</div>;\n }\n\n const populatedTiles = tiles.filter((t) => !t.isEmpty);\n\n return (\n <div\n className={cn(\n \"grid gap-4 items-start grid-cols-[repeat(auto-fit,minmax(320px,1fr))]\",\n className,\n )}\n >\n {populatedTiles.map((tile) => (\n <PortfolioBentoTile\n key={tile.key}\n title={tile.title}\n href={tile.href}\n size={tile.size}\n >\n {tile.content}\n </PortfolioBentoTile>\n ))}\n </div>\n );\n}\n"],"mappings":";AA8CW;AA5CX,SAAS,UAAU;AACnB,SAAS,0BAAwD;AAoC1D,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AACzB,MAAI,SAAS;AACX,WAAO,oBAAC,SAAI,WAAuB,sBAAW;AAAA,EAChD;AAEA,QAAM,iBAAiB,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO;AAErD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC,yBAAe,IAAI,CAAC,SACnB;AAAA,QAAC;AAAA;AAAA,UAEC,OAAO,KAAK;AAAA,UACZ,MAAM,KAAK;AAAA,UACX,MAAM,KAAK;AAAA,UAEV,eAAK;AAAA;AAAA,QALD,KAAK;AAAA,MAMZ,CACD;AAAA;AAAA,EACH;AAEJ;","names":[]}
|