@mvriu5/payload-icon-picker 0.1.1

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 ADDED
@@ -0,0 +1,168 @@
1
+ # Payload Icon Picker
2
+
3
+ Payload plugin for adding an icon picker field to the admin UI. The field stores a string, while editors can search and choose from the icons you register in the plugin config.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @mvriu5/payload-icon-picker
9
+ ```
10
+
11
+ Install the icon library you want to use in your project, for example:
12
+
13
+ ```bash
14
+ pnpm add lucide-react
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Register all icons once in your Payload config, then use `iconField()` wherever a Payload text field should become an icon picker.
20
+
21
+ ```ts
22
+ import { postgresAdapter } from "@payloadcms/db-postgres"
23
+ import { lexicalEditor } from "@payloadcms/richtext-lexical"
24
+ import * as Icons from "lucide-react"
25
+ import { buildConfig } from "payload"
26
+ import { iconField, payloadIconPlugin } from "@mvriu5/payload-icon-picker"
27
+
28
+ export default buildConfig({
29
+ collections: [
30
+ {
31
+ slug: "posts",
32
+ fields: [
33
+ {
34
+ name: "title",
35
+ type: "text",
36
+ required: true,
37
+ },
38
+ iconField({
39
+ name: "icon",
40
+ label: "Icon",
41
+ }),
42
+ ],
43
+ },
44
+ ],
45
+ plugins: [
46
+ payloadIconPlugin({
47
+ icons: Icons,
48
+ resolveIcon: ({ name }) => name,
49
+ }),
50
+ ],
51
+ db: postgresAdapter({
52
+ pool: {
53
+ connectionString: process.env.DATABASE_URL,
54
+ },
55
+ }),
56
+ editor: lexicalEditor(),
57
+ secret: process.env.PAYLOAD_SECRET,
58
+ })
59
+ ```
60
+
61
+ ## Stored Value
62
+
63
+ `iconField()` creates a normal Payload `text` field. The selected icon is resolved to a string and stored in the database.
64
+
65
+ By default, the stored value is `icon.value ?? icon.name`. Use `resolveIcon` to choose a different format:
66
+
67
+ ```ts
68
+ payloadIconPlugin({
69
+ icons: Icons,
70
+ resolveIcon: ({ name }) => `lucide:${name}`,
71
+ })
72
+ ```
73
+
74
+ Selecting `ArrowRight` would store:
75
+
76
+ ```txt
77
+ lucide:ArrowRight
78
+ ```
79
+
80
+ ## Resolving Stored Icons
81
+
82
+ Use `createIconResolver()` to turn a stored string back into the registered icon.
83
+
84
+ ```tsx
85
+ import * as Icons from "lucide-react"
86
+ import { createIconResolver } from "@mvriu5/payload-icon-picker"
87
+
88
+ const resolveStoredIcon = createIconResolver({
89
+ icons: Icons,
90
+ resolveIcon: ({ name }) => name,
91
+ })
92
+
93
+ export function PostIcon({ icon }: { icon?: string }) {
94
+ const resolvedIcon = resolveStoredIcon(icon)
95
+ const Icon = resolvedIcon?.Icon
96
+
97
+ if (!Icon) {
98
+ return null
99
+ }
100
+
101
+ return <Icon aria-hidden size={24} />
102
+ }
103
+ ```
104
+
105
+ ## Icon Inputs
106
+
107
+ You can pass a full icon library namespace:
108
+
109
+ ```ts
110
+ import * as Icons from "lucide-react"
111
+
112
+ payloadIconPlugin({
113
+ icons: Icons,
114
+ })
115
+ ```
116
+
117
+ Or pass explicit icon metadata:
118
+
119
+ ```ts
120
+ payloadIconPlugin({
121
+ icons: [
122
+ {
123
+ Icon: Icons.Home,
124
+ keywords: ["house", "start"],
125
+ label: "Home",
126
+ name: "Home",
127
+ value: "home",
128
+ },
129
+ ],
130
+ })
131
+ ```
132
+
133
+ ## Field Options
134
+
135
+ `iconField()` accepts normal single-value Payload text field options, plus picker labels:
136
+
137
+ ```ts
138
+ iconField({
139
+ name: "icon",
140
+ label: "Icon",
141
+ required: true,
142
+ placeholder: "Search icons",
143
+ noResultsLabel: "No icons found",
144
+ admin: {
145
+ position: "sidebar",
146
+ },
147
+ })
148
+ ```
149
+
150
+ ## Development
151
+
152
+ The dev Payload config in `dev/payload.config.ts` registers the plugin with a small local icon registry so the picker can be tested without installing another icon package.
153
+
154
+ ```bash
155
+ pnpm dev
156
+ ```
157
+
158
+ Generate the Payload import map after changing admin components:
159
+
160
+ ```bash
161
+ pnpm generate:importmap
162
+ ```
163
+
164
+ Build the package:
165
+
166
+ ```bash
167
+ pnpm build
168
+ ```
@@ -0,0 +1,27 @@
1
+ import type { TextFieldClientProps } from "payload";
2
+ import type { ElementType } from "react";
3
+ import React from "react";
4
+ type IconComponent = ElementType<{
5
+ "aria-hidden"?: boolean;
6
+ className?: string;
7
+ focusable?: boolean;
8
+ size?: number | string;
9
+ }>;
10
+ export type IconFieldIcon = {
11
+ Icon?: IconComponent;
12
+ component?: IconComponent;
13
+ keywords?: string[];
14
+ label?: string;
15
+ name: string;
16
+ svg?: string;
17
+ value?: string;
18
+ };
19
+ export type IconFieldIconRecord = Record<string, IconComponent> | Record<string, IconFieldIcon>;
20
+ export type IconFieldProps = TextFieldClientProps & {
21
+ icons?: IconFieldIcon[] | IconFieldIconRecord;
22
+ noResultsLabel?: string;
23
+ placeholder?: string;
24
+ resolveIcon?: (icon: IconFieldIcon) => string;
25
+ };
26
+ export declare const IconField: React.FC<IconFieldProps>;
27
+ export {};
@@ -0,0 +1,364 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { FieldLabel, useField } from "@payloadcms/ui";
4
+ import { icons as lucideIcons } from "lucide-react";
5
+ import React, { useMemo, useState } from "react";
6
+ const defaultResolveIcon = (icon)=>icon.value ?? icon.name;
7
+ const ICON_BATCH_SIZE = 120;
8
+ const normalizeIcons = (icons)=>{
9
+ if (!icons) {
10
+ return [];
11
+ }
12
+ if (Array.isArray(icons)) {
13
+ return icons;
14
+ }
15
+ return Object.entries(icons).map(([name, icon])=>{
16
+ if (isIconComponent(icon)) {
17
+ return {
18
+ Icon: icon,
19
+ name
20
+ };
21
+ }
22
+ return {
23
+ name,
24
+ ...icon
25
+ };
26
+ });
27
+ };
28
+ const isIconComponent = (icon)=>{
29
+ if (typeof icon === "function" || typeof icon === "string") {
30
+ return true;
31
+ }
32
+ return !("Icon" in icon || "component" in icon || "label" in icon || "value" in icon);
33
+ };
34
+ const getIconSearchText = (icon)=>[
35
+ icon.name,
36
+ icon.label,
37
+ icon.value,
38
+ ...icon.keywords ?? []
39
+ ].filter(Boolean).join(" ").toLowerCase();
40
+ export const IconField = ({ field, icons: iconsFromProps, noResultsLabel = "No icons found", path, placeholder = "Search icons", resolveIcon = defaultResolveIcon, validate })=>{
41
+ const { custom = {}, description } = field.admin ?? {};
42
+ const { label, required } = field;
43
+ const iconsFromField = custom.icons;
44
+ const resolvedIcons = useMemo(()=>normalizeIcons(iconsFromProps ?? iconsFromField), [
45
+ iconsFromField,
46
+ iconsFromProps
47
+ ]);
48
+ const { errorMessage, setValue, showError, value } = useField({
49
+ path,
50
+ validate: validate
51
+ });
52
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
53
+ const [query, setQuery] = useState("");
54
+ const [visibleCount, setVisibleCount] = useState(ICON_BATCH_SIZE);
55
+ const selectedIcon = useMemo(()=>resolvedIcons.find((icon)=>resolveIcon(icon) === value), [
56
+ resolvedIcons,
57
+ resolveIcon,
58
+ value
59
+ ]);
60
+ const filteredIcons = useMemo(()=>{
61
+ const normalizedQuery = query.trim().toLowerCase();
62
+ if (!normalizedQuery) {
63
+ return resolvedIcons;
64
+ }
65
+ return resolvedIcons.filter((icon)=>getIconSearchText(icon).includes(normalizedQuery));
66
+ }, [
67
+ query,
68
+ resolvedIcons
69
+ ]);
70
+ const visibleIcons = useMemo(()=>filteredIcons.slice(0, visibleCount), [
71
+ filteredIcons,
72
+ visibleCount
73
+ ]);
74
+ const hasMoreIcons = visibleCount < filteredIcons.length;
75
+ const openDialog = ()=>{
76
+ setVisibleCount(ICON_BATCH_SIZE);
77
+ setIsDialogOpen(true);
78
+ };
79
+ const closeDialog = ()=>{
80
+ setIsDialogOpen(false);
81
+ setQuery("");
82
+ setVisibleCount(ICON_BATCH_SIZE);
83
+ };
84
+ return /*#__PURE__*/ _jsxs("div", {
85
+ className: "field-type text payload-icon-picker",
86
+ children: [
87
+ /*#__PURE__*/ _jsx(FieldLabel, {
88
+ label: label,
89
+ path: path,
90
+ required: required
91
+ }),
92
+ description ? /*#__PURE__*/ _jsx("div", {
93
+ className: "field-description",
94
+ children: description
95
+ }) : null,
96
+ /*#__PURE__*/ _jsxs("div", {
97
+ style: {
98
+ display: "grid",
99
+ gap: 8
100
+ },
101
+ children: [
102
+ /*#__PURE__*/ _jsxs("button", {
103
+ "aria-haspopup": "dialog",
104
+ "aria-expanded": isDialogOpen,
105
+ onClick: openDialog,
106
+ style: {
107
+ alignItems: "center",
108
+ background: "var(--theme-input-bg)",
109
+ border: "1px solid var(--theme-elevation-150)",
110
+ borderRadius: 4,
111
+ color: "inherit",
112
+ cursor: "pointer",
113
+ display: "flex",
114
+ font: "inherit",
115
+ gap: 10,
116
+ minHeight: 40,
117
+ padding: "8px 10px",
118
+ textAlign: "left",
119
+ width: "100%"
120
+ },
121
+ type: "button",
122
+ children: [
123
+ selectedIcon ? /*#__PURE__*/ _jsx(IconPreview, {
124
+ icon: selectedIcon
125
+ }) : null,
126
+ /*#__PURE__*/ _jsx("span", {
127
+ style: {
128
+ color: selectedIcon ? "inherit" : "var(--theme-elevation-500)",
129
+ flex: 1,
130
+ overflow: "hidden",
131
+ textOverflow: "ellipsis",
132
+ whiteSpace: "nowrap"
133
+ },
134
+ children: selectedIcon ? selectedIcon.label ?? selectedIcon.name : placeholder
135
+ }),
136
+ value ? /*#__PURE__*/ _jsx("span", {
137
+ onClick: (event)=>{
138
+ event.stopPropagation();
139
+ setValue("");
140
+ },
141
+ onKeyDown: (event)=>{
142
+ if (event.key === "Enter" || event.key === " ") {
143
+ event.preventDefault();
144
+ event.stopPropagation();
145
+ setValue("");
146
+ }
147
+ },
148
+ role: "button",
149
+ tabIndex: 0,
150
+ style: {
151
+ borderLeft: "1px solid var(--theme-elevation-150)",
152
+ color: "var(--theme-elevation-600)",
153
+ cursor: "pointer",
154
+ paddingLeft: 10
155
+ },
156
+ children: "Clear"
157
+ }) : null
158
+ ]
159
+ }),
160
+ isDialogOpen ? /*#__PURE__*/ _jsx("div", {
161
+ "aria-modal": "true",
162
+ onClick: closeDialog,
163
+ role: "dialog",
164
+ style: {
165
+ alignItems: "center",
166
+ background: "rgb(0 0 0 / 45%)",
167
+ display: "flex",
168
+ inset: 0,
169
+ justifyContent: "center",
170
+ padding: 24,
171
+ position: "fixed",
172
+ zIndex: 1000
173
+ },
174
+ children: /*#__PURE__*/ _jsxs("div", {
175
+ onClick: (event)=>event.stopPropagation(),
176
+ style: {
177
+ background: "var(--theme-bg)",
178
+ border: "1px solid var(--theme-elevation-150)",
179
+ borderRadius: 4,
180
+ boxShadow: "0 20px 60px rgb(0 0 0 / 30%)",
181
+ display: "grid",
182
+ gap: 16,
183
+ maxHeight: "min(720px, calc(100vh - 48px))",
184
+ maxWidth: 760,
185
+ overflow: "hidden",
186
+ padding: 20,
187
+ width: "100%"
188
+ },
189
+ children: [
190
+ /*#__PURE__*/ _jsxs("div", {
191
+ style: {
192
+ alignItems: "center",
193
+ display: "flex",
194
+ gap: 12
195
+ },
196
+ children: [
197
+ /*#__PURE__*/ _jsx("h3", {
198
+ style: {
199
+ flex: 1,
200
+ fontSize: 18,
201
+ margin: 0
202
+ },
203
+ children: "Select icon"
204
+ }),
205
+ /*#__PURE__*/ _jsx("button", {
206
+ onClick: closeDialog,
207
+ type: "button",
208
+ children: "Close"
209
+ })
210
+ ]
211
+ }),
212
+ /*#__PURE__*/ _jsx("input", {
213
+ autoFocus: true,
214
+ "aria-label": "Search icons",
215
+ onChange: (event)=>{
216
+ setQuery(event.target.value);
217
+ setVisibleCount(ICON_BATCH_SIZE);
218
+ },
219
+ placeholder: placeholder,
220
+ style: {
221
+ border: "1px solid var(--theme-elevation-150)",
222
+ borderRadius: 4,
223
+ font: "inherit",
224
+ padding: "8px 10px",
225
+ width: "100%"
226
+ },
227
+ type: "search",
228
+ value: query
229
+ }),
230
+ /*#__PURE__*/ _jsxs("div", {
231
+ "aria-label": "Icon options",
232
+ role: "listbox",
233
+ onScroll: (event)=>{
234
+ const element = event.currentTarget;
235
+ const distanceToBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
236
+ if (distanceToBottom < 240 && hasMoreIcons) {
237
+ setVisibleCount((count)=>Math.min(count + ICON_BATCH_SIZE, filteredIcons.length));
238
+ }
239
+ },
240
+ style: {
241
+ display: "grid",
242
+ gap: 8,
243
+ gridTemplateColumns: "repeat(auto-fill, minmax(112px, 1fr))",
244
+ maxHeight: "min(440px, calc(100vh - 260px))",
245
+ minHeight: 220,
246
+ overflowY: "auto",
247
+ paddingRight: 2
248
+ },
249
+ children: [
250
+ filteredIcons.length > 0 ? visibleIcons.map((icon)=>{
251
+ const iconValue = resolveIcon(icon);
252
+ const isSelected = iconValue === value;
253
+ return /*#__PURE__*/ _jsxs("button", {
254
+ "aria-selected": isSelected,
255
+ onClick: ()=>{
256
+ setValue(iconValue);
257
+ closeDialog();
258
+ },
259
+ role: "option",
260
+ style: {
261
+ alignItems: "center",
262
+ background: isSelected ? "var(--theme-elevation-100)" : "transparent",
263
+ border: `1px solid ${isSelected ? "var(--theme-success-500)" : "var(--theme-elevation-150)"}`,
264
+ borderRadius: 4,
265
+ cursor: "pointer",
266
+ display: "grid",
267
+ gap: 6,
268
+ justifyItems: "center",
269
+ minHeight: 82,
270
+ padding: 8
271
+ },
272
+ title: icon.label ?? icon.name,
273
+ type: "button",
274
+ children: [
275
+ /*#__PURE__*/ _jsx(IconPreview, {
276
+ icon: icon
277
+ }),
278
+ /*#__PURE__*/ _jsx("span", {
279
+ style: {
280
+ fontSize: 12,
281
+ maxWidth: "100%",
282
+ overflow: "hidden",
283
+ textOverflow: "ellipsis",
284
+ whiteSpace: "nowrap"
285
+ },
286
+ children: icon.label ?? icon.name
287
+ })
288
+ ]
289
+ }, iconValue);
290
+ }) : /*#__PURE__*/ _jsx("div", {
291
+ children: noResultsLabel
292
+ }),
293
+ hasMoreIcons ? /*#__PURE__*/ _jsx("button", {
294
+ onClick: ()=>setVisibleCount((count)=>Math.min(count + ICON_BATCH_SIZE, filteredIcons.length)),
295
+ style: {
296
+ gridColumn: "1 / -1",
297
+ minHeight: 40
298
+ },
299
+ type: "button",
300
+ children: "Load more"
301
+ }) : null
302
+ ]
303
+ }),
304
+ value ? /*#__PURE__*/ _jsx("button", {
305
+ onClick: ()=>{
306
+ setValue("");
307
+ closeDialog();
308
+ },
309
+ style: {
310
+ justifySelf: "start"
311
+ },
312
+ type: "button",
313
+ children: "Clear selection"
314
+ }) : null
315
+ ]
316
+ })
317
+ }) : null
318
+ ]
319
+ }),
320
+ showError && errorMessage ? /*#__PURE__*/ _jsx("div", {
321
+ className: "field-error",
322
+ children: errorMessage
323
+ }) : null
324
+ ]
325
+ });
326
+ };
327
+ const IconPreview = ({ icon })=>{
328
+ const PreviewIcon = icon.Icon ?? icon.component;
329
+ const LucideIcon = lucideIcons[icon.name];
330
+ if (PreviewIcon) {
331
+ return /*#__PURE__*/ _jsx(PreviewIcon, {
332
+ "aria-hidden": true,
333
+ focusable: false,
334
+ size: 24
335
+ });
336
+ }
337
+ if (LucideIcon) {
338
+ return /*#__PURE__*/ _jsx(LucideIcon, {
339
+ "aria-hidden": true,
340
+ focusable: false,
341
+ size: 24
342
+ });
343
+ }
344
+ if (icon.svg) {
345
+ return /*#__PURE__*/ _jsx("span", {
346
+ "aria-hidden": true,
347
+ dangerouslySetInnerHTML: {
348
+ __html: icon.svg
349
+ },
350
+ style: {
351
+ display: "inline-flex",
352
+ lineHeight: 1
353
+ }
354
+ });
355
+ }
356
+ return /*#__PURE__*/ _jsx("span", {
357
+ "aria-hidden": true,
358
+ style: {
359
+ fontSize: 20,
360
+ lineHeight: 1
361
+ },
362
+ children: icon.label?.charAt(0) ?? icon.name.charAt(0)
363
+ });
364
+ };
@@ -0,0 +1,2 @@
1
+ export { IconField } from "../IconField.js";
2
+ export type { IconFieldIcon, IconFieldIconRecord, IconFieldProps } from "../IconField.js";
@@ -0,0 +1 @@
1
+ export { IconField } from "../IconField.js";
@@ -0,0 +1,33 @@
1
+ import type { Config, TextField } from "payload";
2
+ import type { IconFieldIcon, IconFieldIconRecord } from "./IconField.js";
3
+ export type PayloadIconPluginConfig = {
4
+ /**
5
+ * Icons from the selected icon library. You can pass an array or a module namespace,
6
+ * e.g. `import * as Icons from 'lucide-react'`.
7
+ */
8
+ icons: IconFieldIcon[] | IconFieldIconRecord;
9
+ /**
10
+ * Disable only the admin picker behavior. The field remains a text field so stored
11
+ * data and database schema stay stable.
12
+ */
13
+ disabled?: boolean;
14
+ /**
15
+ * Maps a selected icon to the string that is stored in Payload.
16
+ */
17
+ resolveIcon?: (icon: IconFieldIcon) => string;
18
+ };
19
+ export type IconFieldConfig = Omit<TextField, "admin" | "hasMany" | "maxRows" | "minRows" | "type"> & {
20
+ admin?: TextField["admin"];
21
+ noResultsLabel?: string;
22
+ placeholder?: string;
23
+ };
24
+ export type ResolveIconFromStringConfig = {
25
+ icons: IconFieldIcon[] | IconFieldIconRecord;
26
+ resolveIcon?: (icon: IconFieldIcon) => string;
27
+ };
28
+ export type ResolvedIcon = IconFieldIcon & {
29
+ resolvedValue: string;
30
+ };
31
+ export declare const iconField: ({ admin, noResultsLabel, placeholder, ...field }: IconFieldConfig) => TextField;
32
+ export declare const payloadIconPlugin: (pluginOptions: PayloadIconPluginConfig) => (config: Config) => Config;
33
+ export declare const createIconResolver: ({ icons, resolveIcon }: ResolveIconFromStringConfig) => (value: null | string | undefined) => ResolvedIcon | undefined;
package/dist/index.js ADDED
@@ -0,0 +1,132 @@
1
+ const ICON_FIELD_MARKER = "payloadIconPicker";
2
+ const ICON_FIELD_COMPONENT = "@mvriu5/payload-icon-picker/client#IconField";
3
+ export const iconField = ({ admin, noResultsLabel, placeholder, ...field })=>({
4
+ ...field,
5
+ hasMany: false,
6
+ type: "text",
7
+ admin: {
8
+ ...admin,
9
+ custom: {
10
+ ...admin?.custom ?? {},
11
+ [ICON_FIELD_MARKER]: {
12
+ noResultsLabel,
13
+ placeholder
14
+ }
15
+ }
16
+ }
17
+ });
18
+ export const payloadIconPlugin = (pluginOptions)=>(config)=>{
19
+ if (pluginOptions.disabled) {
20
+ return config;
21
+ }
22
+ const icons = normalizeIconsForClient(pluginOptions.icons, pluginOptions.resolveIcon);
23
+ config.collections = config.collections?.map((collection)=>({
24
+ ...collection,
25
+ fields: withIconFields(collection.fields, icons)
26
+ }));
27
+ config.globals = config.globals?.map((global)=>({
28
+ ...global,
29
+ fields: withIconFields(global.fields, icons)
30
+ }));
31
+ return config;
32
+ };
33
+ export const createIconResolver = ({ icons, resolveIcon })=>{
34
+ const iconMap = new Map();
35
+ normalizeIcons(icons).forEach((icon)=>{
36
+ const resolvedValue = resolveIcon ? resolveIcon(icon) : icon.value ?? icon.name;
37
+ iconMap.set(resolvedValue, {
38
+ ...icon,
39
+ resolvedValue
40
+ });
41
+ });
42
+ return (value)=>{
43
+ if (!value) {
44
+ return undefined;
45
+ }
46
+ return iconMap.get(value);
47
+ };
48
+ };
49
+ const withIconFields = (fields, icons)=>(fields ?? []).map((field)=>{
50
+ const fieldWithNestedFields = field;
51
+ if ("admin" in field && field.admin?.custom?.[ICON_FIELD_MARKER]) {
52
+ const marker = field.admin.custom[ICON_FIELD_MARKER];
53
+ const textField = field;
54
+ const admin = textField.admin;
55
+ return {
56
+ ...textField,
57
+ admin: {
58
+ ...admin,
59
+ components: {
60
+ ...admin?.components ?? {},
61
+ Field: {
62
+ clientProps: {
63
+ icons,
64
+ noResultsLabel: marker.noResultsLabel,
65
+ placeholder: marker.placeholder
66
+ },
67
+ path: ICON_FIELD_COMPONENT
68
+ }
69
+ },
70
+ custom: admin?.custom
71
+ }
72
+ };
73
+ }
74
+ if (fieldWithNestedFields.fields) {
75
+ return {
76
+ ...fieldWithNestedFields,
77
+ fields: withIconFields(fieldWithNestedFields.fields, icons)
78
+ };
79
+ }
80
+ if (fieldWithNestedFields.tabs) {
81
+ return {
82
+ ...fieldWithNestedFields,
83
+ tabs: fieldWithNestedFields.tabs.map((tab)=>({
84
+ ...tab,
85
+ fields: withIconFields(tab.fields, icons)
86
+ }))
87
+ };
88
+ }
89
+ if (fieldWithNestedFields.blocks) {
90
+ return {
91
+ ...fieldWithNestedFields,
92
+ blocks: fieldWithNestedFields.blocks.map((block)=>({
93
+ ...block,
94
+ fields: withIconFields(block.fields, icons)
95
+ }))
96
+ };
97
+ }
98
+ return field;
99
+ });
100
+ const normalizeIconsForClient = (icons, resolveIcon)=>{
101
+ return normalizeIcons(icons).map(({ Icon, component, ...icon })=>({
102
+ ...icon,
103
+ value: resolveIcon ? resolveIcon({
104
+ Icon,
105
+ component,
106
+ ...icon
107
+ }) : icon.value ?? icon.name
108
+ }));
109
+ };
110
+ const normalizeIcons = (icons)=>{
111
+ if (Array.isArray(icons)) {
112
+ return icons;
113
+ }
114
+ return Object.entries(icons).map(([name, icon])=>{
115
+ if (isIconComponent(icon)) {
116
+ return {
117
+ Icon: icon,
118
+ name
119
+ };
120
+ }
121
+ return {
122
+ name,
123
+ ...icon
124
+ };
125
+ });
126
+ };
127
+ const isIconComponent = (icon)=>{
128
+ if (typeof icon === "function" || typeof icon === "string") {
129
+ return true;
130
+ }
131
+ return !("Icon" in icon || "component" in icon || "label" in icon || "value" in icon);
132
+ };
package/package.json ADDED
@@ -0,0 +1,134 @@
1
+ {
2
+ "name": "@mvriu5/payload-icon-picker",
3
+ "version": "0.1.1",
4
+ "description": "Payload plugin introducing an icon picker into the CMS.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.ts",
10
+ "types": "./src/index.ts",
11
+ "default": "./src/index.ts"
12
+ },
13
+ "./client": {
14
+ "import": "./src/exports/client.ts",
15
+ "types": "./src/exports/client.ts",
16
+ "default": "./src/exports/client.ts"
17
+ },
18
+ "./rsc": {
19
+ "import": "./src/exports/rsc.ts",
20
+ "types": "./src/exports/rsc.ts",
21
+ "default": "./src/exports/rsc.ts"
22
+ }
23
+ },
24
+ "main": "./src/index.ts",
25
+ "types": "./src/index.ts",
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
31
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
32
+ "build:types": "tsc --outDir dist --rootDir ./src",
33
+ "clean": "rimraf {dist,*.tsbuildinfo}",
34
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
35
+ "dev": "next dev dev --turbo",
36
+ "dev:generate-importmap": "pnpm dev:payload generate:importmap",
37
+ "dev:generate-types": "pnpm dev:payload generate:types",
38
+ "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
39
+ "generate:importmap": "pnpm dev:generate-importmap",
40
+ "generate:types": "pnpm dev:generate-types",
41
+ "prepublishOnly": "pnpm clean && pnpm build",
42
+ "test": "pnpm test:unit && pnpm test:components",
43
+ "test:e2e": "playwright test",
44
+ "test:e2e:headed": "playwright test --headed",
45
+ "test:components": "vitest run tests/components",
46
+ "test:unit": "vitest run tests/unit",
47
+ "knip": "knip"
48
+ },
49
+ "devDependencies": {
50
+ "@payloadcms/db-postgres": "3.84.1",
51
+ "@payloadcms/next": "3.84.1",
52
+ "@payloadcms/richtext-lexical": "3.84.1",
53
+ "@payloadcms/ui": "3.84.1",
54
+ "@playwright/test": "1.58.2",
55
+ "@swc/cli": "0.6.0",
56
+ "@types/node": "22.19.9",
57
+ "@types/react": "19.2.14",
58
+ "@types/react-dom": "19.2.3",
59
+ "copyfiles": "2.4.1",
60
+ "cross-env": "^7.0.3",
61
+ "eslint": "^9.23.0",
62
+ "graphql": "^16.8.1",
63
+ "mongodb-memory-server": "10.1.4",
64
+ "next": "16.2.6",
65
+ "payload": "3.84.1",
66
+ "prettier": "^3.4.2",
67
+ "react": "19.2.6",
68
+ "react-dom": "19.2.6",
69
+ "rimraf": "3.0.2",
70
+ "sharp": "0.34.2",
71
+ "typescript": "5.7.3",
72
+ "vitest": "4.0.18"
73
+ },
74
+ "peerDependencies": {
75
+ "payload": "^3.84.1",
76
+ "react": "^19.2.6",
77
+ "react-dom": "^19.2.6"
78
+ },
79
+ "engines": {
80
+ "node": "^18.20.2 || >=20.9.0",
81
+ "pnpm": "^9 || ^10 || ^11"
82
+ },
83
+ "publishConfig": {
84
+ "exports": {
85
+ ".": {
86
+ "import": "./dist/index.js",
87
+ "types": "./dist/index.d.ts",
88
+ "default": "./dist/index.js"
89
+ },
90
+ "./client": {
91
+ "import": "./dist/exports/client.js",
92
+ "types": "./dist/exports/client.d.ts",
93
+ "default": "./dist/exports/client.js"
94
+ },
95
+ "./rsc": {
96
+ "import": "./dist/exports/rsc.js",
97
+ "types": "./dist/exports/rsc.d.ts",
98
+ "default": "./dist/exports/rsc.js"
99
+ }
100
+ },
101
+ "main": "./dist/index.js",
102
+ "types": "./dist/index.d.ts"
103
+ },
104
+ "knip": {
105
+ "entry": [
106
+ "src/exports/client.ts",
107
+ "dev/payload.config.ts",
108
+ "dev/next.config.mjs",
109
+ "dev/app/**/*.{ts,tsx}"
110
+ ],
111
+ "ignore": [
112
+ "dev/payload-types.ts"
113
+ ],
114
+ "ignoreDependencies": [
115
+ "prettier"
116
+ ],
117
+ "project": [
118
+ "src/**/*.{ts,tsx}",
119
+ "dev/**/*.{ts,tsx,mjs}"
120
+ ]
121
+ },
122
+ "pnpm": {
123
+ "onlyBuiltDependencies": [
124
+ "sharp",
125
+ "esbuild",
126
+ "unrs-resolver"
127
+ ]
128
+ },
129
+ "registry": "https://registry.npmjs.org/",
130
+ "dependencies": {
131
+ "knip": "^6.24.0",
132
+ "lucide-react": "^1.23.0"
133
+ }
134
+ }