@mvriu5/payload-icon-picker 1.0.1 → 1.0.2
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 +43 -1
- package/dist/adapters/phosphor.d.ts +2 -1
- package/dist/adapters/utils.d.ts +3 -3
- package/dist/components/IconDialog.js +51 -2
- package/dist/components/IconField.d.ts +4 -0
- package/dist/components/IconRenderer.d.ts +4 -0
- package/dist/index.d.ts +22 -2
- package/dist/index.js +72 -4
- package/dist/utils.d.ts +2 -6
- package/dist/utils.js +2 -2
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -80,6 +80,14 @@ lucide:ArrowRight
|
|
|
80
80
|
|
|
81
81
|
You can still use `resolveIcon` if you need a custom final storage format.
|
|
82
82
|
|
|
83
|
+
### About `resolveIcon`
|
|
84
|
+
|
|
85
|
+
When using `payloadIconPlugin()`, `resolveIcon` runs during Payload config setup. The resolved string is passed to the admin field as each icon's final `value`.
|
|
86
|
+
|
|
87
|
+
That means normal `iconField()` usage does not require passing `resolveIcon` to the admin component manually.
|
|
88
|
+
|
|
89
|
+
The `resolveIcon` prop on `IconRenderer`, `IconField`, and `createIconResolver()` is primarily for direct component usage, tests, or advanced cases where you bypass `payloadIconPlugin()`. If you use a custom `resolveIcon` for stored values, pass the same resolver when rendering or resolving those values outside the Payload admin UI.
|
|
90
|
+
|
|
83
91
|
## Rendering Stored Icons
|
|
84
92
|
|
|
85
93
|
Use `IconRenderer` from the client export to render a stored icon string in your frontend.
|
|
@@ -174,6 +182,8 @@ payloadIconPlugin({
|
|
|
174
182
|
|
|
175
183
|
Adapters support `prefix`, `include`, `exclude`, and optional label/value formatters:
|
|
176
184
|
|
|
185
|
+
When registering icons from more than one library, use a unique `prefix` for every adapter. Many icon libraries export generic names such as `Home`, `Search`, or `User`; prefixes keep stored values unique and make it clear which library an icon came from.
|
|
186
|
+
|
|
177
187
|
```ts
|
|
178
188
|
import * as SimpleIcons from "@icons-pack/react-simple-icons"
|
|
179
189
|
import * as TablerIcons from "@tabler/icons-react"
|
|
@@ -195,6 +205,8 @@ payloadIconPlugin({
|
|
|
195
205
|
|
|
196
206
|
With `prefix`, the adapter keeps `name` as the original library export name and sets `value` to `prefix:name`, for example `tabler:IconHome`.
|
|
197
207
|
|
|
208
|
+
Without prefixes, duplicate icon values can collide. In development, the plugin warns when multiple registered icons resolve to the same stored value.
|
|
209
|
+
|
|
198
210
|
Or pass explicit icon metadata:
|
|
199
211
|
|
|
200
212
|
```ts
|
|
@@ -213,7 +225,7 @@ payloadIconPlugin({
|
|
|
213
225
|
|
|
214
226
|
## Field Options
|
|
215
227
|
|
|
216
|
-
`iconField()` accepts normal single-value Payload text field options, plus picker labels:
|
|
228
|
+
`iconField()` accepts normal single-value Payload text field options, plus picker labels and per-field icon filters:
|
|
217
229
|
|
|
218
230
|
```ts
|
|
219
231
|
iconField({
|
|
@@ -228,6 +240,36 @@ iconField({
|
|
|
228
240
|
})
|
|
229
241
|
```
|
|
230
242
|
|
|
243
|
+
Use `libraries` to limit a field to icons from selected adapter prefixes:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
iconField({
|
|
247
|
+
name: "navigationIcon",
|
|
248
|
+
label: "Navigation Icon",
|
|
249
|
+
libraries: ["lucide"],
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Use `icons` to allow only specific stored icon values:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
iconField({
|
|
257
|
+
name: "socialIcon",
|
|
258
|
+
label: "Social Icon",
|
|
259
|
+
icons: ["si:SiGithub", "si:SiDiscord"],
|
|
260
|
+
})
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
You can combine both options. The filters are additive, so this example allows all Lucide icons plus the explicit GitHub icon:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
iconField({
|
|
267
|
+
name: "featuredIcon",
|
|
268
|
+
libraries: ["lucide"],
|
|
269
|
+
icons: ["si:SiGithub"],
|
|
270
|
+
})
|
|
271
|
+
```
|
|
272
|
+
|
|
231
273
|
## Development
|
|
232
274
|
|
|
233
275
|
The dev Payload config in `dev/payload.config.ts` registers the plugin with `lucide-react`, which is installed as a development dependency.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { IconFieldIcon } from "../utils.js";
|
|
2
2
|
import type { IconAdapterOptions, IconLibrary } from "./utils.js";
|
|
3
|
-
|
|
3
|
+
type PhosphorIconWeight = "bold" | "duotone" | "fill" | "light" | "regular" | "thin";
|
|
4
4
|
export type PhosphorIconAdapterOptions = Omit<IconAdapterOptions, "weight"> & {
|
|
5
5
|
weight?: PhosphorIconWeight;
|
|
6
6
|
};
|
|
7
7
|
export declare const phosphorIconAdapter: (icons: IconLibrary, options?: PhosphorIconAdapterOptions) => IconFieldIcon[];
|
|
8
|
+
export {};
|
package/dist/adapters/utils.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { IconFieldIcon } from "../utils.js";
|
|
2
|
-
export type IconNode = Array<[string, Record<string, unknown>]>;
|
|
3
2
|
export type IconAdapterOptions = {
|
|
4
3
|
exclude?: string[];
|
|
5
4
|
include?: string[];
|
|
@@ -11,14 +10,15 @@ export type IconAdapterOptions = {
|
|
|
11
10
|
weight?: string;
|
|
12
11
|
};
|
|
13
12
|
export type IconLibrary = Record<string, unknown>;
|
|
14
|
-
|
|
13
|
+
type IconAdapterLabelArgs = {
|
|
15
14
|
defaultLabel: string;
|
|
16
15
|
name: string;
|
|
17
16
|
prefix?: string;
|
|
18
17
|
value: string;
|
|
19
18
|
};
|
|
20
|
-
|
|
19
|
+
type IconAdapterValueArgs = {
|
|
21
20
|
name: string;
|
|
22
21
|
prefix?: string;
|
|
23
22
|
};
|
|
24
23
|
export declare const createSvgIconAdapter: (icons: IconLibrary, options?: IconAdapterOptions) => IconFieldIcon[];
|
|
24
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import React, { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { VirtuosoGrid } from "react-virtuoso";
|
|
4
4
|
import "./IconDialog.css";
|
|
5
5
|
import { sanitizeSvg } from "../sanitizeSvg.js";
|
|
@@ -8,6 +8,11 @@ import { getIconLabelParts, getIconLibrary } from "../utils.js";
|
|
|
8
8
|
const SEARCH_DEBOUNCE_MS = 150;
|
|
9
9
|
const ALL_LIBRARY_FILTER = "__all";
|
|
10
10
|
export const IconDialog = ({ icons, isDisabled, noResultsLabel, onClear, onClose, onSelect, placeholder, resolveIcon, value })=>{
|
|
11
|
+
const dialogTitleId = useId();
|
|
12
|
+
const dialogDescriptionId = useId();
|
|
13
|
+
const dialogRef = useRef(null);
|
|
14
|
+
const searchInputRef = useRef(null);
|
|
15
|
+
const previouslyFocusedElementRef = useRef(null);
|
|
11
16
|
const [query, setQuery] = useState("");
|
|
12
17
|
const [activeLibrary, setActiveLibrary] = useState(ALL_LIBRARY_FILTER);
|
|
13
18
|
const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
|
|
@@ -37,11 +42,42 @@ export const IconDialog = ({ icons, isDisabled, noResultsLabel, onClear, onClose
|
|
|
37
42
|
icons,
|
|
38
43
|
resolveIcon
|
|
39
44
|
]);
|
|
45
|
+
useEffect(()=>{
|
|
46
|
+
previouslyFocusedElementRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
47
|
+
searchInputRef.current?.focus();
|
|
48
|
+
return ()=>{
|
|
49
|
+
previouslyFocusedElementRef.current?.focus();
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
40
52
|
useEffect(()=>{
|
|
41
53
|
const handleKeyDown = (event)=>{
|
|
42
54
|
if (event.key === "Escape") {
|
|
43
55
|
event.preventDefault();
|
|
44
56
|
onClose();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (event.key !== "Tab") {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const dialog = dialogRef.current;
|
|
63
|
+
if (!dialog) return;
|
|
64
|
+
const focusableElements = getFocusableElements(dialog);
|
|
65
|
+
if (focusableElements.length === 0) {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
dialog.focus();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const firstFocusableElement = focusableElements[0];
|
|
71
|
+
const lastFocusableElement = focusableElements[focusableElements.length - 1];
|
|
72
|
+
if (!firstFocusableElement || !lastFocusableElement) return;
|
|
73
|
+
if (event.shiftKey && document.activeElement === firstFocusableElement) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
lastFocusableElement.focus();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!event.shiftKey && document.activeElement === lastFocusableElement) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
firstFocusableElement.focus();
|
|
45
81
|
}
|
|
46
82
|
};
|
|
47
83
|
window.addEventListener("keydown", handleKeyDown);
|
|
@@ -50,10 +86,14 @@ export const IconDialog = ({ icons, isDisabled, noResultsLabel, onClear, onClose
|
|
|
50
86
|
onClose
|
|
51
87
|
]);
|
|
52
88
|
return /*#__PURE__*/ _jsx("div", {
|
|
89
|
+
"aria-describedby": dialogDescriptionId,
|
|
90
|
+
"aria-labelledby": dialogTitleId,
|
|
53
91
|
"aria-modal": "true",
|
|
54
92
|
className: "payload-icon-picker__overlay",
|
|
55
93
|
onClick: onClose,
|
|
94
|
+
ref: dialogRef,
|
|
56
95
|
role: "dialog",
|
|
96
|
+
tabIndex: -1,
|
|
57
97
|
children: /*#__PURE__*/ _jsxs("div", {
|
|
58
98
|
className: "payload-icon-picker__dialog",
|
|
59
99
|
onClick: (event)=>event.stopPropagation(),
|
|
@@ -63,21 +103,28 @@ export const IconDialog = ({ icons, isDisabled, noResultsLabel, onClear, onClose
|
|
|
63
103
|
children: [
|
|
64
104
|
/*#__PURE__*/ _jsx("h3", {
|
|
65
105
|
className: "payload-icon-picker__dialog-title",
|
|
106
|
+
id: dialogTitleId,
|
|
66
107
|
children: "Select icon"
|
|
67
108
|
}),
|
|
68
109
|
/*#__PURE__*/ _jsx("button", {
|
|
110
|
+
"aria-label": "Close icon picker",
|
|
69
111
|
onClick: onClose,
|
|
70
112
|
type: "button",
|
|
71
113
|
children: "Close"
|
|
72
114
|
})
|
|
73
115
|
]
|
|
74
116
|
}),
|
|
117
|
+
/*#__PURE__*/ _jsx("p", {
|
|
118
|
+
id: dialogDescriptionId,
|
|
119
|
+
hidden: true,
|
|
120
|
+
children: "Search, filter, and select an icon. Press Escape to close the dialog."
|
|
121
|
+
}),
|
|
75
122
|
/*#__PURE__*/ _jsx("input", {
|
|
76
|
-
autoFocus: true,
|
|
77
123
|
"aria-label": "Search icons",
|
|
78
124
|
className: "payload-icon-picker__search",
|
|
79
125
|
onChange: (event)=>setQuery(event.target.value),
|
|
80
126
|
placeholder: placeholder,
|
|
127
|
+
ref: searchInputRef,
|
|
81
128
|
type: "search",
|
|
82
129
|
value: query
|
|
83
130
|
}),
|
|
@@ -108,6 +155,7 @@ export const IconDialog = ({ icons, isDisabled, noResultsLabel, onClear, onClose
|
|
|
108
155
|
const iconValue = resolveIcon(icon);
|
|
109
156
|
const isSelected = iconValue === value;
|
|
110
157
|
return /*#__PURE__*/ _jsxs("button", {
|
|
158
|
+
"aria-label": `Select ${icon.label ?? icon.name}`,
|
|
111
159
|
"aria-selected": isSelected,
|
|
112
160
|
className: "payload-icon-picker__option",
|
|
113
161
|
onClick: ()=>{
|
|
@@ -164,6 +212,7 @@ const LibraryFilterButton = ({ isActive, label, onClick })=>/*#__PURE__*/ _jsx("
|
|
|
164
212
|
type: "button",
|
|
165
213
|
children: label
|
|
166
214
|
});
|
|
215
|
+
const getFocusableElements = (element)=>Array.from(element.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((focusableElement)=>!focusableElement.hasAttribute("hidden") && focusableElement.getAttribute("aria-hidden") !== "true");
|
|
167
216
|
const useDebouncedValue = (value, delay)=>{
|
|
168
217
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
169
218
|
useEffect(()=>{
|
|
@@ -7,6 +7,10 @@ export type IconFieldProps = TextFieldClientProps & {
|
|
|
7
7
|
icons?: IconFieldIcon[] | IconFieldIconRecord;
|
|
8
8
|
noResultsLabel?: string;
|
|
9
9
|
placeholder?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Primarily for direct component usage. With `payloadIconPlugin()`, icon
|
|
12
|
+
* values are already resolved before they reach this component.
|
|
13
|
+
*/
|
|
10
14
|
resolveIcon?: (icon: IconFieldIcon) => string;
|
|
11
15
|
};
|
|
12
16
|
export declare const IconField: React.FC<IconFieldProps>;
|
|
@@ -4,6 +4,10 @@ import type { IconFieldIcon, IconFieldIconRecord } from "../utils.js";
|
|
|
4
4
|
export type IconRendererProps = Omit<HTMLAttributes<HTMLSpanElement>, "children" | "dangerouslySetInnerHTML"> & {
|
|
5
5
|
fallback?: ReactNode;
|
|
6
6
|
icons: IconFieldIcon[] | IconFieldIconRecord;
|
|
7
|
+
/**
|
|
8
|
+
* Maps registered icons to the stored string. Pass this when your stored
|
|
9
|
+
* values were produced with a custom `payloadIconPlugin({ resolveIcon })`.
|
|
10
|
+
*/
|
|
7
11
|
resolveIcon?: (icon: IconFieldIcon) => string;
|
|
8
12
|
size?: number | string;
|
|
9
13
|
value?: null | string;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,22 +12,42 @@ export type PayloadIconPluginConfig = {
|
|
|
12
12
|
*/
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
/**
|
|
15
|
-
* Maps
|
|
15
|
+
* Maps each registered icon to the final string stored in Payload.
|
|
16
|
+
*
|
|
17
|
+
* In normal `payloadIconPlugin()` + `iconField()` usage this runs during
|
|
18
|
+
* Payload config setup. The resolved string is then passed to the admin
|
|
19
|
+
* field as `icon.value`, so you do not need to also pass `resolveIcon` to
|
|
20
|
+
* the admin component manually.
|
|
16
21
|
*/
|
|
17
22
|
resolveIcon?: (icon: IconFieldIcon) => string;
|
|
18
23
|
};
|
|
19
24
|
export type IconFieldConfig = Omit<TextField, "admin" | "hasMany" | "maxRows" | "minRows" | "type"> & {
|
|
20
25
|
admin?: TextField["admin"];
|
|
26
|
+
/**
|
|
27
|
+
* Limit this field to icons whose final stored value uses one of these
|
|
28
|
+
* prefixes. Example: `["lucide"]` matches `lucide:Home`.
|
|
29
|
+
*/
|
|
30
|
+
libraries?: string[];
|
|
21
31
|
noResultsLabel?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Limit this field to specific final stored icon values.
|
|
34
|
+
* Example: `["lucide:Home", "si:SiGithub"]`.
|
|
35
|
+
*/
|
|
36
|
+
icons?: string[];
|
|
22
37
|
placeholder?: string;
|
|
23
38
|
};
|
|
24
39
|
export type ResolveIconFromStringConfig = {
|
|
25
40
|
icons: IconFieldIcon[] | IconFieldIconRecord;
|
|
41
|
+
/**
|
|
42
|
+
* Maps registered icons to the stored string. Use the same resolver that
|
|
43
|
+
* produced existing stored values when resolving icons outside the Payload
|
|
44
|
+
* admin UI.
|
|
45
|
+
*/
|
|
26
46
|
resolveIcon?: (icon: IconFieldIcon) => string;
|
|
27
47
|
};
|
|
28
48
|
export type ResolvedIcon = IconFieldIcon & {
|
|
29
49
|
resolvedValue: string;
|
|
30
50
|
};
|
|
31
|
-
export declare const iconField: ({ admin, noResultsLabel, placeholder, ...field }: IconFieldConfig) => TextField;
|
|
51
|
+
export declare const iconField: ({ admin, icons, libraries, noResultsLabel, placeholder, ...field }: IconFieldConfig) => TextField;
|
|
32
52
|
export declare const payloadIconPlugin: (pluginOptions: PayloadIconPluginConfig) => (config: Config) => Config;
|
|
33
53
|
export declare const createIconResolver: ({ icons, resolveIcon }: ResolveIconFromStringConfig) => (value: null | string | undefined) => ResolvedIcon | undefined;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { sanitizeSvg } from "./sanitizeSvg.js";
|
|
|
2
2
|
import { normalizeIcons } from "./utils.js";
|
|
3
3
|
const ICON_FIELD_MARKER = "payloadIconPicker";
|
|
4
4
|
const ICON_FIELD_COMPONENT = "@mvriu5/payload-icon-picker/client#IconField";
|
|
5
|
-
|
|
5
|
+
const PACKAGE_NAME = "@mvriu5/payload-icon-picker";
|
|
6
|
+
export const iconField = ({ admin, icons, libraries, noResultsLabel, placeholder, ...field })=>({
|
|
6
7
|
...field,
|
|
7
8
|
hasMany: false,
|
|
8
9
|
type: "text",
|
|
@@ -11,6 +12,8 @@ export const iconField = ({ admin, noResultsLabel, placeholder, ...field })=>({
|
|
|
11
12
|
custom: {
|
|
12
13
|
...admin?.custom ?? {},
|
|
13
14
|
[ICON_FIELD_MARKER]: {
|
|
15
|
+
icons,
|
|
16
|
+
libraries,
|
|
14
17
|
noResultsLabel,
|
|
15
18
|
placeholder
|
|
16
19
|
}
|
|
@@ -34,7 +37,9 @@ export const payloadIconPlugin = (pluginOptions)=>(config)=>{
|
|
|
34
37
|
};
|
|
35
38
|
export const createIconResolver = ({ icons, resolveIcon })=>{
|
|
36
39
|
const iconMap = new Map();
|
|
37
|
-
normalizeIcons(icons)
|
|
40
|
+
const normalizedIcons = normalizeIcons(icons);
|
|
41
|
+
warnAboutDuplicateIconValues(normalizedIcons, (icon)=>resolveIcon ? resolveIcon(icon) : icon.value ?? icon.name);
|
|
42
|
+
normalizedIcons.forEach((icon)=>{
|
|
38
43
|
const resolvedValue = resolveIcon ? resolveIcon(icon) : icon.value ?? icon.name;
|
|
39
44
|
iconMap.set(resolvedValue, {
|
|
40
45
|
...icon,
|
|
@@ -53,6 +58,8 @@ const withIconFields = (fields, icons)=>(fields ?? []).map((field)=>{
|
|
|
53
58
|
const fieldWithNestedFields = field;
|
|
54
59
|
if ("admin" in field && field.admin?.custom?.[ICON_FIELD_MARKER]) {
|
|
55
60
|
const marker = field.admin.custom[ICON_FIELD_MARKER];
|
|
61
|
+
const fieldName = "name" in field && typeof field.name === "string" ? field.name : undefined;
|
|
62
|
+
const filteredIcons = filterIconsForField(icons, marker, fieldName);
|
|
56
63
|
const textField = field;
|
|
57
64
|
const admin = textField.admin;
|
|
58
65
|
return {
|
|
@@ -63,7 +70,7 @@ const withIconFields = (fields, icons)=>(fields ?? []).map((field)=>{
|
|
|
63
70
|
...admin?.components ?? {},
|
|
64
71
|
Field: {
|
|
65
72
|
clientProps: {
|
|
66
|
-
icons,
|
|
73
|
+
icons: filteredIcons,
|
|
67
74
|
noResultsLabel: marker.noResultsLabel,
|
|
68
75
|
placeholder: marker.placeholder
|
|
69
76
|
},
|
|
@@ -101,7 +108,9 @@ const withIconFields = (fields, icons)=>(fields ?? []).map((field)=>{
|
|
|
101
108
|
return field;
|
|
102
109
|
});
|
|
103
110
|
const normalizeIconsForClient = (icons, resolveIcon)=>{
|
|
104
|
-
|
|
111
|
+
const normalizedIcons = normalizeIcons(icons);
|
|
112
|
+
warnAboutDuplicateIconValues(normalizedIcons, (icon)=>resolveIcon ? resolveIcon(icon) : icon.value ?? icon.name);
|
|
113
|
+
return normalizedIcons.map(({ Icon, component, ...icon })=>{
|
|
105
114
|
const sanitizedSvg = sanitizeSvg(icon.svg);
|
|
106
115
|
return {
|
|
107
116
|
...icon,
|
|
@@ -114,3 +123,62 @@ const normalizeIconsForClient = (icons, resolveIcon)=>{
|
|
|
114
123
|
};
|
|
115
124
|
});
|
|
116
125
|
};
|
|
126
|
+
const filterIconsForField = (icons, marker, fieldName)=>{
|
|
127
|
+
const allowedLibraries = new Set(marker.libraries ?? []);
|
|
128
|
+
const allowedIconValues = new Set(marker.icons ?? []);
|
|
129
|
+
if (allowedLibraries.size === 0 && allowedIconValues.size === 0) {
|
|
130
|
+
return icons;
|
|
131
|
+
}
|
|
132
|
+
const filteredIcons = icons.filter((icon)=>{
|
|
133
|
+
const library = getIconValueLibrary(icon.value);
|
|
134
|
+
return allowedIconValues.has(icon.value) || (library ? allowedLibraries.has(library) : false);
|
|
135
|
+
});
|
|
136
|
+
warnAboutUnmatchedIconFieldFilters({
|
|
137
|
+
allowedIconValues,
|
|
138
|
+
allowedLibraries,
|
|
139
|
+
fieldName,
|
|
140
|
+
filteredIcons,
|
|
141
|
+
icons
|
|
142
|
+
});
|
|
143
|
+
return filteredIcons;
|
|
144
|
+
};
|
|
145
|
+
const getIconValueLibrary = (value)=>{
|
|
146
|
+
const separatorIndex = value.indexOf(":");
|
|
147
|
+
if (separatorIndex <= 0) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return value.slice(0, separatorIndex);
|
|
151
|
+
};
|
|
152
|
+
const warnAboutUnmatchedIconFieldFilters = ({ allowedIconValues, allowedLibraries, fieldName, filteredIcons, icons })=>{
|
|
153
|
+
if (isProduction()) return;
|
|
154
|
+
const knownIconValues = new Set(icons.map((icon)=>icon.value));
|
|
155
|
+
const knownLibraries = new Set(icons.map((icon)=>getIconValueLibrary(icon.value)).filter((library)=>Boolean(library)));
|
|
156
|
+
const unknownIconValues = Array.from(allowedIconValues).filter((value)=>!knownIconValues.has(value));
|
|
157
|
+
const unknownLibraries = Array.from(allowedLibraries).filter((library)=>!knownLibraries.has(library));
|
|
158
|
+
const fieldLabel = fieldName ? `iconField("${fieldName}")` : "iconField()";
|
|
159
|
+
if (unknownIconValues.length > 0) {
|
|
160
|
+
console.warn(`[${PACKAGE_NAME}] ${fieldLabel} references unknown icon values: ${unknownIconValues.map((value)=>`"${value}"`).join(", ")}. ` + "Make sure the matching icons are registered in payloadIconPlugin().");
|
|
161
|
+
}
|
|
162
|
+
if (unknownLibraries.length > 0) {
|
|
163
|
+
console.warn(`[${PACKAGE_NAME}] ${fieldLabel} references unknown icon libraries: ${unknownLibraries.map((library)=>`"${library}"`).join(", ")}. ` + "Make sure the matching adapters use these prefixes in payloadIconPlugin().");
|
|
164
|
+
}
|
|
165
|
+
if (filteredIcons.length === 0) {
|
|
166
|
+
console.warn(`[${PACKAGE_NAME}] ${fieldLabel} filter matched no icons. The picker will render with an empty icon list.`);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const warnAboutDuplicateIconValues = (icons, resolveValue)=>{
|
|
170
|
+
if (isProduction()) return;
|
|
171
|
+
const iconsByValue = new Map();
|
|
172
|
+
icons.forEach((icon)=>{
|
|
173
|
+
const value = resolveValue(icon);
|
|
174
|
+
const names = iconsByValue.get(value) ?? [];
|
|
175
|
+
names.push(icon.name);
|
|
176
|
+
iconsByValue.set(value, names);
|
|
177
|
+
});
|
|
178
|
+
const duplicateMessages = Array.from(iconsByValue.entries()).filter(([, names])=>names.length > 1).map(([value, names])=>`"${value}" used by ${names.map((name)=>`"${name}"`).join(", ")}`);
|
|
179
|
+
if (duplicateMessages.length === 0) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
console.warn(`[${PACKAGE_NAME}] Duplicate icon values detected: ${duplicateMessages.join("; ")}. ` + "Icon values must be unique so stored values resolve predictably. Use adapter prefixes to avoid collisions.");
|
|
183
|
+
};
|
|
184
|
+
const isProduction = ()=>typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ElementType } from "react";
|
|
2
|
-
|
|
2
|
+
type IconComponent = ElementType<{
|
|
3
3
|
"aria-hidden"?: boolean;
|
|
4
4
|
className?: string;
|
|
5
5
|
focusable?: boolean;
|
|
@@ -17,13 +17,9 @@ export type IconFieldIcon = {
|
|
|
17
17
|
export type IconFieldIconRecord = Record<string, IconComponent> | Record<string, IconFieldIcon>;
|
|
18
18
|
export declare const defaultResolveIcon: (icon: IconFieldIcon) => string;
|
|
19
19
|
export declare const normalizeIcons: (icons: IconFieldIcon[] | IconFieldIconRecord | undefined) => IconFieldIcon[];
|
|
20
|
-
export declare const isIconComponent: (icon: IconComponent | IconFieldIcon) => icon is IconComponent;
|
|
21
20
|
export declare const getIconLabelParts: (icon: IconFieldIcon, resolveIcon: (icon: IconFieldIcon) => string) => {
|
|
22
21
|
name: string;
|
|
23
22
|
prefix?: string;
|
|
24
23
|
};
|
|
25
24
|
export declare const getIconLibrary: (icon: IconFieldIcon, resolveIcon: (icon: IconFieldIcon) => string) => string | undefined;
|
|
26
|
-
export
|
|
27
|
-
name: string;
|
|
28
|
-
prefix: string;
|
|
29
|
-
} | undefined;
|
|
25
|
+
export {};
|
package/dist/utils.js
CHANGED
|
@@ -19,7 +19,7 @@ export const normalizeIcons = (icons)=>{
|
|
|
19
19
|
};
|
|
20
20
|
});
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
const isIconComponent = (icon)=>{
|
|
23
23
|
if (typeof icon === "function" || typeof icon === "string") {
|
|
24
24
|
return true;
|
|
25
25
|
}
|
|
@@ -41,7 +41,7 @@ export const getIconLabelParts = (icon, resolveIcon)=>{
|
|
|
41
41
|
};
|
|
42
42
|
};
|
|
43
43
|
export const getIconLibrary = (icon, resolveIcon)=>splitPrefixedValue(resolveIcon(icon))?.prefix;
|
|
44
|
-
|
|
44
|
+
const splitPrefixedValue = (value)=>{
|
|
45
45
|
const separatorIndex = value.indexOf(":");
|
|
46
46
|
if (separatorIndex <= 0 || separatorIndex === value.length - 1) return undefined;
|
|
47
47
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mvriu5/payload-icon-picker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Payload plugin introducing an icon picker into the CMS.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -88,7 +88,6 @@
|
|
|
88
88
|
"@types/react-dom": "19.2.3",
|
|
89
89
|
"copyfiles": "2.4.1",
|
|
90
90
|
"cross-env": "^7.0.3",
|
|
91
|
-
"eslint": "^9.23.0",
|
|
92
91
|
"graphql": "^17.0.2",
|
|
93
92
|
"knip": "^6.24.0",
|
|
94
93
|
"lucide-react": "^1.23.0",
|
|
@@ -162,7 +161,6 @@
|
|
|
162
161
|
"entry": [
|
|
163
162
|
"src/exports/client.ts",
|
|
164
163
|
"dev/payload.config.ts",
|
|
165
|
-
"dev/next.config.mjs",
|
|
166
164
|
"dev/app/**/*.{ts,tsx}"
|
|
167
165
|
],
|
|
168
166
|
"ignore": [
|