@medialane/ui 0.74.2 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var collection_filters_exports = {};
21
+ __export(collection_filters_exports, {
22
+ CollectionFiltersBody: () => CollectionFiltersBody,
23
+ CollectionFiltersTrigger: () => CollectionFiltersTrigger,
24
+ SORT_OPTIONS: () => SORT_OPTIONS,
25
+ useCollectionFilters: () => useCollectionFilters
26
+ });
27
+ module.exports = __toCommonJS(collection_filters_exports);
28
+ var import_jsx_runtime = require("react/jsx-runtime");
29
+ var import_react = require("react");
30
+ var import_lucide_react = require("lucide-react");
31
+ var import_cn = require("../utils/cn.js");
32
+ const EXCLUDED_TRAIT_TYPES = /* @__PURE__ */ new Set(["registration"]);
33
+ const SORT_OPTIONS = [
34
+ { value: "recent", label: "Recent" },
35
+ { value: "oldest", label: "Oldest" },
36
+ { value: "name", label: "Name" },
37
+ { value: "price", label: "Price" }
38
+ ];
39
+ function useCollectionFilters(tokens, selected, onChange, sort, onSortChange) {
40
+ const traitSections = (0, import_react.useMemo)(() => {
41
+ const map = /* @__PURE__ */ new Map();
42
+ for (const token of tokens) {
43
+ const attrs = Array.isArray(token.metadata?.attributes) ? token.metadata.attributes : [];
44
+ for (const attr of attrs) {
45
+ if (!attr.trait_type || attr.value == null) continue;
46
+ if (EXCLUDED_TRAIT_TYPES.has(attr.trait_type.toLowerCase())) continue;
47
+ if (!map.has(attr.trait_type)) map.set(attr.trait_type, /* @__PURE__ */ new Map());
48
+ const counts = map.get(attr.trait_type);
49
+ const v = String(attr.value);
50
+ counts.set(v, (counts.get(v) ?? 0) + 1);
51
+ }
52
+ }
53
+ return Array.from(map.entries()).map(([traitType, counts]) => ({
54
+ traitType,
55
+ values: Array.from(counts.entries()).map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value))
56
+ })).filter((section) => section.values.length >= 2);
57
+ }, [tokens]);
58
+ const activeEntries = Object.entries(selected).flatMap(
59
+ ([traitType, values]) => values.map((value) => ({ traitType, value }))
60
+ );
61
+ const traitActiveCount = activeEntries.length;
62
+ const sortIsDefault = sort === "recent";
63
+ const totalActiveCount = traitActiveCount + (sortIsDefault ? 0 : 1);
64
+ function toggleValue(traitType, value) {
65
+ const current = selected[traitType] ?? [];
66
+ const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
67
+ const nextSelected = { ...selected };
68
+ if (next.length === 0) {
69
+ delete nextSelected[traitType];
70
+ } else {
71
+ nextSelected[traitType] = next;
72
+ }
73
+ onChange(nextSelected);
74
+ }
75
+ function clearAll() {
76
+ onChange({});
77
+ onSortChange("recent");
78
+ }
79
+ function removeFilter(traitType, value) {
80
+ toggleValue(traitType, value);
81
+ }
82
+ return { traitSections, activeEntries, totalActiveCount, sortIsDefault, toggleValue, clearAll, removeFilter };
83
+ }
84
+ const TRIGGER_BTN = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-input bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground h-8 px-3 shrink-0";
85
+ function CollectionFiltersTrigger({
86
+ totalActiveCount,
87
+ sortIsDefault,
88
+ sort,
89
+ activeEntries,
90
+ onOpen,
91
+ onSortReset,
92
+ onRemoveFilter,
93
+ onClearAll
94
+ }) {
95
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-end gap-2 flex-wrap", children: [
96
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { onClick: onOpen, className: TRIGGER_BTN, children: [
97
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.SlidersHorizontal, { className: "h-3.5 w-3.5" }),
98
+ "Filters",
99
+ totalActiveCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "h-4 min-w-4 rounded-full bg-primary text-[10px] font-bold text-primary-foreground flex items-center justify-center px-1 -mr-1", children: totalActiveCount })
100
+ ] }),
101
+ !sortIsDefault && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
102
+ "button",
103
+ {
104
+ onClick: onSortReset,
105
+ className: "inline-flex items-center gap-1 h-8 px-2.5 rounded-md border border-primary/40 bg-primary/10 text-xs font-medium text-foreground hover:bg-primary/20 transition-colors",
106
+ children: [
107
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground text-[11px]", children: "Sort:" }),
108
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: SORT_OPTIONS.find((o) => o.value === sort)?.label }),
109
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-3 w-3 text-muted-foreground ml-0.5" })
110
+ ]
111
+ }
112
+ ),
113
+ activeEntries.map(({ traitType, value }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
114
+ "button",
115
+ {
116
+ onClick: () => onRemoveFilter(traitType, value),
117
+ className: "inline-flex items-center gap-1 h-8 px-2.5 rounded-md border border-primary/40 bg-primary/10 text-xs font-medium text-foreground hover:bg-primary/20 transition-colors",
118
+ children: [
119
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-muted-foreground text-[11px]", children: [
120
+ traitType,
121
+ ":"
122
+ ] }),
123
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: value }),
124
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-3 w-3 text-muted-foreground ml-0.5" })
125
+ ]
126
+ },
127
+ `${traitType}:${value}`
128
+ )),
129
+ totalActiveCount > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
130
+ "button",
131
+ {
132
+ onClick: onClearAll,
133
+ className: "text-xs text-muted-foreground hover:text-foreground underline-offset-2 hover:underline transition-colors",
134
+ children: "Clear all"
135
+ }
136
+ )
137
+ ] });
138
+ }
139
+ const PILL_BASE = "inline-flex items-center gap-1 h-7 px-2.5 rounded-full border text-[12px] font-medium transition-colors";
140
+ const PILL_ON = "border-primary bg-primary text-primary-foreground";
141
+ const PILL_OFF = "border-border bg-transparent text-muted-foreground hover:text-foreground hover:border-foreground/30";
142
+ function CollectionFiltersBody({
143
+ sort,
144
+ onSortChange,
145
+ traitSections,
146
+ selected,
147
+ onToggleValue
148
+ }) {
149
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex-1 overflow-y-auto px-5 py-3 space-y-4", children: [
150
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
151
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mb-2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm font-medium", children: "Sort by" }) }),
152
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-1.5", children: SORT_OPTIONS.map(({ value, label }) => {
153
+ const isSelected = sort === value;
154
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
155
+ "button",
156
+ {
157
+ onClick: () => onSortChange(value),
158
+ className: (0, import_cn.cn)(PILL_BASE, isSelected ? PILL_ON : PILL_OFF),
159
+ children: [
160
+ isSelected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Check, { className: "h-3 w-3" }),
161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: label })
162
+ ]
163
+ },
164
+ value
165
+ );
166
+ }) })
167
+ ] }),
168
+ traitSections.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
169
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-px w-full bg-border" }),
170
+ traitSections.map((section, i) => {
171
+ const activeValues = selected[section.traitType] ?? [];
172
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
173
+ i > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-px w-full bg-border mb-4" }),
174
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [
175
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm font-medium", children: section.traitType }),
176
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-[11px] text-muted-foreground", children: [
177
+ section.values.length,
178
+ " values"
179
+ ] })
180
+ ] }),
181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-1.5", children: section.values.map(({ value, count }) => {
182
+ const isSelected = activeValues.includes(value);
183
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
184
+ "button",
185
+ {
186
+ onClick: () => onToggleValue(section.traitType, value),
187
+ className: (0, import_cn.cn)(PILL_BASE, isSelected ? PILL_ON : PILL_OFF),
188
+ children: [
189
+ isSelected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Check, { className: "h-3 w-3" }),
190
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: value }),
191
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: isSelected ? "opacity-80" : "opacity-60", children: count })
192
+ ]
193
+ },
194
+ value
195
+ );
196
+ }) })
197
+ ] }, section.traitType);
198
+ })
199
+ ] })
200
+ ] });
201
+ }
202
+ // Annotate the CommonJS export names for ESM import in node:
203
+ 0 && (module.exports = {
204
+ CollectionFiltersBody,
205
+ CollectionFiltersTrigger,
206
+ SORT_OPTIONS,
207
+ useCollectionFilters
208
+ });
209
+ //# sourceMappingURL=collection-filters.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/collection-filters.tsx"],"sourcesContent":["\"use client\";\n\nimport { useMemo } from \"react\";\nimport { SlidersHorizontal, X, Check } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport type { ApiToken, CollectionTokensSort } from \"@medialane/sdk\";\n\n// Trait types that are per-token stamps rather than real categories — never\n// useful as a filter regardless of how many distinct values they have.\n// \"Registration\" is the Berne Convention registration date, near-unique per\n// mint; filtering by an exact date isn't a meaningful narrowing action.\nconst EXCLUDED_TRAIT_TYPES = new Set([\"registration\"]);\n\nexport const SORT_OPTIONS: { value: CollectionTokensSort; label: string }[] = [\n { value: \"recent\", label: \"Recent\" },\n { value: \"oldest\", label: \"Oldest\" },\n { value: \"name\", label: \"Name\" },\n { value: \"price\", label: \"Price\" },\n];\n\nexport interface TraitSection {\n traitType: string;\n values: { value: string; count: number }[];\n}\n\nexport function useCollectionFilters(\n tokens: ApiToken[],\n selected: Record<string, string[]>,\n onChange: (filters: Record<string, string[]>) => void,\n sort: CollectionTokensSort,\n onSortChange: (sort: CollectionTokensSort) => void,\n) {\n // Build trait map with value counts from loaded tokens, then drop any\n // trait type where every token shares the same single value (can never\n // narrow the result set) or that's a known per-token stamp, not a category.\n const traitSections: TraitSection[] = useMemo(() => {\n const map = new Map<string, Map<string, number>>();\n for (const token of tokens) {\n const attrs = Array.isArray(token.metadata?.attributes)\n ? (token.metadata.attributes as { trait_type?: string; value?: string }[])\n : [];\n for (const attr of attrs) {\n if (!attr.trait_type || attr.value == null) continue;\n if (EXCLUDED_TRAIT_TYPES.has(attr.trait_type.toLowerCase())) continue;\n if (!map.has(attr.trait_type)) map.set(attr.trait_type, new Map());\n const counts = map.get(attr.trait_type)!;\n const v = String(attr.value);\n counts.set(v, (counts.get(v) ?? 0) + 1);\n }\n }\n return Array.from(map.entries())\n .map(([traitType, counts]) => ({\n traitType,\n values: Array.from(counts.entries())\n .map(([value, count]) => ({ value, count }))\n .sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)),\n }))\n .filter((section) => section.values.length >= 2);\n }, [tokens]);\n\n const activeEntries = Object.entries(selected).flatMap(([traitType, values]) =>\n values.map((value) => ({ traitType, value }))\n );\n const traitActiveCount = activeEntries.length;\n const sortIsDefault = sort === \"recent\";\n const totalActiveCount = traitActiveCount + (sortIsDefault ? 0 : 1);\n\n function toggleValue(traitType: string, value: string) {\n const current = selected[traitType] ?? [];\n const next = current.includes(value)\n ? current.filter((v) => v !== value)\n : [...current, value];\n const nextSelected = { ...selected };\n if (next.length === 0) {\n delete nextSelected[traitType];\n } else {\n nextSelected[traitType] = next;\n }\n onChange(nextSelected);\n }\n\n function clearAll() {\n onChange({});\n onSortChange(\"recent\");\n }\n\n function removeFilter(traitType: string, value: string) {\n toggleValue(traitType, value);\n }\n\n return { traitSections, activeEntries, totalActiveCount, sortIsDefault, toggleValue, clearAll, removeFilter };\n}\n\nconst TRIGGER_BTN = \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-input bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground h-8 px-3 shrink-0\";\n\nexport interface CollectionFiltersTriggerProps {\n totalActiveCount: number;\n sortIsDefault: boolean;\n sort: CollectionTokensSort;\n activeEntries: { traitType: string; value: string }[];\n onOpen: () => void;\n onSortReset: () => void;\n onRemoveFilter: (traitType: string, value: string) => void;\n onClearAll: () => void;\n}\n\nexport function CollectionFiltersTrigger({\n totalActiveCount,\n sortIsDefault,\n sort,\n activeEntries,\n onOpen,\n onSortReset,\n onRemoveFilter,\n onClearAll,\n}: CollectionFiltersTriggerProps) {\n return (\n <div className=\"flex items-center justify-end gap-2 flex-wrap\">\n <button onClick={onOpen} className={TRIGGER_BTN}>\n <SlidersHorizontal className=\"h-3.5 w-3.5\" />\n Filters\n {totalActiveCount > 0 && (\n <span className=\"h-4 min-w-4 rounded-full bg-primary text-[10px] font-bold text-primary-foreground flex items-center justify-center px-1 -mr-1\">\n {totalActiveCount}\n </span>\n )}\n </button>\n\n {/* Sort pill — only shown when not the default */}\n {!sortIsDefault && (\n <button\n onClick={onSortReset}\n className=\"inline-flex items-center gap-1 h-8 px-2.5 rounded-md border border-primary/40 bg-primary/10 text-xs font-medium text-foreground hover:bg-primary/20 transition-colors\"\n >\n <span className=\"text-muted-foreground text-[11px]\">Sort:</span>\n <span>{SORT_OPTIONS.find((o) => o.value === sort)?.label}</span>\n <X className=\"h-3 w-3 text-muted-foreground ml-0.5\" />\n </button>\n )}\n\n {/* Active trait pills — one per selected value */}\n {activeEntries.map(({ traitType, value }) => (\n <button\n key={`${traitType}:${value}`}\n onClick={() => onRemoveFilter(traitType, value)}\n className=\"inline-flex items-center gap-1 h-8 px-2.5 rounded-md border border-primary/40 bg-primary/10 text-xs font-medium text-foreground hover:bg-primary/20 transition-colors\"\n >\n <span className=\"text-muted-foreground text-[11px]\">{traitType}:</span>\n <span>{value}</span>\n <X className=\"h-3 w-3 text-muted-foreground ml-0.5\" />\n </button>\n ))}\n\n {totalActiveCount > 1 && (\n <button\n onClick={onClearAll}\n className=\"text-xs text-muted-foreground hover:text-foreground underline-offset-2 hover:underline transition-colors\"\n >\n Clear all\n </button>\n )}\n </div>\n );\n}\n\nexport interface CollectionFiltersBodyProps {\n sort: CollectionTokensSort;\n onSortChange: (sort: CollectionTokensSort) => void;\n traitSections: TraitSection[];\n selected: Record<string, string[]>;\n onToggleValue: (traitType: string, value: string) => void;\n}\n\nconst PILL_BASE = \"inline-flex items-center gap-1 h-7 px-2.5 rounded-full border text-[12px] font-medium transition-colors\";\nconst PILL_ON = \"border-primary bg-primary text-primary-foreground\";\nconst PILL_OFF = \"border-border bg-transparent text-muted-foreground hover:text-foreground hover:border-foreground/30\";\n\nexport function CollectionFiltersBody({\n sort,\n onSortChange,\n traitSections,\n selected,\n onToggleValue,\n}: CollectionFiltersBodyProps) {\n return (\n <div className=\"flex-1 overflow-y-auto px-5 py-3 space-y-4\">\n {/* Sort section */}\n <div>\n <div className=\"mb-2\">\n <span className=\"text-sm font-medium\">Sort by</span>\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {SORT_OPTIONS.map(({ value, label }) => {\n const isSelected = sort === value;\n return (\n <button\n key={value}\n onClick={() => onSortChange(value)}\n className={cn(PILL_BASE, isSelected ? PILL_ON : PILL_OFF)}\n >\n {isSelected && <Check className=\"h-3 w-3\" />}\n <span>{label}</span>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Trait sections — always expanded (pruning already removed the noise) */}\n {traitSections.length > 0 && (\n <>\n <div className=\"h-px w-full bg-border\" />\n {traitSections.map((section, i) => {\n const activeValues = selected[section.traitType] ?? [];\n return (\n <div key={section.traitType}>\n {i > 0 && <div className=\"h-px w-full bg-border mb-4\" />}\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm font-medium\">{section.traitType}</span>\n <span className=\"text-[11px] text-muted-foreground\">\n {section.values.length} values\n </span>\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {section.values.map(({ value, count }) => {\n const isSelected = activeValues.includes(value);\n return (\n <button\n key={value}\n onClick={() => onToggleValue(section.traitType, value)}\n className={cn(PILL_BASE, isSelected ? PILL_ON : PILL_OFF)}\n >\n {isSelected && <Check className=\"h-3 w-3\" />}\n <span>{value}</span>\n <span className={isSelected ? \"opacity-80\" : \"opacity-60\"}>\n {count}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n );\n })}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,50 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { CollectionTokensSort, ApiToken } from '@medialane/sdk';
3
+
4
+ declare const SORT_OPTIONS: {
5
+ value: CollectionTokensSort;
6
+ label: string;
7
+ }[];
8
+ interface TraitSection {
9
+ traitType: string;
10
+ values: {
11
+ value: string;
12
+ count: number;
13
+ }[];
14
+ }
15
+ declare function useCollectionFilters(tokens: ApiToken[], selected: Record<string, string[]>, onChange: (filters: Record<string, string[]>) => void, sort: CollectionTokensSort, onSortChange: (sort: CollectionTokensSort) => void): {
16
+ traitSections: TraitSection[];
17
+ activeEntries: {
18
+ traitType: string;
19
+ value: string;
20
+ }[];
21
+ totalActiveCount: number;
22
+ sortIsDefault: boolean;
23
+ toggleValue: (traitType: string, value: string) => void;
24
+ clearAll: () => void;
25
+ removeFilter: (traitType: string, value: string) => void;
26
+ };
27
+ interface CollectionFiltersTriggerProps {
28
+ totalActiveCount: number;
29
+ sortIsDefault: boolean;
30
+ sort: CollectionTokensSort;
31
+ activeEntries: {
32
+ traitType: string;
33
+ value: string;
34
+ }[];
35
+ onOpen: () => void;
36
+ onSortReset: () => void;
37
+ onRemoveFilter: (traitType: string, value: string) => void;
38
+ onClearAll: () => void;
39
+ }
40
+ declare function CollectionFiltersTrigger({ totalActiveCount, sortIsDefault, sort, activeEntries, onOpen, onSortReset, onRemoveFilter, onClearAll, }: CollectionFiltersTriggerProps): react_jsx_runtime.JSX.Element;
41
+ interface CollectionFiltersBodyProps {
42
+ sort: CollectionTokensSort;
43
+ onSortChange: (sort: CollectionTokensSort) => void;
44
+ traitSections: TraitSection[];
45
+ selected: Record<string, string[]>;
46
+ onToggleValue: (traitType: string, value: string) => void;
47
+ }
48
+ declare function CollectionFiltersBody({ sort, onSortChange, traitSections, selected, onToggleValue, }: CollectionFiltersBodyProps): react_jsx_runtime.JSX.Element;
49
+
50
+ export { CollectionFiltersBody, type CollectionFiltersBodyProps, CollectionFiltersTrigger, type CollectionFiltersTriggerProps, SORT_OPTIONS, type TraitSection, useCollectionFilters };
@@ -0,0 +1,50 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { CollectionTokensSort, ApiToken } from '@medialane/sdk';
3
+
4
+ declare const SORT_OPTIONS: {
5
+ value: CollectionTokensSort;
6
+ label: string;
7
+ }[];
8
+ interface TraitSection {
9
+ traitType: string;
10
+ values: {
11
+ value: string;
12
+ count: number;
13
+ }[];
14
+ }
15
+ declare function useCollectionFilters(tokens: ApiToken[], selected: Record<string, string[]>, onChange: (filters: Record<string, string[]>) => void, sort: CollectionTokensSort, onSortChange: (sort: CollectionTokensSort) => void): {
16
+ traitSections: TraitSection[];
17
+ activeEntries: {
18
+ traitType: string;
19
+ value: string;
20
+ }[];
21
+ totalActiveCount: number;
22
+ sortIsDefault: boolean;
23
+ toggleValue: (traitType: string, value: string) => void;
24
+ clearAll: () => void;
25
+ removeFilter: (traitType: string, value: string) => void;
26
+ };
27
+ interface CollectionFiltersTriggerProps {
28
+ totalActiveCount: number;
29
+ sortIsDefault: boolean;
30
+ sort: CollectionTokensSort;
31
+ activeEntries: {
32
+ traitType: string;
33
+ value: string;
34
+ }[];
35
+ onOpen: () => void;
36
+ onSortReset: () => void;
37
+ onRemoveFilter: (traitType: string, value: string) => void;
38
+ onClearAll: () => void;
39
+ }
40
+ declare function CollectionFiltersTrigger({ totalActiveCount, sortIsDefault, sort, activeEntries, onOpen, onSortReset, onRemoveFilter, onClearAll, }: CollectionFiltersTriggerProps): react_jsx_runtime.JSX.Element;
41
+ interface CollectionFiltersBodyProps {
42
+ sort: CollectionTokensSort;
43
+ onSortChange: (sort: CollectionTokensSort) => void;
44
+ traitSections: TraitSection[];
45
+ selected: Record<string, string[]>;
46
+ onToggleValue: (traitType: string, value: string) => void;
47
+ }
48
+ declare function CollectionFiltersBody({ sort, onSortChange, traitSections, selected, onToggleValue, }: CollectionFiltersBodyProps): react_jsx_runtime.JSX.Element;
49
+
50
+ export { CollectionFiltersBody, type CollectionFiltersBodyProps, CollectionFiltersTrigger, type CollectionFiltersTriggerProps, SORT_OPTIONS, type TraitSection, useCollectionFilters };
@@ -0,0 +1,182 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { SlidersHorizontal, X, Check } from "lucide-react";
5
+ import { cn } from "../utils/cn.js";
6
+ const EXCLUDED_TRAIT_TYPES = /* @__PURE__ */ new Set(["registration"]);
7
+ const SORT_OPTIONS = [
8
+ { value: "recent", label: "Recent" },
9
+ { value: "oldest", label: "Oldest" },
10
+ { value: "name", label: "Name" },
11
+ { value: "price", label: "Price" }
12
+ ];
13
+ function useCollectionFilters(tokens, selected, onChange, sort, onSortChange) {
14
+ const traitSections = useMemo(() => {
15
+ const map = /* @__PURE__ */ new Map();
16
+ for (const token of tokens) {
17
+ const attrs = Array.isArray(token.metadata?.attributes) ? token.metadata.attributes : [];
18
+ for (const attr of attrs) {
19
+ if (!attr.trait_type || attr.value == null) continue;
20
+ if (EXCLUDED_TRAIT_TYPES.has(attr.trait_type.toLowerCase())) continue;
21
+ if (!map.has(attr.trait_type)) map.set(attr.trait_type, /* @__PURE__ */ new Map());
22
+ const counts = map.get(attr.trait_type);
23
+ const v = String(attr.value);
24
+ counts.set(v, (counts.get(v) ?? 0) + 1);
25
+ }
26
+ }
27
+ return Array.from(map.entries()).map(([traitType, counts]) => ({
28
+ traitType,
29
+ values: Array.from(counts.entries()).map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value))
30
+ })).filter((section) => section.values.length >= 2);
31
+ }, [tokens]);
32
+ const activeEntries = Object.entries(selected).flatMap(
33
+ ([traitType, values]) => values.map((value) => ({ traitType, value }))
34
+ );
35
+ const traitActiveCount = activeEntries.length;
36
+ const sortIsDefault = sort === "recent";
37
+ const totalActiveCount = traitActiveCount + (sortIsDefault ? 0 : 1);
38
+ function toggleValue(traitType, value) {
39
+ const current = selected[traitType] ?? [];
40
+ const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
41
+ const nextSelected = { ...selected };
42
+ if (next.length === 0) {
43
+ delete nextSelected[traitType];
44
+ } else {
45
+ nextSelected[traitType] = next;
46
+ }
47
+ onChange(nextSelected);
48
+ }
49
+ function clearAll() {
50
+ onChange({});
51
+ onSortChange("recent");
52
+ }
53
+ function removeFilter(traitType, value) {
54
+ toggleValue(traitType, value);
55
+ }
56
+ return { traitSections, activeEntries, totalActiveCount, sortIsDefault, toggleValue, clearAll, removeFilter };
57
+ }
58
+ const TRIGGER_BTN = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-input bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground h-8 px-3 shrink-0";
59
+ function CollectionFiltersTrigger({
60
+ totalActiveCount,
61
+ sortIsDefault,
62
+ sort,
63
+ activeEntries,
64
+ onOpen,
65
+ onSortReset,
66
+ onRemoveFilter,
67
+ onClearAll
68
+ }) {
69
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 flex-wrap", children: [
70
+ /* @__PURE__ */ jsxs("button", { onClick: onOpen, className: TRIGGER_BTN, children: [
71
+ /* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }),
72
+ "Filters",
73
+ totalActiveCount > 0 && /* @__PURE__ */ jsx("span", { className: "h-4 min-w-4 rounded-full bg-primary text-[10px] font-bold text-primary-foreground flex items-center justify-center px-1 -mr-1", children: totalActiveCount })
74
+ ] }),
75
+ !sortIsDefault && /* @__PURE__ */ jsxs(
76
+ "button",
77
+ {
78
+ onClick: onSortReset,
79
+ className: "inline-flex items-center gap-1 h-8 px-2.5 rounded-md border border-primary/40 bg-primary/10 text-xs font-medium text-foreground hover:bg-primary/20 transition-colors",
80
+ children: [
81
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-[11px]", children: "Sort:" }),
82
+ /* @__PURE__ */ jsx("span", { children: SORT_OPTIONS.find((o) => o.value === sort)?.label }),
83
+ /* @__PURE__ */ jsx(X, { className: "h-3 w-3 text-muted-foreground ml-0.5" })
84
+ ]
85
+ }
86
+ ),
87
+ activeEntries.map(({ traitType, value }) => /* @__PURE__ */ jsxs(
88
+ "button",
89
+ {
90
+ onClick: () => onRemoveFilter(traitType, value),
91
+ className: "inline-flex items-center gap-1 h-8 px-2.5 rounded-md border border-primary/40 bg-primary/10 text-xs font-medium text-foreground hover:bg-primary/20 transition-colors",
92
+ children: [
93
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-[11px]", children: [
94
+ traitType,
95
+ ":"
96
+ ] }),
97
+ /* @__PURE__ */ jsx("span", { children: value }),
98
+ /* @__PURE__ */ jsx(X, { className: "h-3 w-3 text-muted-foreground ml-0.5" })
99
+ ]
100
+ },
101
+ `${traitType}:${value}`
102
+ )),
103
+ totalActiveCount > 1 && /* @__PURE__ */ jsx(
104
+ "button",
105
+ {
106
+ onClick: onClearAll,
107
+ className: "text-xs text-muted-foreground hover:text-foreground underline-offset-2 hover:underline transition-colors",
108
+ children: "Clear all"
109
+ }
110
+ )
111
+ ] });
112
+ }
113
+ const PILL_BASE = "inline-flex items-center gap-1 h-7 px-2.5 rounded-full border text-[12px] font-medium transition-colors";
114
+ const PILL_ON = "border-primary bg-primary text-primary-foreground";
115
+ const PILL_OFF = "border-border bg-transparent text-muted-foreground hover:text-foreground hover:border-foreground/30";
116
+ function CollectionFiltersBody({
117
+ sort,
118
+ onSortChange,
119
+ traitSections,
120
+ selected,
121
+ onToggleValue
122
+ }) {
123
+ return /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto px-5 py-3 space-y-4", children: [
124
+ /* @__PURE__ */ jsxs("div", { children: [
125
+ /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Sort by" }) }),
126
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: SORT_OPTIONS.map(({ value, label }) => {
127
+ const isSelected = sort === value;
128
+ return /* @__PURE__ */ jsxs(
129
+ "button",
130
+ {
131
+ onClick: () => onSortChange(value),
132
+ className: cn(PILL_BASE, isSelected ? PILL_ON : PILL_OFF),
133
+ children: [
134
+ isSelected && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }),
135
+ /* @__PURE__ */ jsx("span", { children: label })
136
+ ]
137
+ },
138
+ value
139
+ );
140
+ }) })
141
+ ] }),
142
+ traitSections.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
143
+ /* @__PURE__ */ jsx("div", { className: "h-px w-full bg-border" }),
144
+ traitSections.map((section, i) => {
145
+ const activeValues = selected[section.traitType] ?? [];
146
+ return /* @__PURE__ */ jsxs("div", { children: [
147
+ i > 0 && /* @__PURE__ */ jsx("div", { className: "h-px w-full bg-border mb-4" }),
148
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
149
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: section.traitType }),
150
+ /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-muted-foreground", children: [
151
+ section.values.length,
152
+ " values"
153
+ ] })
154
+ ] }),
155
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: section.values.map(({ value, count }) => {
156
+ const isSelected = activeValues.includes(value);
157
+ return /* @__PURE__ */ jsxs(
158
+ "button",
159
+ {
160
+ onClick: () => onToggleValue(section.traitType, value),
161
+ className: cn(PILL_BASE, isSelected ? PILL_ON : PILL_OFF),
162
+ children: [
163
+ isSelected && /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }),
164
+ /* @__PURE__ */ jsx("span", { children: value }),
165
+ /* @__PURE__ */ jsx("span", { className: isSelected ? "opacity-80" : "opacity-60", children: count })
166
+ ]
167
+ },
168
+ value
169
+ );
170
+ }) })
171
+ ] }, section.traitType);
172
+ })
173
+ ] })
174
+ ] });
175
+ }
176
+ export {
177
+ CollectionFiltersBody,
178
+ CollectionFiltersTrigger,
179
+ SORT_OPTIONS,
180
+ useCollectionFilters
181
+ };
182
+ //# sourceMappingURL=collection-filters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/collection-filters.tsx"],"sourcesContent":["\"use client\";\n\nimport { useMemo } from \"react\";\nimport { SlidersHorizontal, X, Check } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport type { ApiToken, CollectionTokensSort } from \"@medialane/sdk\";\n\n// Trait types that are per-token stamps rather than real categories — never\n// useful as a filter regardless of how many distinct values they have.\n// \"Registration\" is the Berne Convention registration date, near-unique per\n// mint; filtering by an exact date isn't a meaningful narrowing action.\nconst EXCLUDED_TRAIT_TYPES = new Set([\"registration\"]);\n\nexport const SORT_OPTIONS: { value: CollectionTokensSort; label: string }[] = [\n { value: \"recent\", label: \"Recent\" },\n { value: \"oldest\", label: \"Oldest\" },\n { value: \"name\", label: \"Name\" },\n { value: \"price\", label: \"Price\" },\n];\n\nexport interface TraitSection {\n traitType: string;\n values: { value: string; count: number }[];\n}\n\nexport function useCollectionFilters(\n tokens: ApiToken[],\n selected: Record<string, string[]>,\n onChange: (filters: Record<string, string[]>) => void,\n sort: CollectionTokensSort,\n onSortChange: (sort: CollectionTokensSort) => void,\n) {\n // Build trait map with value counts from loaded tokens, then drop any\n // trait type where every token shares the same single value (can never\n // narrow the result set) or that's a known per-token stamp, not a category.\n const traitSections: TraitSection[] = useMemo(() => {\n const map = new Map<string, Map<string, number>>();\n for (const token of tokens) {\n const attrs = Array.isArray(token.metadata?.attributes)\n ? (token.metadata.attributes as { trait_type?: string; value?: string }[])\n : [];\n for (const attr of attrs) {\n if (!attr.trait_type || attr.value == null) continue;\n if (EXCLUDED_TRAIT_TYPES.has(attr.trait_type.toLowerCase())) continue;\n if (!map.has(attr.trait_type)) map.set(attr.trait_type, new Map());\n const counts = map.get(attr.trait_type)!;\n const v = String(attr.value);\n counts.set(v, (counts.get(v) ?? 0) + 1);\n }\n }\n return Array.from(map.entries())\n .map(([traitType, counts]) => ({\n traitType,\n values: Array.from(counts.entries())\n .map(([value, count]) => ({ value, count }))\n .sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)),\n }))\n .filter((section) => section.values.length >= 2);\n }, [tokens]);\n\n const activeEntries = Object.entries(selected).flatMap(([traitType, values]) =>\n values.map((value) => ({ traitType, value }))\n );\n const traitActiveCount = activeEntries.length;\n const sortIsDefault = sort === \"recent\";\n const totalActiveCount = traitActiveCount + (sortIsDefault ? 0 : 1);\n\n function toggleValue(traitType: string, value: string) {\n const current = selected[traitType] ?? [];\n const next = current.includes(value)\n ? current.filter((v) => v !== value)\n : [...current, value];\n const nextSelected = { ...selected };\n if (next.length === 0) {\n delete nextSelected[traitType];\n } else {\n nextSelected[traitType] = next;\n }\n onChange(nextSelected);\n }\n\n function clearAll() {\n onChange({});\n onSortChange(\"recent\");\n }\n\n function removeFilter(traitType: string, value: string) {\n toggleValue(traitType, value);\n }\n\n return { traitSections, activeEntries, totalActiveCount, sortIsDefault, toggleValue, clearAll, removeFilter };\n}\n\nconst TRIGGER_BTN = \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-input bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground h-8 px-3 shrink-0\";\n\nexport interface CollectionFiltersTriggerProps {\n totalActiveCount: number;\n sortIsDefault: boolean;\n sort: CollectionTokensSort;\n activeEntries: { traitType: string; value: string }[];\n onOpen: () => void;\n onSortReset: () => void;\n onRemoveFilter: (traitType: string, value: string) => void;\n onClearAll: () => void;\n}\n\nexport function CollectionFiltersTrigger({\n totalActiveCount,\n sortIsDefault,\n sort,\n activeEntries,\n onOpen,\n onSortReset,\n onRemoveFilter,\n onClearAll,\n}: CollectionFiltersTriggerProps) {\n return (\n <div className=\"flex items-center justify-end gap-2 flex-wrap\">\n <button onClick={onOpen} className={TRIGGER_BTN}>\n <SlidersHorizontal className=\"h-3.5 w-3.5\" />\n Filters\n {totalActiveCount > 0 && (\n <span className=\"h-4 min-w-4 rounded-full bg-primary text-[10px] font-bold text-primary-foreground flex items-center justify-center px-1 -mr-1\">\n {totalActiveCount}\n </span>\n )}\n </button>\n\n {/* Sort pill — only shown when not the default */}\n {!sortIsDefault && (\n <button\n onClick={onSortReset}\n className=\"inline-flex items-center gap-1 h-8 px-2.5 rounded-md border border-primary/40 bg-primary/10 text-xs font-medium text-foreground hover:bg-primary/20 transition-colors\"\n >\n <span className=\"text-muted-foreground text-[11px]\">Sort:</span>\n <span>{SORT_OPTIONS.find((o) => o.value === sort)?.label}</span>\n <X className=\"h-3 w-3 text-muted-foreground ml-0.5\" />\n </button>\n )}\n\n {/* Active trait pills — one per selected value */}\n {activeEntries.map(({ traitType, value }) => (\n <button\n key={`${traitType}:${value}`}\n onClick={() => onRemoveFilter(traitType, value)}\n className=\"inline-flex items-center gap-1 h-8 px-2.5 rounded-md border border-primary/40 bg-primary/10 text-xs font-medium text-foreground hover:bg-primary/20 transition-colors\"\n >\n <span className=\"text-muted-foreground text-[11px]\">{traitType}:</span>\n <span>{value}</span>\n <X className=\"h-3 w-3 text-muted-foreground ml-0.5\" />\n </button>\n ))}\n\n {totalActiveCount > 1 && (\n <button\n onClick={onClearAll}\n className=\"text-xs text-muted-foreground hover:text-foreground underline-offset-2 hover:underline transition-colors\"\n >\n Clear all\n </button>\n )}\n </div>\n );\n}\n\nexport interface CollectionFiltersBodyProps {\n sort: CollectionTokensSort;\n onSortChange: (sort: CollectionTokensSort) => void;\n traitSections: TraitSection[];\n selected: Record<string, string[]>;\n onToggleValue: (traitType: string, value: string) => void;\n}\n\nconst PILL_BASE = \"inline-flex items-center gap-1 h-7 px-2.5 rounded-full border text-[12px] font-medium transition-colors\";\nconst PILL_ON = \"border-primary bg-primary text-primary-foreground\";\nconst PILL_OFF = \"border-border bg-transparent text-muted-foreground hover:text-foreground hover:border-foreground/30\";\n\nexport function CollectionFiltersBody({\n sort,\n onSortChange,\n traitSections,\n selected,\n onToggleValue,\n}: CollectionFiltersBodyProps) {\n return (\n <div className=\"flex-1 overflow-y-auto px-5 py-3 space-y-4\">\n {/* Sort section */}\n <div>\n <div className=\"mb-2\">\n <span className=\"text-sm font-medium\">Sort by</span>\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {SORT_OPTIONS.map(({ value, label }) => {\n const isSelected = sort === value;\n return (\n <button\n key={value}\n onClick={() => onSortChange(value)}\n className={cn(PILL_BASE, isSelected ? PILL_ON : PILL_OFF)}\n >\n {isSelected && <Check className=\"h-3 w-3\" />}\n <span>{label}</span>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Trait sections — always expanded (pruning already removed the noise) */}\n {traitSections.length > 0 && (\n <>\n <div className=\"h-px w-full bg-border\" />\n {traitSections.map((section, i) => {\n const activeValues = selected[section.traitType] ?? [];\n return (\n <div key={section.traitType}>\n {i > 0 && <div className=\"h-px w-full bg-border mb-4\" />}\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm font-medium\">{section.traitType}</span>\n <span className=\"text-[11px] text-muted-foreground\">\n {section.values.length} values\n </span>\n </div>\n <div className=\"flex flex-wrap gap-1.5\">\n {section.values.map(({ value, count }) => {\n const isSelected = activeValues.includes(value);\n return (\n <button\n key={value}\n onClick={() => onToggleValue(section.traitType, value)}\n className={cn(PILL_BASE, isSelected ? PILL_ON : PILL_OFF)}\n >\n {isSelected && <Check className=\"h-3 w-3\" />}\n <span>{value}</span>\n <span className={isSelected ? \"opacity-80\" : \"opacity-60\"}>\n {count}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n );\n })}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}