@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
- * Searchable Lucide icon picker. Stores the kebab-case Lucide name (e.g. `sparkles`).
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,EAAgB,MAAM,SAAS,CAAA;AA4BrE;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,wBA4D7B,CAAA"}
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, SelectInput, useField } from '@payloadcms/ui';
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 ICON_OPTIONS = Object.keys(icons)
19
+ const ALL_ICONS = Object.keys(icons)
20
20
  .filter((key) => /^[A-Z]/.test(key))
21
- .map((pascal) => {
22
- const value = toKebabCase(pascal);
23
- return { label: value, value };
24
- })
25
- .sort((a, b) => a.value.localeCompare(b.value));
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
- * Searchable Lucide icon picker. Stores the kebab-case Lucide name (e.g. `sparkles`).
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 PreviewIcon = selected
38
+ const SelectedIcon = selected
37
39
  ? icons[toPascalCase(selected)]
38
40
  : undefined;
39
- const options = useMemo(() => ICON_OPTIONS, []);
40
- return (_jsxs("div", { className: "field-type", children: [_jsx(FieldLabel, { label: field?.label ?? 'Icon', path: path, required: Boolean(field?.required) }), _jsxs("div", { style: { alignItems: 'center', display: 'flex', gap: '0.75rem' }, children: [_jsx("div", { "aria-hidden": true, style: {
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: 6,
44
- display: 'flex',
45
- flexShrink: 0,
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
- justifyContent: 'center',
48
- width: 40,
49
- }, children: PreviewIcon ? (_jsx(PreviewIcon, { size: 20 })) : (_jsx("span", { style: { color: 'var(--theme-elevation-400)', fontSize: 12 }, children: "\u2014" })) }), _jsx("div", { style: { flex: 1, minWidth: 0 }, children: _jsx(SelectInput, { isClearable: true, name: path, onChange: (option) => {
50
- if (Array.isArray(option)) {
51
- setValue(option[0]?.value ?? null);
52
- return;
53
- }
54
- setValue(option?.value ?? null);
55
- }, options: options, path: path, readOnly: Boolean(readOnly), value: selected }) })] }), _jsx(FieldDescription, { description: field?.admin?.description, path: path }), _jsx(FieldError, { message: errorMessage, path: path, showError: showError })] }));
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.5",
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",