@proveanything/smartlinks-utils-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/IconPicker-BMMQLR5I.d.ts +43 -0
- package/dist/chunk-HLFNSOPD.js +518 -0
- package/dist/chunk-HLFNSOPD.js.map +1 -0
- package/dist/chunk-IVUFK6SS.js +780 -0
- package/dist/chunk-IVUFK6SS.js.map +1 -0
- package/dist/chunk-L7FQ52F5.js +11 -0
- package/dist/chunk-L7FQ52F5.js.map +1 -0
- package/dist/chunk-V7JHAER7.js +920 -0
- package/dist/chunk-V7JHAER7.js.map +1 -0
- package/dist/components/AssetPicker/index.d.ts +2 -0
- package/dist/components/AssetPicker/index.js +4 -0
- package/dist/components/AssetPicker/index.js.map +1 -0
- package/dist/components/ConditionsEditor/index.d.ts +99 -0
- package/dist/components/ConditionsEditor/index.js +4 -0
- package/dist/components/ConditionsEditor/index.js.map +1 -0
- package/dist/components/IconPicker/index.d.ts +12 -0
- package/dist/components/IconPicker/index.js +4 -0
- package/dist/components/IconPicker/index.js.map +1 -0
- package/dist/index-B8GE7cLC.d.ts +150 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @proveanything/smartlinks-ui
|
|
2
|
+
|
|
3
|
+
Reusable React components for [SmartLinks](https://smartlinks.app) microapps.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @proveanything/smartlinks-ui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Peer Dependencies
|
|
12
|
+
|
|
13
|
+
This package requires the following peer dependencies in your app:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install react react-dom @proveanything/smartlinks
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Components
|
|
20
|
+
|
|
21
|
+
### Asset Picker
|
|
22
|
+
|
|
23
|
+
Browse, upload, and select media assets scoped to a collection, product, or proof.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { AssetPicker } from '@proveanything/smartlinks-ui/asset-picker';
|
|
27
|
+
|
|
28
|
+
<AssetPicker
|
|
29
|
+
scope={{ type: 'collection', collectionId: 'abc123' }}
|
|
30
|
+
mode="dialog"
|
|
31
|
+
allowUpload
|
|
32
|
+
onSelect={(asset) => console.log('Selected:', asset)}
|
|
33
|
+
trigger={<button>Choose Image</button>}
|
|
34
|
+
/>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Conditions Editor
|
|
38
|
+
|
|
39
|
+
Visual editor for building condition rules with AND/OR logic groups.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { ConditionsEditor } from '@proveanything/smartlinks-ui/conditions-editor';
|
|
43
|
+
|
|
44
|
+
<ConditionsEditor
|
|
45
|
+
value={conditionGroup}
|
|
46
|
+
onChange={setConditionGroup}
|
|
47
|
+
fields={[
|
|
48
|
+
{ key: 'status', label: 'Status', type: 'enum', options: [{ label: 'Active', value: 'active' }] },
|
|
49
|
+
{ key: 'age', label: 'Age', type: 'number' },
|
|
50
|
+
]}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Icon Picker
|
|
55
|
+
|
|
56
|
+
Searchable Font Awesome icon picker with lazy-loaded icon index for tree shaking.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { IconPicker } from '@proveanything/smartlinks-ui/icon-picker';
|
|
60
|
+
|
|
61
|
+
<IconPicker
|
|
62
|
+
mode="dialog"
|
|
63
|
+
value="fa-solid fa-heart"
|
|
64
|
+
onSelect={(icon) => console.log(icon.name)}
|
|
65
|
+
trigger={<button>Pick Icon</button>}
|
|
66
|
+
/>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Prerequisites
|
|
70
|
+
|
|
71
|
+
All components assume `@proveanything/smartlinks` is already initialized in your app via `SL.initializeApi()`. Components that interact with the SmartLinks API (Asset Picker) will use the global SDK import directly.
|
|
72
|
+
|
|
73
|
+
## Tree Shaking
|
|
74
|
+
|
|
75
|
+
Each component is a separate entry point. If you only import `AssetPicker`, the Icon Picker's Font Awesome index won't be bundled.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// ✅ Only loads AssetPicker code
|
|
79
|
+
import { AssetPicker } from '@proveanything/smartlinks-ui/asset-picker';
|
|
80
|
+
|
|
81
|
+
// ✅ Also works — barrel import, bundler tree-shakes unused
|
|
82
|
+
import { AssetPicker } from '@proveanything/smartlinks-ui';
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cd packages/smartlinks-ui
|
|
89
|
+
npm install
|
|
90
|
+
npm run build # Build with tsup
|
|
91
|
+
npm run dev # Watch mode
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React$1 from 'react';
|
|
2
|
+
|
|
3
|
+
/** FA7 icon families */
|
|
4
|
+
type IconFamily = 'classic' | 'duotone' | 'brands';
|
|
5
|
+
/** FA7 icon weight/style within a family */
|
|
6
|
+
type IconStyle = 'solid' | 'regular' | 'light';
|
|
7
|
+
/** What the picker returns when an icon is selected */
|
|
8
|
+
interface IconSelection {
|
|
9
|
+
/** Full CSS class string, e.g. 'fa-solid fa-heart' or 'fa-duotone fa-solid fa-heart' */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Icon family: classic, duotone, brands */
|
|
12
|
+
family: IconFamily;
|
|
13
|
+
/** Icon weight/style: solid, regular, light (not applicable for brands) */
|
|
14
|
+
style: IconStyle | null;
|
|
15
|
+
/** Human-readable label */
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
interface IconPickerProps {
|
|
19
|
+
/** Currently selected icon (full CSS class string) */
|
|
20
|
+
value?: string;
|
|
21
|
+
/** Called when an icon is selected */
|
|
22
|
+
onSelect?: (icon: IconSelection) => void;
|
|
23
|
+
/** Display as inline or dialog popup */
|
|
24
|
+
mode?: 'inline' | 'dialog';
|
|
25
|
+
/** Whether the dialog is open (dialog mode, controlled) */
|
|
26
|
+
open?: boolean;
|
|
27
|
+
/** Called when dialog is dismissed */
|
|
28
|
+
onClose?: () => void;
|
|
29
|
+
/** Trigger element for dialog mode */
|
|
30
|
+
trigger?: React.ReactNode;
|
|
31
|
+
/** Filter to specific families */
|
|
32
|
+
families?: IconFamily[];
|
|
33
|
+
/** Filter to specific styles */
|
|
34
|
+
styles?: IconStyle[];
|
|
35
|
+
/** Maximum icons to display per page (for performance) */
|
|
36
|
+
pageSize?: number;
|
|
37
|
+
/** Additional CSS class */
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare const IconPicker: React$1.FC<IconPickerProps>;
|
|
42
|
+
|
|
43
|
+
export { IconPicker as I, type IconPickerProps as a, type IconSelection as b, type IconFamily as c, type IconStyle as d };
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { cn } from './chunk-L7FQ52F5.js';
|
|
2
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
3
|
+
import { ChevronRight, Loader2, AlertCircle, Search, Smile, ChevronLeft, X } from 'lucide-react';
|
|
4
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/components/IconPicker/icon-index.ts
|
|
7
|
+
var FA_API = "https://api.fontawesome.com";
|
|
8
|
+
var FA_VERSION = "7.x";
|
|
9
|
+
var SEARCH_QUERY = `
|
|
10
|
+
query Search($version: String!, $query: String!, $first: Int) {
|
|
11
|
+
search(version: $version, query: $query, first: $first) {
|
|
12
|
+
id
|
|
13
|
+
label
|
|
14
|
+
unicode
|
|
15
|
+
familyStylesByLicense {
|
|
16
|
+
pro {
|
|
17
|
+
family
|
|
18
|
+
style
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
function mapRawIcon(raw) {
|
|
25
|
+
const proStyles = raw.familyStylesByLicense?.pro || [];
|
|
26
|
+
const families = /* @__PURE__ */ new Set();
|
|
27
|
+
const styles = /* @__PURE__ */ new Set();
|
|
28
|
+
let isBrand = false;
|
|
29
|
+
for (const fs of proStyles) {
|
|
30
|
+
if (fs.style === "brands" || fs.family === "brands") {
|
|
31
|
+
families.add("brands");
|
|
32
|
+
isBrand = true;
|
|
33
|
+
} else if (fs.family === "classic" || fs.family === "sharp") {
|
|
34
|
+
families.add("classic");
|
|
35
|
+
if (["solid", "regular", "light"].includes(fs.style)) styles.add(fs.style);
|
|
36
|
+
} else if (fs.family === "duotone") {
|
|
37
|
+
families.add("duotone");
|
|
38
|
+
if (["solid", "regular", "light"].includes(fs.style)) styles.add(fs.style);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
id: raw.id,
|
|
43
|
+
label: raw.label || raw.id,
|
|
44
|
+
unicode: raw.unicode || "",
|
|
45
|
+
families: [...families],
|
|
46
|
+
styles: [...styles],
|
|
47
|
+
isBrand,
|
|
48
|
+
terms: [raw.id, raw.label?.toLowerCase() || ""]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function gqlRequest(query, variables) {
|
|
52
|
+
const res = await fetch(FA_API, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ query, variables })
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) throw new Error(`FA API error: ${res.status}`);
|
|
58
|
+
const json = await res.json();
|
|
59
|
+
if (json.errors?.length) throw new Error(json.errors[0].message);
|
|
60
|
+
return json.data;
|
|
61
|
+
}
|
|
62
|
+
async function searchIcons(query, first = 200) {
|
|
63
|
+
const data = await gqlRequest(SEARCH_QUERY, {
|
|
64
|
+
version: FA_VERSION,
|
|
65
|
+
query,
|
|
66
|
+
first
|
|
67
|
+
});
|
|
68
|
+
return (data.search || []).map(mapRawIcon);
|
|
69
|
+
}
|
|
70
|
+
var _catalog = /* @__PURE__ */ new Map();
|
|
71
|
+
var _catalogListeners = /* @__PURE__ */ new Set();
|
|
72
|
+
var _catalogLoading = false;
|
|
73
|
+
var _catalogLoaded = false;
|
|
74
|
+
function getCatalog() {
|
|
75
|
+
return [..._catalog.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
76
|
+
}
|
|
77
|
+
function isCatalogLoaded() {
|
|
78
|
+
return _catalogLoaded;
|
|
79
|
+
}
|
|
80
|
+
function subscribeCatalog(fn) {
|
|
81
|
+
_catalogListeners.add(fn);
|
|
82
|
+
return () => {
|
|
83
|
+
_catalogListeners.delete(fn);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function notifyListeners() {
|
|
87
|
+
for (const fn of _catalogListeners) fn();
|
|
88
|
+
}
|
|
89
|
+
var BROWSE_QUERIES = [
|
|
90
|
+
"a",
|
|
91
|
+
"b",
|
|
92
|
+
"c",
|
|
93
|
+
"d",
|
|
94
|
+
"e",
|
|
95
|
+
"f",
|
|
96
|
+
"g",
|
|
97
|
+
"h",
|
|
98
|
+
"i",
|
|
99
|
+
"j",
|
|
100
|
+
"k",
|
|
101
|
+
"l",
|
|
102
|
+
"m",
|
|
103
|
+
"n",
|
|
104
|
+
"o",
|
|
105
|
+
"p",
|
|
106
|
+
"q",
|
|
107
|
+
"r",
|
|
108
|
+
"s",
|
|
109
|
+
"t",
|
|
110
|
+
"u",
|
|
111
|
+
"v",
|
|
112
|
+
"w",
|
|
113
|
+
"x",
|
|
114
|
+
"y",
|
|
115
|
+
"z",
|
|
116
|
+
"0",
|
|
117
|
+
"1",
|
|
118
|
+
"2",
|
|
119
|
+
"3",
|
|
120
|
+
"4",
|
|
121
|
+
"5"
|
|
122
|
+
];
|
|
123
|
+
async function loadCatalogInBackground() {
|
|
124
|
+
if (_catalogLoaded || _catalogLoading) return;
|
|
125
|
+
_catalogLoading = true;
|
|
126
|
+
notifyListeners();
|
|
127
|
+
for (let i = 0; i < BROWSE_QUERIES.length; i += 3) {
|
|
128
|
+
const batch = BROWSE_QUERIES.slice(i, i + 3);
|
|
129
|
+
const results = await Promise.all(
|
|
130
|
+
batch.map((q) => searchIcons(q, 200).catch(() => []))
|
|
131
|
+
);
|
|
132
|
+
for (const icons of results) {
|
|
133
|
+
for (const icon of icons) {
|
|
134
|
+
if (!_catalog.has(icon.id)) _catalog.set(icon.id, icon);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
notifyListeners();
|
|
138
|
+
}
|
|
139
|
+
_catalogLoaded = true;
|
|
140
|
+
_catalogLoading = false;
|
|
141
|
+
notifyListeners();
|
|
142
|
+
}
|
|
143
|
+
function toFaClass(id, family, style) {
|
|
144
|
+
if (family === "brands") return `fa-brands fa-${id}`;
|
|
145
|
+
const stylePrefix = style ? `fa-${style}` : "fa-solid";
|
|
146
|
+
if (family === "duotone") return `fa-duotone ${stylePrefix} fa-${id}`;
|
|
147
|
+
return `${stylePrefix} fa-${id}`;
|
|
148
|
+
}
|
|
149
|
+
function parseFaClass(cls) {
|
|
150
|
+
if (!cls) return null;
|
|
151
|
+
const parts = cls.trim().split(/\s+/);
|
|
152
|
+
if (parts.length < 2) return null;
|
|
153
|
+
const hasDuotone = parts.includes("fa-duotone");
|
|
154
|
+
const hasBrands = parts.includes("fa-brands");
|
|
155
|
+
const prefixes = /* @__PURE__ */ new Set(["fa-solid", "fa-regular", "fa-light", "fa-thin", "fa-brands", "fa-duotone"]);
|
|
156
|
+
const namePart = parts.find((p) => p.startsWith("fa-") && !prefixes.has(p));
|
|
157
|
+
if (!namePart) return null;
|
|
158
|
+
const id = namePart.replace(/^fa-/, "");
|
|
159
|
+
if (hasBrands) return { id, family: "brands", style: null };
|
|
160
|
+
let style = "solid";
|
|
161
|
+
if (parts.includes("fa-regular")) style = "regular";
|
|
162
|
+
else if (parts.includes("fa-light")) style = "light";
|
|
163
|
+
return { id, family: hasDuotone ? "duotone" : "classic", style };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/components/IconPicker/useIconSearch.ts
|
|
167
|
+
function useIconSearch(options = {}) {
|
|
168
|
+
const { families, styles, pageSize = 100 } = options;
|
|
169
|
+
const [catalog, setCatalog] = useState(() => getCatalog());
|
|
170
|
+
const [catalogReady, setCatalogReady] = useState(() => isCatalogLoaded());
|
|
171
|
+
const [searchResults, setSearchResults] = useState(null);
|
|
172
|
+
const [loading, setLoading] = useState(true);
|
|
173
|
+
const [searching, setSearching] = useState(false);
|
|
174
|
+
const [error, setError] = useState(null);
|
|
175
|
+
const [query, setQuery] = useState("");
|
|
176
|
+
const [activeFamily, setActiveFamily] = useState("classic");
|
|
177
|
+
const [activeStyle, setActiveStyle] = useState(null);
|
|
178
|
+
const [page, setPage] = useState(0);
|
|
179
|
+
const debounceRef = useRef();
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const unsub = subscribeCatalog(() => {
|
|
182
|
+
setCatalog(getCatalog());
|
|
183
|
+
setCatalogReady(isCatalogLoaded());
|
|
184
|
+
});
|
|
185
|
+
return unsub;
|
|
186
|
+
}, []);
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
loadCatalogInBackground().then(() => setLoading(false)).catch((err) => {
|
|
189
|
+
setError(err.message);
|
|
190
|
+
setLoading(false);
|
|
191
|
+
});
|
|
192
|
+
}, []);
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (catalog.length > 0) setLoading(false);
|
|
195
|
+
}, [catalog.length]);
|
|
196
|
+
const setSearch = useCallback((q) => {
|
|
197
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
198
|
+
debounceRef.current = setTimeout(async () => {
|
|
199
|
+
setPage(0);
|
|
200
|
+
const trimmed = q.trim();
|
|
201
|
+
setQuery(trimmed);
|
|
202
|
+
if (!trimmed) {
|
|
203
|
+
setSearchResults(null);
|
|
204
|
+
setSearching(false);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
setSearching(true);
|
|
208
|
+
try {
|
|
209
|
+
const results = await searchIcons(trimmed, 300);
|
|
210
|
+
setSearchResults(results);
|
|
211
|
+
} catch {
|
|
212
|
+
setSearchResults(null);
|
|
213
|
+
} finally {
|
|
214
|
+
setSearching(false);
|
|
215
|
+
}
|
|
216
|
+
}, 250);
|
|
217
|
+
}, []);
|
|
218
|
+
const source = searchResults ?? catalog;
|
|
219
|
+
const availableFamilies = useMemo(() => {
|
|
220
|
+
const set = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const icon of source) {
|
|
222
|
+
for (const f of icon.families) {
|
|
223
|
+
if (!families || families.includes(f)) set.add(f);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const order = ["classic", "duotone", "brands"];
|
|
227
|
+
return order.filter((f) => set.has(f));
|
|
228
|
+
}, [source, families]);
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (availableFamilies.length > 0 && !availableFamilies.includes(activeFamily)) {
|
|
231
|
+
setActiveFamily(availableFamilies[0]);
|
|
232
|
+
}
|
|
233
|
+
}, [availableFamilies, activeFamily]);
|
|
234
|
+
const availableStyles = useMemo(() => {
|
|
235
|
+
if (activeFamily === "brands") return [];
|
|
236
|
+
const set = /* @__PURE__ */ new Set();
|
|
237
|
+
for (const icon of source) {
|
|
238
|
+
if (!icon.families.includes(activeFamily)) continue;
|
|
239
|
+
for (const s of icon.styles) {
|
|
240
|
+
if (!styles || styles.includes(s)) set.add(s);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const order = ["solid", "regular", "light"];
|
|
244
|
+
return order.filter((s) => set.has(s));
|
|
245
|
+
}, [source, activeFamily, styles]);
|
|
246
|
+
const handleSetFamily = useCallback((f) => {
|
|
247
|
+
setActiveFamily(f);
|
|
248
|
+
setActiveStyle(null);
|
|
249
|
+
setPage(0);
|
|
250
|
+
}, []);
|
|
251
|
+
const filtered = useMemo(() => {
|
|
252
|
+
let result = source;
|
|
253
|
+
result = result.filter((icon) => icon.families.includes(activeFamily));
|
|
254
|
+
if (activeStyle && activeFamily !== "brands") {
|
|
255
|
+
result = result.filter((icon) => icon.styles.includes(activeStyle));
|
|
256
|
+
}
|
|
257
|
+
if (query && !searchResults) {
|
|
258
|
+
const lower = query.toLowerCase();
|
|
259
|
+
const terms = lower.split(/\s+/);
|
|
260
|
+
result = result.filter(
|
|
261
|
+
(icon) => terms.every((term) => icon.terms.some((t) => t.includes(term)))
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}, [source, searchResults, activeFamily, activeStyle, query]);
|
|
266
|
+
const totalPages = Math.ceil(filtered.length / pageSize);
|
|
267
|
+
const pageIcons = useMemo(
|
|
268
|
+
() => filtered.slice(page * pageSize, (page + 1) * pageSize),
|
|
269
|
+
[filtered, page, pageSize]
|
|
270
|
+
);
|
|
271
|
+
return {
|
|
272
|
+
loading: loading && catalog.length === 0,
|
|
273
|
+
searching,
|
|
274
|
+
error,
|
|
275
|
+
query,
|
|
276
|
+
setSearch,
|
|
277
|
+
activeFamily,
|
|
278
|
+
setActiveFamily: handleSetFamily,
|
|
279
|
+
availableFamilies,
|
|
280
|
+
activeStyle,
|
|
281
|
+
setActiveStyle: useCallback((s) => {
|
|
282
|
+
setActiveStyle(s);
|
|
283
|
+
setPage(0);
|
|
284
|
+
}, []),
|
|
285
|
+
availableStyles,
|
|
286
|
+
filtered,
|
|
287
|
+
pageIcons,
|
|
288
|
+
page,
|
|
289
|
+
setPage,
|
|
290
|
+
totalPages,
|
|
291
|
+
totalCount: filtered.length,
|
|
292
|
+
catalogReady
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
var FAMILY_LABELS = {
|
|
296
|
+
classic: "Classic",
|
|
297
|
+
duotone: "Duotone",
|
|
298
|
+
brands: "Brands"
|
|
299
|
+
};
|
|
300
|
+
var IconPickerContent = ({
|
|
301
|
+
value,
|
|
302
|
+
onSelect,
|
|
303
|
+
onConfirm,
|
|
304
|
+
families: allowedFamilies,
|
|
305
|
+
styles: allowedStyles,
|
|
306
|
+
pageSize = 100,
|
|
307
|
+
className
|
|
308
|
+
}) => {
|
|
309
|
+
const {
|
|
310
|
+
loading,
|
|
311
|
+
searching,
|
|
312
|
+
error,
|
|
313
|
+
setSearch,
|
|
314
|
+
activeFamily,
|
|
315
|
+
setActiveFamily,
|
|
316
|
+
availableFamilies,
|
|
317
|
+
activeStyle,
|
|
318
|
+
setActiveStyle,
|
|
319
|
+
availableStyles,
|
|
320
|
+
pageIcons,
|
|
321
|
+
page,
|
|
322
|
+
setPage,
|
|
323
|
+
totalPages,
|
|
324
|
+
totalCount
|
|
325
|
+
} = useIconSearch({ families: allowedFamilies, styles: allowedStyles, pageSize });
|
|
326
|
+
const inputRef = useRef(null);
|
|
327
|
+
const parsed = value ? parseFaClass(value) : null;
|
|
328
|
+
const [hoveredId, setHoveredId] = useState(null);
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
inputRef.current?.focus();
|
|
331
|
+
}, [loading]);
|
|
332
|
+
const handleSelect = useCallback((id) => {
|
|
333
|
+
const family = activeFamily;
|
|
334
|
+
const style = family === "brands" ? null : activeStyle || "solid";
|
|
335
|
+
const icon = {
|
|
336
|
+
name: toFaClass(id, family, style),
|
|
337
|
+
family,
|
|
338
|
+
style,
|
|
339
|
+
label: id
|
|
340
|
+
};
|
|
341
|
+
onSelect?.(icon);
|
|
342
|
+
onConfirm?.(icon);
|
|
343
|
+
}, [onSelect, onConfirm, activeFamily, activeStyle]);
|
|
344
|
+
if (loading) {
|
|
345
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col items-center justify-center py-12 gap-3", className), children: [
|
|
346
|
+
/* @__PURE__ */ jsx(Loader2, { className: "w-6 h-6 animate-spin text-muted-foreground" }),
|
|
347
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Loading icons\u2026" })
|
|
348
|
+
] });
|
|
349
|
+
}
|
|
350
|
+
if (error) {
|
|
351
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col items-center justify-center py-12 gap-3", className), children: [
|
|
352
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "w-6 h-6 text-destructive" }),
|
|
353
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error })
|
|
354
|
+
] });
|
|
355
|
+
}
|
|
356
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), children: [
|
|
357
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
358
|
+
/* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" }),
|
|
359
|
+
searching && /* @__PURE__ */ jsx(Loader2, { className: "absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 animate-spin text-muted-foreground" }),
|
|
360
|
+
/* @__PURE__ */ jsx(
|
|
361
|
+
"input",
|
|
362
|
+
{
|
|
363
|
+
ref: inputRef,
|
|
364
|
+
type: "text",
|
|
365
|
+
placeholder: "Search icons\u2026",
|
|
366
|
+
onChange: (e) => setSearch(e.target.value),
|
|
367
|
+
className: "w-full pl-9 pr-9 py-2 text-sm rounded-md border border-border bg-transparent focus:outline-none focus:ring-1 focus:ring-ring"
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
] }),
|
|
371
|
+
availableFamilies.length > 1 && /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b border-border pb-2", children: availableFamilies.map((f) => /* @__PURE__ */ jsx(
|
|
372
|
+
"button",
|
|
373
|
+
{
|
|
374
|
+
onClick: () => setActiveFamily(f),
|
|
375
|
+
className: cn(
|
|
376
|
+
"px-3 py-1.5 text-xs font-semibold rounded-md transition-colors",
|
|
377
|
+
f === activeFamily ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
378
|
+
),
|
|
379
|
+
children: FAMILY_LABELS[f]
|
|
380
|
+
},
|
|
381
|
+
f
|
|
382
|
+
)) }),
|
|
383
|
+
availableStyles.length > 1 && /* @__PURE__ */ jsxs("div", { className: "flex gap-1 flex-wrap items-center", children: [
|
|
384
|
+
/* @__PURE__ */ jsx(
|
|
385
|
+
"button",
|
|
386
|
+
{
|
|
387
|
+
onClick: () => setActiveStyle(null),
|
|
388
|
+
className: cn(
|
|
389
|
+
"px-2.5 py-1 text-xs font-medium rounded-full transition-colors",
|
|
390
|
+
!activeStyle ? "bg-foreground text-background" : "bg-muted text-muted-foreground hover:bg-accent"
|
|
391
|
+
),
|
|
392
|
+
children: "All"
|
|
393
|
+
}
|
|
394
|
+
),
|
|
395
|
+
availableStyles.map((s) => /* @__PURE__ */ jsx(
|
|
396
|
+
"button",
|
|
397
|
+
{
|
|
398
|
+
onClick: () => setActiveStyle(s === activeStyle ? null : s),
|
|
399
|
+
className: cn(
|
|
400
|
+
"px-2.5 py-1 text-xs font-medium rounded-full capitalize transition-colors",
|
|
401
|
+
s === activeStyle ? "bg-foreground text-background" : "bg-muted text-muted-foreground hover:bg-accent"
|
|
402
|
+
),
|
|
403
|
+
children: s
|
|
404
|
+
},
|
|
405
|
+
s
|
|
406
|
+
)),
|
|
407
|
+
/* @__PURE__ */ jsxs("span", { className: "ml-auto text-[11px] text-muted-foreground", children: [
|
|
408
|
+
totalCount,
|
|
409
|
+
" icons"
|
|
410
|
+
] })
|
|
411
|
+
] }),
|
|
412
|
+
pageIcons.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-10 gap-2", children: [
|
|
413
|
+
/* @__PURE__ */ jsx(Smile, { className: "w-6 h-6 text-muted-foreground/40" }),
|
|
414
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "No icons found" })
|
|
415
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-8 sm:grid-cols-10 md:grid-cols-12 gap-1", children: pageIcons.map((icon) => {
|
|
416
|
+
const isSelected = parsed?.id === icon.id && parsed?.family === activeFamily;
|
|
417
|
+
const isHovered = hoveredId === icon.id;
|
|
418
|
+
const displayStyle = activeFamily === "brands" ? null : activeStyle || "solid";
|
|
419
|
+
const cssClass = toFaClass(icon.id, activeFamily, displayStyle);
|
|
420
|
+
return /* @__PURE__ */ jsx(
|
|
421
|
+
"button",
|
|
422
|
+
{
|
|
423
|
+
onClick: () => handleSelect(icon.id),
|
|
424
|
+
onMouseEnter: () => setHoveredId(icon.id),
|
|
425
|
+
onMouseLeave: () => setHoveredId(null),
|
|
426
|
+
title: icon.label,
|
|
427
|
+
className: cn(
|
|
428
|
+
"aspect-square flex items-center justify-center rounded-md text-base transition-all",
|
|
429
|
+
isSelected ? "bg-primary/10 text-primary ring-2 ring-primary" : isHovered ? "bg-accent text-accent-foreground scale-110" : "text-muted-foreground hover:bg-accent/50"
|
|
430
|
+
),
|
|
431
|
+
children: /* @__PURE__ */ jsx("i", { className: cssClass })
|
|
432
|
+
},
|
|
433
|
+
icon.id
|
|
434
|
+
);
|
|
435
|
+
}) }),
|
|
436
|
+
/* @__PURE__ */ jsx("div", { className: "h-6 text-center", children: hoveredId && /* @__PURE__ */ jsxs("code", { className: "text-[11px] text-muted-foreground bg-muted px-2 py-0.5 rounded", children: [
|
|
437
|
+
"fa-",
|
|
438
|
+
hoveredId
|
|
439
|
+
] }) }),
|
|
440
|
+
totalPages > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2", children: [
|
|
441
|
+
/* @__PURE__ */ jsx(
|
|
442
|
+
"button",
|
|
443
|
+
{
|
|
444
|
+
onClick: () => setPage(Math.max(0, page - 1)),
|
|
445
|
+
disabled: page === 0,
|
|
446
|
+
className: "p-1 rounded hover:bg-accent disabled:opacity-30 transition-colors",
|
|
447
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "w-4 h-4" })
|
|
448
|
+
}
|
|
449
|
+
),
|
|
450
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
451
|
+
page + 1,
|
|
452
|
+
" / ",
|
|
453
|
+
totalPages
|
|
454
|
+
] }),
|
|
455
|
+
/* @__PURE__ */ jsx(
|
|
456
|
+
"button",
|
|
457
|
+
{
|
|
458
|
+
onClick: () => setPage(Math.min(totalPages - 1, page + 1)),
|
|
459
|
+
disabled: page >= totalPages - 1,
|
|
460
|
+
className: "p-1 rounded hover:bg-accent disabled:opacity-30 transition-colors",
|
|
461
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "w-4 h-4" })
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
] })
|
|
465
|
+
] });
|
|
466
|
+
};
|
|
467
|
+
var PickerDialog = ({ open, onClose, children }) => {
|
|
468
|
+
if (!open) return null;
|
|
469
|
+
return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
470
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-sm", onClick: onClose }),
|
|
471
|
+
/* @__PURE__ */ jsxs("div", { className: "relative z-10 w-full max-w-xl max-h-[80vh] bg-background rounded-xl shadow-2xl border border-border flex flex-col overflow-hidden mx-4", children: [
|
|
472
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-border", children: [
|
|
473
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Select Icon" }),
|
|
474
|
+
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "p-1 rounded hover:bg-accent transition-colors", children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4 text-muted-foreground" }) })
|
|
475
|
+
] }),
|
|
476
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-4", children })
|
|
477
|
+
] })
|
|
478
|
+
] });
|
|
479
|
+
};
|
|
480
|
+
var IconPicker = (props) => {
|
|
481
|
+
const { mode = "inline", trigger, open: controlledOpen, onClose, value, onSelect, className, ...rest } = props;
|
|
482
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
483
|
+
const isOpen = controlledOpen ?? internalOpen;
|
|
484
|
+
const handleClose = useCallback(() => {
|
|
485
|
+
setInternalOpen(false);
|
|
486
|
+
onClose?.();
|
|
487
|
+
}, [onClose]);
|
|
488
|
+
const handleSelectAndClose = useCallback((icon) => {
|
|
489
|
+
onSelect?.(icon);
|
|
490
|
+
handleClose();
|
|
491
|
+
}, [onSelect, handleClose]);
|
|
492
|
+
if (mode === "inline") {
|
|
493
|
+
return /* @__PURE__ */ jsx(IconPickerContent, { value, onSelect, className, ...rest });
|
|
494
|
+
}
|
|
495
|
+
const parsed = value ? parseFaClass(value) : null;
|
|
496
|
+
const triggerElement = trigger || /* @__PURE__ */ jsxs("div", { className: cn(
|
|
497
|
+
"inline-flex items-center gap-2 px-3 py-2 rounded-md border border-border cursor-pointer",
|
|
498
|
+
"hover:border-ring transition-colors",
|
|
499
|
+
className
|
|
500
|
+
), children: [
|
|
501
|
+
parsed ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
502
|
+
/* @__PURE__ */ jsx("i", { className: value }),
|
|
503
|
+
/* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground", children: [
|
|
504
|
+
"fa-",
|
|
505
|
+
parsed.id
|
|
506
|
+
] })
|
|
507
|
+
] }) : /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: "Choose icon\u2026" }),
|
|
508
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "w-3.5 h-3.5 text-muted-foreground ml-auto" })
|
|
509
|
+
] });
|
|
510
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
511
|
+
/* @__PURE__ */ jsx("span", { onClick: () => setInternalOpen(true), className: "cursor-pointer", children: triggerElement }),
|
|
512
|
+
/* @__PURE__ */ jsx(PickerDialog, { open: isOpen, onClose: handleClose, children: /* @__PURE__ */ jsx(IconPickerContent, { value, onSelect: handleSelectAndClose, ...rest }) })
|
|
513
|
+
] });
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
export { IconPicker, parseFaClass, toFaClass };
|
|
517
|
+
//# sourceMappingURL=chunk-HLFNSOPD.js.map
|
|
518
|
+
//# sourceMappingURL=chunk-HLFNSOPD.js.map
|