@dappermountain/payload-plugin-achievements 0.2.5 → 0.2.6
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TextFieldClientComponent } from 'payload';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Emoji-picker style Lucide field: searchable icon grid, stores kebab-case names.
|
|
4
4
|
*/
|
|
5
5
|
export declare const LucideIconField: TextFieldClientComponent;
|
|
6
6
|
//# sourceMappingURL=LucideIconField.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LucideIconField.d.ts","sourceRoot":"","sources":["../../src/admin/LucideIconField.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,
|
|
1
|
+
{"version":3,"file":"LucideIconField.d.ts","sourceRoot":"","sources":["../../src/admin/LucideIconField.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AAoCvD;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,wBAsO7B,CAAA"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { FieldDescription, FieldError, FieldLabel,
|
|
3
|
+
import { FieldDescription, FieldError, FieldLabel, useField } from '@payloadcms/ui';
|
|
4
4
|
import { icons } from 'lucide-react';
|
|
5
|
-
import { useMemo } from 'react';
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
function toKebabCase(pascal) {
|
|
7
7
|
return pascal
|
|
8
8
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
@@ -16,15 +16,17 @@ function toPascalCase(kebab) {
|
|
|
16
16
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
17
17
|
.join('');
|
|
18
18
|
}
|
|
19
|
-
const
|
|
19
|
+
const ALL_ICONS = Object.keys(icons)
|
|
20
20
|
.filter((key) => /^[A-Z]/.test(key))
|
|
21
|
-
.map((pascal) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
25
|
-
.sort((a, b) => a.
|
|
21
|
+
.map((pascal) => ({
|
|
22
|
+
name: toKebabCase(pascal),
|
|
23
|
+
Icon: icons[pascal],
|
|
24
|
+
}))
|
|
25
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
26
|
+
const MAX_VISIBLE = 180;
|
|
27
|
+
const CELL = 36;
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
29
|
+
* Emoji-picker style Lucide field: searchable icon grid, stores kebab-case names.
|
|
28
30
|
*/
|
|
29
31
|
export const LucideIconField = (props) => {
|
|
30
32
|
const { field, path: pathFromProps, readOnly } = props;
|
|
@@ -33,24 +35,137 @@ export const LucideIconField = (props) => {
|
|
|
33
35
|
path,
|
|
34
36
|
});
|
|
35
37
|
const selected = typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
36
|
-
const
|
|
38
|
+
const SelectedIcon = selected
|
|
37
39
|
? icons[toPascalCase(selected)]
|
|
38
40
|
: undefined;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const [query, setQuery] = useState('');
|
|
43
|
+
const rootRef = useRef(null);
|
|
44
|
+
const searchRef = useRef(null);
|
|
45
|
+
const filtered = useMemo(() => {
|
|
46
|
+
const q = query.trim().toLowerCase();
|
|
47
|
+
const list = q
|
|
48
|
+
? ALL_ICONS.filter((entry) => entry.name.includes(q))
|
|
49
|
+
: ALL_ICONS;
|
|
50
|
+
return list.slice(0, MAX_VISIBLE);
|
|
51
|
+
}, [query]);
|
|
52
|
+
const totalMatches = useMemo(() => {
|
|
53
|
+
const q = query.trim().toLowerCase();
|
|
54
|
+
if (!q)
|
|
55
|
+
return ALL_ICONS.length;
|
|
56
|
+
return ALL_ICONS.filter((entry) => entry.name.includes(q)).length;
|
|
57
|
+
}, [query]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!open)
|
|
60
|
+
return;
|
|
61
|
+
const onPointerDown = (event) => {
|
|
62
|
+
if (!rootRef.current?.contains(event.target)) {
|
|
63
|
+
setOpen(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const onKeyDown = (event) => {
|
|
67
|
+
if (event.key === 'Escape')
|
|
68
|
+
setOpen(false);
|
|
69
|
+
};
|
|
70
|
+
document.addEventListener('mousedown', onPointerDown);
|
|
71
|
+
document.addEventListener('keydown', onKeyDown);
|
|
72
|
+
queueMicrotask(() => searchRef.current?.focus());
|
|
73
|
+
return () => {
|
|
74
|
+
document.removeEventListener('mousedown', onPointerDown);
|
|
75
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
76
|
+
};
|
|
77
|
+
}, [open]);
|
|
78
|
+
const pick = useCallback((name) => {
|
|
79
|
+
setValue(name);
|
|
80
|
+
setOpen(false);
|
|
81
|
+
setQuery('');
|
|
82
|
+
}, [setValue]);
|
|
83
|
+
const clear = useCallback(() => {
|
|
84
|
+
setValue(null);
|
|
85
|
+
setQuery('');
|
|
86
|
+
}, [setValue]);
|
|
87
|
+
return (_jsxs("div", { className: "field-type", ref: rootRef, children: [_jsx(FieldLabel, { label: field?.label ?? 'Icon', path: path, required: Boolean(field?.required) }), _jsxs("div", { style: { alignItems: 'center', display: 'flex', gap: '0.5rem', position: 'relative' }, children: [_jsxs("button", { "aria-expanded": open, "aria-haspopup": "dialog", disabled: Boolean(readOnly), onClick: () => setOpen((prev) => !prev), style: {
|
|
41
88
|
alignItems: 'center',
|
|
89
|
+
background: 'var(--theme-input-bg)',
|
|
42
90
|
border: '1px solid var(--theme-elevation-150)',
|
|
43
|
-
borderRadius:
|
|
44
|
-
|
|
45
|
-
|
|
91
|
+
borderRadius: 'var(--style-radius-s, 4px)',
|
|
92
|
+
color: 'var(--theme-text)',
|
|
93
|
+
cursor: readOnly ? 'default' : 'pointer',
|
|
94
|
+
display: 'inline-flex',
|
|
95
|
+
gap: '0.5rem',
|
|
96
|
+
minHeight: 40,
|
|
97
|
+
minWidth: 160,
|
|
98
|
+
padding: '0 0.75rem',
|
|
99
|
+
}, type: "button", children: [_jsx("span", { "aria-hidden": true, style: {
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
display: 'inline-flex',
|
|
102
|
+
height: 22,
|
|
103
|
+
justifyContent: 'center',
|
|
104
|
+
width: 22,
|
|
105
|
+
}, children: SelectedIcon ? (_jsx(SelectedIcon, { size: 18 })) : (_jsx("span", { style: { color: 'var(--theme-elevation-400)', fontSize: 12 }, children: "\u2014" })) }), _jsx("span", { style: { fontSize: 13, opacity: selected ? 1 : 0.55 }, children: selected ?? 'Choose icon…' })] }), selected && !readOnly ? (_jsx("button", { onClick: clear, style: {
|
|
106
|
+
background: 'transparent',
|
|
107
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
108
|
+
borderRadius: 'var(--style-radius-s, 4px)',
|
|
109
|
+
color: 'var(--theme-elevation-600)',
|
|
110
|
+
cursor: 'pointer',
|
|
111
|
+
fontSize: 12,
|
|
46
112
|
height: 40,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
113
|
+
padding: '0 0.65rem',
|
|
114
|
+
}, type: "button", children: "Clear" })) : null, open && !readOnly ? (_jsxs("div", { role: "dialog", style: {
|
|
115
|
+
background: 'var(--theme-elevation-0)',
|
|
116
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
117
|
+
borderRadius: 'var(--style-radius-m, 6px)',
|
|
118
|
+
boxShadow: '0 8px 28px rgba(0,0,0,0.18)',
|
|
119
|
+
display: 'flex',
|
|
120
|
+
flexDirection: 'column',
|
|
121
|
+
left: 0,
|
|
122
|
+
maxWidth: 'min(360px, 100vw)',
|
|
123
|
+
padding: '0.65rem',
|
|
124
|
+
position: 'absolute',
|
|
125
|
+
top: 'calc(100% + 6px)',
|
|
126
|
+
width: 360,
|
|
127
|
+
zIndex: 40,
|
|
128
|
+
}, children: [_jsx("input", { "aria-label": "Search Lucide icons", onChange: (event) => setQuery(event.target.value), placeholder: "Search icons\u2026", ref: searchRef, style: {
|
|
129
|
+
background: 'var(--theme-input-bg)',
|
|
130
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
131
|
+
borderRadius: 'var(--style-radius-s, 4px)',
|
|
132
|
+
color: 'var(--theme-text)',
|
|
133
|
+
fontSize: 13,
|
|
134
|
+
marginBottom: '0.5rem',
|
|
135
|
+
padding: '0.45rem 0.6rem',
|
|
136
|
+
width: '100%',
|
|
137
|
+
}, type: "search", value: query }), _jsx("div", { style: {
|
|
138
|
+
display: 'grid',
|
|
139
|
+
gap: 4,
|
|
140
|
+
gridTemplateColumns: `repeat(auto-fill, minmax(${CELL}px, 1fr))`,
|
|
141
|
+
maxHeight: 260,
|
|
142
|
+
overflowY: 'auto',
|
|
143
|
+
paddingBottom: 4,
|
|
144
|
+
}, children: filtered.map(({ name, Icon }) => {
|
|
145
|
+
const isActive = name === selected;
|
|
146
|
+
return (_jsx("button", { onClick: () => pick(name), style: {
|
|
147
|
+
alignItems: 'center',
|
|
148
|
+
aspectRatio: '1',
|
|
149
|
+
background: isActive
|
|
150
|
+
? 'var(--theme-success-100, var(--theme-elevation-100))'
|
|
151
|
+
: 'transparent',
|
|
152
|
+
border: isActive
|
|
153
|
+
? '1px solid var(--theme-success-500, var(--theme-elevation-400))'
|
|
154
|
+
: '1px solid transparent',
|
|
155
|
+
borderRadius: 6,
|
|
156
|
+
color: 'var(--theme-text)',
|
|
157
|
+
cursor: 'pointer',
|
|
158
|
+
display: 'inline-flex',
|
|
159
|
+
justifyContent: 'center',
|
|
160
|
+
padding: 0,
|
|
161
|
+
}, title: name, type: "button", children: _jsx(Icon, { size: 18 }) }, name));
|
|
162
|
+
}) }), _jsx("div", { style: {
|
|
163
|
+
color: 'var(--theme-elevation-500)',
|
|
164
|
+
fontSize: 11,
|
|
165
|
+
marginTop: 6,
|
|
166
|
+
}, children: totalMatches === 0
|
|
167
|
+
? 'No icons match.'
|
|
168
|
+
: totalMatches > MAX_VISIBLE
|
|
169
|
+
? `Showing ${MAX_VISIBLE} of ${totalMatches} — refine search`
|
|
170
|
+
: `${totalMatches} icon${totalMatches === 1 ? '' : 's'}` })] })) : null] }), _jsx(FieldDescription, { description: field?.admin?.description, path: path }), _jsx(FieldError, { message: errorMessage, path: path, showError: showError })] }));
|
|
56
171
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dappermountain/payload-plugin-achievements",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "A robust, Admin-configured achievements engine for Payload: composable unlock rules, derived rank progress, request/review grants, metrics, and scoped multi-tenant support.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|