@mvriu5/payload-icon-picker 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,7 @@ import { lexicalEditor } from "@payloadcms/richtext-lexical"
24
24
  import * as Icons from "lucide-react"
25
25
  import { buildConfig } from "payload"
26
26
  import { iconField, payloadIconPlugin } from "@mvriu5/payload-icon-picker"
27
+ import { lucideIconAdapter } from "@mvriu5/payload-icon-picker/adapters/lucide"
27
28
 
28
29
  export default buildConfig({
29
30
  collections: [
@@ -44,8 +45,7 @@ export default buildConfig({
44
45
  ],
45
46
  plugins: [
46
47
  payloadIconPlugin({
47
- icons: Icons,
48
- resolveIcon: ({ name }) => name,
48
+ icons: lucideIconAdapter(Icons),
49
49
  }),
50
50
  ],
51
51
  db: postgresAdapter({
@@ -84,33 +84,42 @@ Use `createIconResolver()` to turn a stored string back into the registered icon
84
84
  ```tsx
85
85
  import * as Icons from "lucide-react"
86
86
  import { createIconResolver } from "@mvriu5/payload-icon-picker"
87
+ import { lucideIconAdapter } from "@mvriu5/payload-icon-picker/adapters/lucide"
87
88
 
88
89
  const resolveStoredIcon = createIconResolver({
89
- icons: Icons,
90
- resolveIcon: ({ name }) => name,
90
+ icons: lucideIconAdapter(Icons),
91
91
  })
92
92
 
93
93
  export function PostIcon({ icon }: { icon?: string }) {
94
94
  const resolvedIcon = resolveStoredIcon(icon)
95
- const Icon = resolvedIcon?.Icon
96
95
 
97
- if (!Icon) {
96
+ if (!resolvedIcon?.svg) {
98
97
  return null
99
98
  }
100
99
 
101
- return <Icon aria-hidden size={24} />
100
+ return <span aria-hidden dangerouslySetInnerHTML={{ __html: resolvedIcon.svg }} />
102
101
  }
103
102
  ```
104
103
 
105
104
  ## Icon Inputs
106
105
 
107
- You can pass a full icon library namespace:
106
+ Use an adapter for supported icon libraries. Adapters convert React icon exports to serializable SVG metadata for the Payload admin UI.
108
107
 
109
108
  ```ts
110
109
  import * as Icons from "lucide-react"
110
+ import { lucideIconAdapter } from "@mvriu5/payload-icon-picker/adapters/lucide"
111
111
 
112
112
  payloadIconPlugin({
113
- icons: Icons,
113
+ icons: lucideIconAdapter(Icons),
114
+ })
115
+ ```
116
+
117
+ ```ts
118
+ import * as TablerIcons from "@tabler/icons-react"
119
+ import { tablerIconAdapter } from "@mvriu5/payload-icon-picker/adapters/tabler"
120
+
121
+ payloadIconPlugin({
122
+ icons: tablerIconAdapter(TablerIcons),
114
123
  })
115
124
  ```
116
125
 
@@ -149,7 +158,7 @@ iconField({
149
158
 
150
159
  ## Development
151
160
 
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.
161
+ The dev Payload config in `dev/payload.config.ts` registers the plugin with `lucide-react`, which is installed as a development dependency.
153
162
 
154
163
  ```bash
155
164
  pnpm dev
package/dist/IconField.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { FieldLabel, useField } from "@payloadcms/ui";
4
- import { icons as lucideIcons } from "lucide-react";
5
4
  import React, { useMemo, useState } from "react";
6
5
  const defaultResolveIcon = (icon)=>icon.value ?? icon.name;
7
6
  const ICON_BATCH_SIZE = 120;
@@ -131,7 +130,7 @@ export const IconField = ({ field, icons: iconsFromProps, noResultsLabel = "No i
131
130
  textOverflow: "ellipsis",
132
131
  whiteSpace: "nowrap"
133
132
  },
134
- children: selectedIcon ? selectedIcon.label ?? selectedIcon.name : placeholder
133
+ children: selectedIcon ? selectedIcon.label ?? selectedIcon.name : value || placeholder
135
134
  }),
136
135
  value ? /*#__PURE__*/ _jsx("span", {
137
136
  onClick: (event)=>{
@@ -326,7 +325,6 @@ export const IconField = ({ field, icons: iconsFromProps, noResultsLabel = "No i
326
325
  };
327
326
  const IconPreview = ({ icon })=>{
328
327
  const PreviewIcon = icon.Icon ?? icon.component;
329
- const LucideIcon = lucideIcons[icon.name];
330
328
  if (PreviewIcon) {
331
329
  return /*#__PURE__*/ _jsx(PreviewIcon, {
332
330
  "aria-hidden": true,
@@ -334,13 +332,6 @@ const IconPreview = ({ icon })=>{
334
332
  size: 24
335
333
  });
336
334
  }
337
- if (LucideIcon) {
338
- return /*#__PURE__*/ _jsx(LucideIcon, {
339
- "aria-hidden": true,
340
- focusable: false,
341
- size: 24
342
- });
343
- }
344
335
  if (icon.svg) {
345
336
  return /*#__PURE__*/ _jsx("span", {
346
337
  "aria-hidden": true,
@@ -0,0 +1,4 @@
1
+ import type { IconFieldIcon } from "../IconField.js";
2
+ import type { IconAdapterOptions, IconLibrary } from "./utils.js";
3
+ export type LucideIconAdapterOptions = IconAdapterOptions;
4
+ export declare const lucideIconAdapter: (icons: IconLibrary, options?: LucideIconAdapterOptions) => IconFieldIcon[];
@@ -0,0 +1,2 @@
1
+ import { createSvgIconAdapter } from "./utils.js";
2
+ export const lucideIconAdapter = (icons, options)=>createSvgIconAdapter(icons, options);
@@ -0,0 +1,4 @@
1
+ import type { IconFieldIcon } from "../IconField.js";
2
+ import type { IconAdapterOptions, IconLibrary } from "./utils.js";
3
+ export type TablerIconAdapterOptions = IconAdapterOptions;
4
+ export declare const tablerIconAdapter: (icons: IconLibrary, options?: TablerIconAdapterOptions) => IconFieldIcon[];
@@ -0,0 +1,2 @@
1
+ import { createSvgIconAdapter } from "./utils.js";
2
+ export const tablerIconAdapter = (icons, options)=>createSvgIconAdapter(icons, options);
@@ -0,0 +1,7 @@
1
+ import type { IconFieldIcon } from "../IconField.js";
2
+ export type IconAdapterOptions = {
3
+ exclude?: string[];
4
+ include?: string[];
5
+ };
6
+ export type IconLibrary = Record<string, unknown>;
7
+ export declare const createSvgIconAdapter: (icons: IconLibrary, options?: IconAdapterOptions) => IconFieldIcon[];
@@ -0,0 +1,77 @@
1
+ export const createSvgIconAdapter = (icons, options = {})=>{
2
+ const include = new Set(options.include ?? []);
3
+ const exclude = new Set(options.exclude ?? []);
4
+ return Object.entries(icons).flatMap(([name, icon])=>{
5
+ if (exclude.has(name) || include.size > 0 && !include.has(name)) {
6
+ return [];
7
+ }
8
+ const iconNode = getIconNode(icon);
9
+ if (!iconNode) {
10
+ return [];
11
+ }
12
+ return [
13
+ {
14
+ label: getLabel(name, icon),
15
+ name,
16
+ svg: iconNodeToSvg(iconNode),
17
+ value: name
18
+ }
19
+ ];
20
+ });
21
+ };
22
+ const getIconNode = (icon)=>{
23
+ if (!icon || typeof icon !== "object") {
24
+ return undefined;
25
+ }
26
+ const component = icon;
27
+ const rendered = component.render?.({}, null);
28
+ const iconNode = rendered?.props?.iconNode;
29
+ if (isIconNode(iconNode)) {
30
+ return iconNode;
31
+ }
32
+ if (isIconNode(rendered?.props?.children)) {
33
+ return rendered.props.children;
34
+ }
35
+ const renderedIconNode = reactElementToIconNode(rendered);
36
+ if (renderedIconNode) {
37
+ return renderedIconNode;
38
+ }
39
+ return undefined;
40
+ };
41
+ const getLabel = (name, icon)=>{
42
+ if (icon && typeof icon === "object" && "displayName" in icon && typeof icon.displayName === "string") {
43
+ return icon.displayName;
44
+ }
45
+ return name;
46
+ };
47
+ const iconNodeToSvg = (iconNode)=>{
48
+ const children = iconNode.map(([tag, attrs])=>`<${tag}${attrsToString(attrs)} />`).join("");
49
+ return `<svg fill="none" height="24" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">${children}</svg>`;
50
+ };
51
+ const reactElementToIconNode = (element)=>{
52
+ if (!element || element.type !== "svg") {
53
+ return undefined;
54
+ }
55
+ const children = Array.isArray(element.props?.children) ? element.props.children : [
56
+ element.props?.children
57
+ ];
58
+ const iconNode = children.filter(isReactElement).map((child)=>[
59
+ String(child.type),
60
+ child.props ?? {}
61
+ ]).filter(([tag])=>tag !== "undefined");
62
+ return iconNode.length > 0 ? iconNode : undefined;
63
+ };
64
+ const attrsToString = (attrs)=>{
65
+ const attrString = Object.entries(attrs).filter(([key, value])=>key !== "key" && value !== null && value !== undefined).map(([key, value])=>` ${toKebabCase(key)}="${escapeHtml(String(value))}"`).join("");
66
+ return attrString;
67
+ };
68
+ const isIconNode = (value)=>Array.isArray(value) && value.every((item)=>{
69
+ if (!Array.isArray(item) || item.length !== 2) {
70
+ return false;
71
+ }
72
+ const [tag, attrs] = item;
73
+ return typeof tag === "string" && Boolean(attrs) && typeof attrs === "object" && !Array.isArray(attrs);
74
+ });
75
+ const isReactElement = (value)=>value !== null && typeof value === "object" && "type" in value && "props" in value;
76
+ const toKebabCase = (value)=>value.replace(/[A-Z]/g, (match)=>`-${match.toLowerCase()}`);
77
+ const escapeHtml = (value)=>value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
package/package.json CHANGED
@@ -1,28 +1,33 @@
1
1
  {
2
2
  "name": "@mvriu5/payload-icon-picker",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Payload plugin introducing an icon picker into the CMS.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": {
9
- "import": "./src/index.ts",
10
- "types": "./src/index.ts",
11
- "default": "./src/index.ts"
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
12
  },
13
13
  "./client": {
14
- "import": "./src/exports/client.ts",
15
- "types": "./src/exports/client.ts",
16
- "default": "./src/exports/client.ts"
14
+ "import": "./dist/exports/client.js",
15
+ "types": "./dist/exports/client.d.ts",
16
+ "default": "./dist/exports/client.js"
17
17
  },
18
- "./rsc": {
19
- "import": "./src/exports/rsc.ts",
20
- "types": "./src/exports/rsc.ts",
21
- "default": "./src/exports/rsc.ts"
18
+ "./adapters/lucide": {
19
+ "import": "./dist/adapters/lucide.js",
20
+ "types": "./dist/adapters/lucide.d.ts",
21
+ "default": "./dist/adapters/lucide.js"
22
+ },
23
+ "./adapters/tabler": {
24
+ "import": "./dist/adapters/tabler.js",
25
+ "types": "./dist/adapters/tabler.d.ts",
26
+ "default": "./dist/adapters/tabler.js"
22
27
  }
23
28
  },
24
- "main": "./src/index.ts",
25
- "types": "./src/index.ts",
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
26
31
  "files": [
27
32
  "dist"
28
33
  ],
@@ -53,6 +58,7 @@
53
58
  "@payloadcms/ui": "3.84.1",
54
59
  "@playwright/test": "1.58.2",
55
60
  "@swc/cli": "0.6.0",
61
+ "@tabler/icons-react": "^3.44.0",
56
62
  "@types/node": "22.19.9",
57
63
  "@types/react": "19.2.14",
58
64
  "@types/react-dom": "19.2.3",
@@ -60,6 +66,7 @@
60
66
  "cross-env": "^7.0.3",
61
67
  "eslint": "^9.23.0",
62
68
  "graphql": "^16.8.1",
69
+ "lucide-react": "^1.23.0",
63
70
  "mongodb-memory-server": "10.1.4",
64
71
  "next": "16.2.6",
65
72
  "payload": "3.84.1",
@@ -92,10 +99,15 @@
92
99
  "types": "./dist/exports/client.d.ts",
93
100
  "default": "./dist/exports/client.js"
94
101
  },
95
- "./rsc": {
96
- "import": "./dist/exports/rsc.js",
97
- "types": "./dist/exports/rsc.d.ts",
98
- "default": "./dist/exports/rsc.js"
102
+ "./adapters/lucide": {
103
+ "import": "./dist/adapters/lucide.js",
104
+ "types": "./dist/adapters/lucide.d.ts",
105
+ "default": "./dist/adapters/lucide.js"
106
+ },
107
+ "./adapters/tabler": {
108
+ "import": "./dist/adapters/tabler.js",
109
+ "types": "./dist/adapters/tabler.d.ts",
110
+ "default": "./dist/adapters/tabler.js"
99
111
  }
100
112
  },
101
113
  "main": "./dist/index.js",
@@ -128,7 +140,6 @@
128
140
  },
129
141
  "registry": "https://registry.npmjs.org/",
130
142
  "dependencies": {
131
- "knip": "^6.24.0",
132
- "lucide-react": "^1.23.0"
143
+ "knip": "^6.24.0"
133
144
  }
134
145
  }