@asteby/metacore-runtime-react 28.1.0 → 28.2.0
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/CHANGELOG.md +6 -0
- package/dist/dynamic-columns.d.ts +11 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +18 -12
- package/dist/dynamic-form.d.ts +1 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +8 -0
- package/dist/dynamic-icon.d.ts +1 -0
- package/dist/dynamic-icon.d.ts.map +1 -1
- package/dist/dynamic-icon.js +29 -11
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +6 -1
- package/dist/icon-picker-field.d.ts +10 -0
- package/dist/icon-picker-field.d.ts.map +1 -0
- package/dist/icon-picker-field.js +43 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/icon-picker-field.test.tsx +67 -0
- package/src/__tests__/image-cell-lucide.test.tsx +52 -0
- package/src/dynamic-columns.tsx +33 -23
- package/src/dynamic-form.tsx +8 -0
- package/src/dynamic-icon.tsx +27 -9
- package/src/dynamic-select-field.tsx +10 -1
- package/src/icon-picker-field.tsx +134 -0
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 28.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c9206cf: Nuevo widget de formulario `icon` (IconPickerField): buscador de íconos lucide con grid y preview, más un modo Imagen que delega en UploadField. El valor almacenado sigue siendo un string retrocompatible (nombre lucide o url/path). Las celdas `image` y los thumbnails de opciones ahora reconocen nombres lucide (PascalCase o kebab, ej. "credit-card") y renderizan el glifo en vez de un `<img>` roto.
|
|
8
|
+
|
|
3
9
|
## 28.1.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
1
2
|
import { type Locale } from 'date-fns';
|
|
2
3
|
import type { ColumnDefinition } from './types';
|
|
3
4
|
import type { GetDynamicColumns } from './dynamic-columns-shim';
|
|
@@ -142,6 +143,16 @@ export declare const resolveRelationSubtitle: (col: ColumnDefinition, row: any)
|
|
|
142
143
|
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
143
144
|
* URL resolution.
|
|
144
145
|
*/
|
|
146
|
+
/**
|
|
147
|
+
* `image`-type cell body. A value that is a lucide icon name (PascalCase or
|
|
148
|
+
* kebab slug, e.g. "Banknote" / "credit-card") — the convention the `icon`
|
|
149
|
+
* form widget stores — renders the glyph instead of an <img> that would 404
|
|
150
|
+
* into an empty grey box. Exported for tests.
|
|
151
|
+
*/
|
|
152
|
+
export declare const ImageCell: React.FC<{
|
|
153
|
+
value: unknown;
|
|
154
|
+
getImageUrl: (path: string) => string;
|
|
155
|
+
}>;
|
|
145
156
|
export declare function makeDefaultGetDynamicColumns(helpers?: DynamicColumnsHelpers): GetDynamicColumns;
|
|
146
157
|
/**
|
|
147
158
|
* Eager-built variant — equivalent to `makeDefaultGetDynamicColumns()`. Use
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAwC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAwFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GACzB,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACR,OAAO,GAAG,EACV,mBAAe,KAChB,MAAM,GAAG,SAcX,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA+ID;;;;GAIG;AACH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CACxC,CAqBA,CAAA;AAED,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAkmBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -459,6 +459,23 @@ const AvatarCell = ({ name, desc, avatarSrc, getImageUrl }) => (_jsxs("div", { c
|
|
|
459
459
|
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
460
460
|
* URL resolution.
|
|
461
461
|
*/
|
|
462
|
+
/**
|
|
463
|
+
* `image`-type cell body. A value that is a lucide icon name (PascalCase or
|
|
464
|
+
* kebab slug, e.g. "Banknote" / "credit-card") — the convention the `icon`
|
|
465
|
+
* form widget stores — renders the glyph instead of an <img> that would 404
|
|
466
|
+
* into an empty grey box. Exported for tests.
|
|
467
|
+
*/
|
|
468
|
+
export const ImageCell = ({ value, getImageUrl }) => {
|
|
469
|
+
if (!value)
|
|
470
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
471
|
+
if (isLucideIconName(value)) {
|
|
472
|
+
return (_jsx("div", { className: "h-10 w-10 flex items-center justify-center rounded bg-muted", children: _jsx(DynamicIcon, { name: value, className: "h-5 w-5" }) }));
|
|
473
|
+
}
|
|
474
|
+
return (_jsx("div", { className: "h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center", children: _jsx("img", { src: getImageUrl(String(value)), alt: "Thumbnail", className: "h-full w-full object-contain", onError: (e) => {
|
|
475
|
+
;
|
|
476
|
+
e.currentTarget.style.display = 'none';
|
|
477
|
+
} }) }));
|
|
478
|
+
};
|
|
462
479
|
export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
463
480
|
const getImageUrl = helpers.getImageUrl ?? defaultGetImageUrl;
|
|
464
481
|
const apiBaseUrl = helpers.apiBaseUrl ?? '';
|
|
@@ -753,18 +770,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
753
770
|
(Array.isArray(row.original.media)
|
|
754
771
|
? row.original.media.find((m) => m.type === 'image')?.url
|
|
755
772
|
: null);
|
|
756
|
-
|
|
757
|
-
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
758
|
-
// Lucide icon name, not an image path (e.g. an addon's
|
|
759
|
-
// `icon` column seeded as "Banknote") — render the glyph;
|
|
760
|
-
// an <img> here would 404 into an empty grey box.
|
|
761
|
-
if (isLucideIconName(imageValue)) {
|
|
762
|
-
return (_jsx("div", { className: "h-10 w-10 flex items-center justify-center rounded bg-muted", children: _jsx(DynamicIcon, { name: imageValue, className: "h-5 w-5" }) }));
|
|
763
|
-
}
|
|
764
|
-
return (_jsx("div", { className: "h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center", children: _jsx("img", { src: getImageUrl(String(imageValue)), alt: "Thumbnail", className: "h-full w-full object-contain", onError: (e) => {
|
|
765
|
-
;
|
|
766
|
-
e.currentTarget.style.display = 'none';
|
|
767
|
-
} }) }));
|
|
773
|
+
return _jsx(ImageCell, { value: imageValue, getImageUrl: getImageUrl });
|
|
768
774
|
}
|
|
769
775
|
default: {
|
|
770
776
|
if (typeof value === 'object' && value !== null) {
|
package/dist/dynamic-form.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { DynamicLineItems } from './dynamic-line-items';
|
|
|
5
5
|
export { DynamicSelectField } from './dynamic-select-field';
|
|
6
6
|
export { DynamicDateField } from './dynamic-date-field';
|
|
7
7
|
export { UploadField } from './upload-field';
|
|
8
|
+
export { IconPickerField } from './icon-picker-field';
|
|
8
9
|
export interface DynamicFormProps {
|
|
9
10
|
fields: ActionFieldDef[];
|
|
10
11
|
initialValues?: Record<string, any>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EACH,cAAc,EACd,aAAa,EAKhB,MAAM,uBAAuB,CAAA;AAQ9B,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,+BA8FlB"}
|
package/dist/dynamic-form.js
CHANGED
|
@@ -10,11 +10,13 @@ import { DynamicLineItems } from './dynamic-line-items';
|
|
|
10
10
|
import { DynamicSelectField } from './dynamic-select-field';
|
|
11
11
|
import { DynamicDateField } from './dynamic-date-field';
|
|
12
12
|
import { UploadField } from './upload-field';
|
|
13
|
+
import { IconPickerField } from './icon-picker-field';
|
|
13
14
|
export { buildZodSchema, resolveWidget };
|
|
14
15
|
export { DynamicLineItems } from './dynamic-line-items';
|
|
15
16
|
export { DynamicSelectField } from './dynamic-select-field';
|
|
16
17
|
export { DynamicDateField } from './dynamic-date-field';
|
|
17
18
|
export { UploadField } from './upload-field';
|
|
19
|
+
export { IconPickerField } from './icon-picker-field';
|
|
18
20
|
export function DynamicForm({ fields, initialValues, onSubmit, onCancel, submitLabel = 'Guardar', cancelLabel = 'Cancelar', disabled = false, }) {
|
|
19
21
|
const [values, setValues] = useState({});
|
|
20
22
|
const [errors, setErrors] = useState({});
|
|
@@ -151,6 +153,12 @@ function FieldRenderer({ field, value, onChange, initialValues, effectiveOptions
|
|
|
151
153
|
if (widget === 'upload') {
|
|
152
154
|
return _jsx(UploadField, { field: field, value: value, onChange: onChange });
|
|
153
155
|
}
|
|
156
|
+
// Icon picker → lucide glyph search grid, with an "Imagen" mode that
|
|
157
|
+
// delegates to the same UploadField as `upload`. Stores a plain string:
|
|
158
|
+
// lucide name or uploaded url/path.
|
|
159
|
+
if (widget === 'icon') {
|
|
160
|
+
return _jsx(IconPickerField, { field: field, value: value, onChange: onChange });
|
|
161
|
+
}
|
|
154
162
|
// Ref-driven select: hook into useOptionsResolver so the canonical
|
|
155
163
|
// /api/options/<ref>?field=id endpoint feeds the dropdown. This is
|
|
156
164
|
// the path the kernel auto-derives for FK columns; legacy callers
|
package/dist/dynamic-icon.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export interface DynamicIconProps {
|
|
|
3
3
|
className?: string;
|
|
4
4
|
}
|
|
5
5
|
export declare function DynamicIcon({ name, className }: DynamicIconProps): import("react").JSX.Element | null;
|
|
6
|
+
export declare function resolveLucideIconName(value: unknown): string | null;
|
|
6
7
|
export declare function isLucideIconName(value: unknown): value is string;
|
|
7
8
|
//# sourceMappingURL=dynamic-icon.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-icon.d.ts","sourceRoot":"","sources":["../src/dynamic-icon.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"dynamic-icon.d.ts","sourceRoot":"","sources":["../src/dynamic-icon.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,gBAAgB,sCAKhE;AAMD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAYnE;AASD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAEhE"}
|
package/dist/dynamic-icon.js
CHANGED
|
@@ -4,21 +4,39 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
4
4
|
// icon sets can override by shadowing this component via their own prop.
|
|
5
5
|
import * as icons from 'lucide-react';
|
|
6
6
|
export function DynamicIcon({ name, className }) {
|
|
7
|
-
const
|
|
7
|
+
const resolved = resolveLucideIconName(name) ?? name;
|
|
8
|
+
const Icon = icons[resolved];
|
|
8
9
|
if (!Icon)
|
|
9
10
|
return null;
|
|
10
11
|
return _jsx(Icon, { className: className });
|
|
11
12
|
}
|
|
13
|
+
// resolveLucideIconName — canonical PascalCase lucide name for a value that is
|
|
14
|
+
// either already PascalCase ("CreditCard") or the kebab slug lucide documents
|
|
15
|
+
// ("credit-card"). Returns null for anything that is not a real glyph: empty,
|
|
16
|
+
// path-like strings (slash, dot, scheme), or the generic "Icon" base export.
|
|
17
|
+
export function resolveLucideIconName(value) {
|
|
18
|
+
if (typeof value !== 'string' || value === '' || value === 'Icon')
|
|
19
|
+
return null;
|
|
20
|
+
if (/[/\\.:\s]/.test(value))
|
|
21
|
+
return null;
|
|
22
|
+
let name = value;
|
|
23
|
+
if (/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
|
|
24
|
+
name = value
|
|
25
|
+
.split('-')
|
|
26
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
27
|
+
.join('');
|
|
28
|
+
}
|
|
29
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(name))
|
|
30
|
+
return null;
|
|
31
|
+
return icons[name] ? name : null;
|
|
32
|
+
}
|
|
12
33
|
// isLucideIconName — true when a string is a lucide-react icon name
|
|
13
|
-
// ("Banknote", "CreditCard"). Lets image-ish
|
|
14
|
-
// from an image path/URL: addons declare
|
|
15
|
-
// as OptionDef.icon), so a column
|
|
16
|
-
// strings (slash, dot, scheme)
|
|
17
|
-
//
|
|
34
|
+
// ("Banknote", "CreditCard", or the kebab slug "credit-card"). Lets image-ish
|
|
35
|
+
// renderers tell an icon name apart from an image path/URL: addons declare
|
|
36
|
+
// icons by lucide slug (same convention as OptionDef.icon), so a column
|
|
37
|
+
// inferred as `image` may carry one. Path-like strings (slash, dot, scheme)
|
|
38
|
+
// are rejected before the registry lookup; "Icon" itself is the generic base
|
|
39
|
+
// component, not a real glyph.
|
|
18
40
|
export function isLucideIconName(value) {
|
|
19
|
-
|
|
20
|
-
return false;
|
|
21
|
-
if (!/^[A-Z][A-Za-z0-9]*$/.test(value))
|
|
22
|
-
return false;
|
|
23
|
-
return Boolean(icons[value]);
|
|
41
|
+
return resolveLucideIconName(value) !== null;
|
|
24
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAwCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,IAAS,GACZ,EAAE;IACC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,+
|
|
1
|
+
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAwCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,IAAS,GACZ,EAAE;IACC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,+BA6BA;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCA4BA;AAWD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,GACnB,EAAE,uBAAuB,+BAqOzB;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -26,7 +26,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
26
26
|
import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, InitialsAvatar, } from '@asteby/metacore-ui/primitives';
|
|
27
27
|
import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react';
|
|
28
28
|
import { resolveColorCss } from '@asteby/metacore-ui/lib';
|
|
29
|
-
import { DynamicIcon } from './dynamic-icon';
|
|
29
|
+
import { DynamicIcon, isLucideIconName } from './dynamic-icon';
|
|
30
30
|
import { useOptionsResolver } from './use-options-resolver';
|
|
31
31
|
import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema';
|
|
32
32
|
/**
|
|
@@ -49,6 +49,11 @@ export function OptionThumb({ image, name, size = 20, }) {
|
|
|
49
49
|
if (!image) {
|
|
50
50
|
return _jsx(InitialsAvatar, { name: name, size: size, rounded: "sm", tone: "neutral" });
|
|
51
51
|
}
|
|
52
|
+
// A lucide icon name stored where an image url/path is expected (the `icon`
|
|
53
|
+
// form widget's icon mode) → render the glyph instead of a broken <img>.
|
|
54
|
+
if (isLucideIconName(image)) {
|
|
55
|
+
return (_jsx("span", { className: "flex shrink-0 items-center justify-center rounded-sm bg-muted", style: box, "aria-hidden": true, children: _jsx(DynamicIcon, { name: image, className: "size-4" }) }));
|
|
56
|
+
}
|
|
52
57
|
return (_jsx("img", { src: image, alt: "", "aria-hidden": true, loading: "lazy", className: "shrink-0 rounded-sm object-cover", style: box,
|
|
53
58
|
// A broken image url shouldn't leave a torn-icon glyph; collapse to
|
|
54
59
|
// the neutral placeholder background instead.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ActionFieldDef } from './types';
|
|
2
|
+
export interface IconPickerFieldProps {
|
|
3
|
+
field: ActionFieldDef;
|
|
4
|
+
value: any;
|
|
5
|
+
onChange: (v: any) => void;
|
|
6
|
+
}
|
|
7
|
+
/** True when a stored value looks like an image url/path rather than an icon name. */
|
|
8
|
+
export declare function looksLikeImageValue(value: unknown): boolean;
|
|
9
|
+
export declare function IconPickerField({ field, value, onChange }: IconPickerFieldProps): import("react").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=icon-picker-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icon-picker-field.d.ts","sourceRoot":"","sources":["../src/icon-picker-field.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAOD,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,oBAAoB,+BAiG/E"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// IconPickerField — the `icon` widget renderer for DynamicForm. Two modes:
|
|
3
|
+
//
|
|
4
|
+
// - "Ícono" (default): search box + grid of lucide glyphs filtered by name.
|
|
5
|
+
// Clicking one stores the PascalCase lucide name ("CreditCard") as the
|
|
6
|
+
// field value — the same convention OptionDef.icon and image-ish cell
|
|
7
|
+
// renderers already understand (see dynamic-icon.tsx).
|
|
8
|
+
// - "Imagen": delegates to the existing UploadField untouched, so the value
|
|
9
|
+
// is the uploaded file url/path exactly as the `upload` widget stores it.
|
|
10
|
+
//
|
|
11
|
+
// The stored value stays a plain string (retrocompatible): a lucide name when
|
|
12
|
+
// an icon was picked, a url/path when an image was uploaded. When editing, the
|
|
13
|
+
// initial mode is inferred from the current value — path-like ("/" or ".")
|
|
14
|
+
// means image, anything else means icon.
|
|
15
|
+
import { useMemo, useState } from 'react';
|
|
16
|
+
import { icons } from 'lucide-react';
|
|
17
|
+
import { Button, Input } from '@asteby/metacore-ui/primitives';
|
|
18
|
+
import { DynamicIcon, resolveLucideIconName } from './dynamic-icon';
|
|
19
|
+
import { UploadField } from './upload-field';
|
|
20
|
+
/** Max glyphs shown in the grid at once — keeps the DOM light while searching. */
|
|
21
|
+
const MAX_RESULTS = 48;
|
|
22
|
+
const ALL_ICON_NAMES = Object.keys(icons);
|
|
23
|
+
/** True when a stored value looks like an image url/path rather than an icon name. */
|
|
24
|
+
export function looksLikeImageValue(value) {
|
|
25
|
+
return typeof value === 'string' && value !== '' && /[/.]/.test(value);
|
|
26
|
+
}
|
|
27
|
+
export function IconPickerField({ field, value, onChange }) {
|
|
28
|
+
const [mode, setMode] = useState(() => looksLikeImageValue(value) ? 'image' : 'icon');
|
|
29
|
+
const [query, setQuery] = useState('');
|
|
30
|
+
const selected = mode === 'icon' ? resolveLucideIconName(value) : null;
|
|
31
|
+
const results = useMemo(() => {
|
|
32
|
+
const q = query.trim().toLowerCase().replace(/[\s_-]/g, '');
|
|
33
|
+
const names = q
|
|
34
|
+
? ALL_ICON_NAMES.filter((n) => n.toLowerCase().includes(q))
|
|
35
|
+
: ALL_ICON_NAMES;
|
|
36
|
+
// Keep the current selection visible even when it doesn't match the query.
|
|
37
|
+
if (selected && !names.slice(0, MAX_RESULTS).includes(selected)) {
|
|
38
|
+
return [selected, ...names.filter((n) => n !== selected)].slice(0, MAX_RESULTS);
|
|
39
|
+
}
|
|
40
|
+
return names.slice(0, MAX_RESULTS);
|
|
41
|
+
}, [query, selected]);
|
|
42
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex gap-1", role: "tablist", "aria-label": "Tipo de \u00EDcono", children: [_jsx(Button, { type: "button", role: "tab", "aria-selected": mode === 'icon', variant: mode === 'icon' ? 'secondary' : 'ghost', size: "sm", onClick: () => setMode('icon'), children: "\u00CDcono" }), _jsx(Button, { type: "button", role: "tab", "aria-selected": mode === 'image', variant: mode === 'image' ? 'secondary' : 'ghost', size: "sm", onClick: () => setMode('image'), children: "Imagen" })] }), mode === 'image' ? (_jsx(UploadField, { field: field, value: value, onChange: onChange })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Input, { id: field.key, value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Buscar \u00EDcono...", "aria-label": "Buscar \u00EDcono" }), selected && (_jsx("div", { "data-testid": "icon-picker-preview", className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-md border bg-muted", title: selected, children: _jsx(DynamicIcon, { name: selected, className: "h-6 w-6" }) }))] }), _jsxs("div", { className: "grid max-h-56 grid-cols-8 gap-1 overflow-y-auto rounded-md border p-2", role: "listbox", "aria-label": "\u00CDconos", children: [results.map((name) => (_jsx("button", { type: "button", role: "option", "aria-selected": name === selected, "aria-label": name, title: name, onClick: () => onChange(name), className: `flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent ${name === selected ? 'bg-accent ring-1 ring-primary' : ''}`, children: _jsx(DynamicIcon, { name: name, className: "h-4 w-4" }) }, name))), results.length === 0 && (_jsx("span", { className: "col-span-8 py-4 text-center text-sm text-muted-foreground", children: "Sin resultados" }))] })] }))] }));
|
|
43
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -296,7 +296,7 @@ export interface FieldValidation {
|
|
|
296
296
|
max?: number;
|
|
297
297
|
custom?: string;
|
|
298
298
|
}
|
|
299
|
-
export type FieldWidget = 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'dynamic_select' | 'switch' | 'upload';
|
|
299
|
+
export type FieldWidget = 'text' | 'textarea' | 'richtext' | 'color' | 'number' | 'date' | 'select' | 'dynamic_select' | 'switch' | 'upload' | 'icon';
|
|
300
300
|
/**
|
|
301
301
|
* Per-option visibility gate for a STATIC enum (`options[]`). The option is only
|
|
302
302
|
* offered when the value of a sibling field (`field`, defaulting to the
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,YAAY,EAAE,CAAA;IAC1B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,SAAS,EAAE,CAAA;IACpB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,aAAa,EAAE,CAAA;IAC7B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAA;CAClC;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACxD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,OAAO,CAAC,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAC3D,CAAA;CACJ;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC1D;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACb;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IACzB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,IAAI,EAAE,aAAa,GAAG,cAAc,CAAA;IACpC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,GAAG,gBAAgB,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IACtF,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACrF,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEjF,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EACE,MAAM,GACN,QAAQ,GACR,MAAM,GAGN,UAAU,GACV,WAAW,GACX,aAAa,GACb,QAAQ,GACR,QAAQ,GACR,qBAAqB,GACrB,QAAQ,GACR,SAAS,GACT,OAAO,GACP,eAAe,GACf,OAAO,GAEP,KAAK,GACL,MAAM,GACN,OAAO,GACP,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,OAAO,GACP,MAAM,GACN,eAAe,GACf,SAAS,GACT,MAAM,GAKN,UAAU,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;IACnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,gBAAgB,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;IAC7F,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC3E;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,eAAe,EAAE,CAAA;IAC9B,8DAA8D;IAC9D,WAAW,CAAC,EAAE,eAAe,EAAE,CAAA;CAClC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,QAAQ,CAAA;IACxC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC3B;AASD,MAAM,WAAW,eAAe;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAID,MAAM,MAAM,WAAW,GACjB,MAAM,GACN,UAAU,GACV,UAAU,GACV,OAAO,GACP,QAAQ,GACR,MAAM,GACN,QAAQ,GACR,gBAAgB,GAChB,QAAQ,GACR,QAAQ,GACR,MAAM,CAAA;AAEZ;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACvB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,EAAE,CAAC,EAAE,MAAM,EAAE,CAAA;IACb,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,UAAU,CAAA;CACpB;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,SAAS,EAAE,CAAA;IACrB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,MAAM,CAAC,EAAE,WAAW,GAAG,MAAM,CAAA;IAC7B;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,kBAAkB,CAAA;IAClC,0EAA0E;IAC1E,cAAc,CAAC,EAAE,kBAAkB,CAAA;IACnC;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,cAAc,EAAE,CAAA;IAC7B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sDAAsD;IACtD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IAC/B,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;CACzC;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,CAAC,CAAA;IACP,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,cAAc;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CAChB;AAKD,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,cAAc,EAAE,CAAA;CAC3B;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;IACzB,4EAA4E;IAC5E,KAAK,CAAC,EAAE,UAAU,EAAE,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;CACzC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "28.
|
|
3
|
+
"version": "28.2.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"typescript": "^6.0.0",
|
|
68
68
|
"vitest": "^4.0.0",
|
|
69
69
|
"zustand": "^5.0.0",
|
|
70
|
-
"@asteby/metacore-
|
|
71
|
-
"@asteby/metacore-
|
|
70
|
+
"@asteby/metacore-sdk": "3.4.0",
|
|
71
|
+
"@asteby/metacore-ui": "2.12.1"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"build": "tsc -p tsconfig.json",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// IconPickerField — the `icon` form widget: lucide search grid (icon mode) and
|
|
4
|
+
// delegation to UploadField (image mode). UploadField is stubbed so the image
|
|
5
|
+
// mode asserts delegation without needing an ApiProvider.
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
import { cleanup, render, screen, fireEvent } from '@testing-library/react'
|
|
8
|
+
|
|
9
|
+
vi.mock('../upload-field', () => ({
|
|
10
|
+
UploadField: ({ field, value }: any) => (
|
|
11
|
+
<div data-testid="upload-field" data-field-key={field.key} data-value={String(value ?? '')} />
|
|
12
|
+
),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
import { IconPickerField, looksLikeImageValue } from '../icon-picker-field'
|
|
16
|
+
import type { ActionFieldDef } from '../types'
|
|
17
|
+
|
|
18
|
+
afterEach(cleanup)
|
|
19
|
+
|
|
20
|
+
const field: ActionFieldDef = { key: 'icon', label: 'Ícono', type: 'text', widget: 'icon' }
|
|
21
|
+
|
|
22
|
+
describe('IconPickerField', () => {
|
|
23
|
+
it('busca "credit", muestra CreditCard y al click emite onChange con el nombre', () => {
|
|
24
|
+
const onChange = vi.fn()
|
|
25
|
+
render(<IconPickerField field={field} value="" onChange={onChange} />)
|
|
26
|
+
fireEvent.change(screen.getByLabelText('Buscar ícono'), { target: { value: 'credit' } })
|
|
27
|
+
const option = screen.getByRole('option', { name: 'CreditCard' })
|
|
28
|
+
fireEvent.click(option)
|
|
29
|
+
expect(onChange).toHaveBeenCalledWith('CreditCard')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('resalta el seleccionado y muestra el preview grande', () => {
|
|
33
|
+
render(<IconPickerField field={field} value="CreditCard" onChange={() => {}} />)
|
|
34
|
+
expect(screen.getByTestId('icon-picker-preview')).toBeTruthy()
|
|
35
|
+
expect(
|
|
36
|
+
screen.getByRole('option', { name: 'CreditCard' }).getAttribute('aria-selected'),
|
|
37
|
+
).toBe('true')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('limita el grid a 48 resultados', () => {
|
|
41
|
+
render(<IconPickerField field={field} value="" onChange={() => {}} />)
|
|
42
|
+
expect(screen.getAllByRole('option').length).toBeLessThanOrEqual(48)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('modo imagen delega a UploadField con los mismos props', () => {
|
|
46
|
+
render(<IconPickerField field={field} value="" onChange={() => {}} />)
|
|
47
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Imagen' }))
|
|
48
|
+
const stub = screen.getByTestId('upload-field')
|
|
49
|
+
expect(stub.getAttribute('data-field-key')).toBe('icon')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('arranca en modo Imagen cuando el valor parece path/URL', () => {
|
|
53
|
+
render(<IconPickerField field={field} value="/uploads/logo.png" onChange={() => {}} />)
|
|
54
|
+
expect(screen.getByTestId('upload-field').getAttribute('data-value')).toBe('/uploads/logo.png')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('looksLikeImageValue', () => {
|
|
59
|
+
it('distingue paths/URLs de nombres lucide', () => {
|
|
60
|
+
expect(looksLikeImageValue('/uploads/a.png')).toBe(true)
|
|
61
|
+
expect(looksLikeImageValue('logo.png')).toBe(true)
|
|
62
|
+
expect(looksLikeImageValue('https://x/y')).toBe(true)
|
|
63
|
+
expect(looksLikeImageValue('CreditCard')).toBe(false)
|
|
64
|
+
expect(looksLikeImageValue('credit-card')).toBe(false)
|
|
65
|
+
expect(looksLikeImageValue('')).toBe(false)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// ImageCell — an `image` cell whose value is a lucide icon name (PascalCase or
|
|
4
|
+
// kebab, as stored by the `icon` form widget) renders the glyph instead of a
|
|
5
|
+
// broken <img>; real paths/urls keep rendering an <img>.
|
|
6
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
7
|
+
import { cleanup, render } from '@testing-library/react'
|
|
8
|
+
import { ImageCell } from '../dynamic-columns'
|
|
9
|
+
import { isLucideIconName, resolveLucideIconName } from '../dynamic-icon'
|
|
10
|
+
|
|
11
|
+
afterEach(cleanup)
|
|
12
|
+
|
|
13
|
+
const getImageUrl = (p: string) => `/img${p}`
|
|
14
|
+
|
|
15
|
+
describe('ImageCell con nombre lucide', () => {
|
|
16
|
+
it('renderiza el ícono (svg) para un nombre PascalCase, sin <img>', () => {
|
|
17
|
+
const { container } = render(<ImageCell value="CreditCard" getImageUrl={getImageUrl} />)
|
|
18
|
+
expect(container.querySelector('svg')).toBeTruthy()
|
|
19
|
+
expect(container.querySelector('img')).toBeNull()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renderiza el ícono también para el slug kebab "credit-card"', () => {
|
|
23
|
+
const { container } = render(<ImageCell value="credit-card" getImageUrl={getImageUrl} />)
|
|
24
|
+
expect(container.querySelector('svg')).toBeTruthy()
|
|
25
|
+
expect(container.querySelector('img')).toBeNull()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('un path real sigue renderizando <img> con getImageUrl aplicado', () => {
|
|
29
|
+
const { container } = render(<ImageCell value="/uploads/x.png" getImageUrl={getImageUrl} />)
|
|
30
|
+
const img = container.querySelector('img')
|
|
31
|
+
expect(img?.getAttribute('src')).toBe('/img/uploads/x.png')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('valor vacío cae al guion', () => {
|
|
35
|
+
const { container } = render(<ImageCell value="" getImageUrl={getImageUrl} />)
|
|
36
|
+
expect(container.textContent).toBe('-')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('resolveLucideIconName / isLucideIconName', () => {
|
|
41
|
+
it('normaliza kebab a PascalCase', () => {
|
|
42
|
+
expect(resolveLucideIconName('credit-card')).toBe('CreditCard')
|
|
43
|
+
expect(resolveLucideIconName('CreditCard')).toBe('CreditCard')
|
|
44
|
+
})
|
|
45
|
+
it('rechaza paths, vacíos y el base "Icon"', () => {
|
|
46
|
+
expect(resolveLucideIconName('/a/b.png')).toBeNull()
|
|
47
|
+
expect(resolveLucideIconName('logo.png')).toBeNull()
|
|
48
|
+
expect(resolveLucideIconName('')).toBeNull()
|
|
49
|
+
expect(resolveLucideIconName('Icon')).toBeNull()
|
|
50
|
+
expect(isLucideIconName('no-such-icon-xyz')).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
})
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -664,6 +664,38 @@ const AvatarCell: React.FC<{
|
|
|
664
664
|
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
665
665
|
* URL resolution.
|
|
666
666
|
*/
|
|
667
|
+
/**
|
|
668
|
+
* `image`-type cell body. A value that is a lucide icon name (PascalCase or
|
|
669
|
+
* kebab slug, e.g. "Banknote" / "credit-card") — the convention the `icon`
|
|
670
|
+
* form widget stores — renders the glyph instead of an <img> that would 404
|
|
671
|
+
* into an empty grey box. Exported for tests.
|
|
672
|
+
*/
|
|
673
|
+
export const ImageCell: React.FC<{
|
|
674
|
+
value: unknown
|
|
675
|
+
getImageUrl: (path: string) => string
|
|
676
|
+
}> = ({ value, getImageUrl }) => {
|
|
677
|
+
if (!value) return <span className="text-muted-foreground">-</span>
|
|
678
|
+
if (isLucideIconName(value)) {
|
|
679
|
+
return (
|
|
680
|
+
<div className="h-10 w-10 flex items-center justify-center rounded bg-muted">
|
|
681
|
+
<DynamicIcon name={value} className="h-5 w-5" />
|
|
682
|
+
</div>
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
return (
|
|
686
|
+
<div className="h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center">
|
|
687
|
+
<img
|
|
688
|
+
src={getImageUrl(String(value))}
|
|
689
|
+
alt="Thumbnail"
|
|
690
|
+
className="h-full w-full object-contain"
|
|
691
|
+
onError={(e) => {
|
|
692
|
+
;(e.currentTarget as HTMLImageElement).style.display = 'none'
|
|
693
|
+
}}
|
|
694
|
+
/>
|
|
695
|
+
</div>
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
|
|
667
699
|
export function makeDefaultGetDynamicColumns(
|
|
668
700
|
helpers: DynamicColumnsHelpers = {},
|
|
669
701
|
): GetDynamicColumns {
|
|
@@ -1155,29 +1187,7 @@ export function makeDefaultGetDynamicColumns(
|
|
|
1155
1187
|
(Array.isArray(row.original.media)
|
|
1156
1188
|
? row.original.media.find((m: any) => m.type === 'image')?.url
|
|
1157
1189
|
: null)
|
|
1158
|
-
|
|
1159
|
-
// Lucide icon name, not an image path (e.g. an addon's
|
|
1160
|
-
// `icon` column seeded as "Banknote") — render the glyph;
|
|
1161
|
-
// an <img> here would 404 into an empty grey box.
|
|
1162
|
-
if (isLucideIconName(imageValue)) {
|
|
1163
|
-
return (
|
|
1164
|
-
<div className="h-10 w-10 flex items-center justify-center rounded bg-muted">
|
|
1165
|
-
<DynamicIcon name={imageValue} className="h-5 w-5" />
|
|
1166
|
-
</div>
|
|
1167
|
-
)
|
|
1168
|
-
}
|
|
1169
|
-
return (
|
|
1170
|
-
<div className="h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center">
|
|
1171
|
-
<img
|
|
1172
|
-
src={getImageUrl(String(imageValue))}
|
|
1173
|
-
alt="Thumbnail"
|
|
1174
|
-
className="h-full w-full object-contain"
|
|
1175
|
-
onError={(e) => {
|
|
1176
|
-
;(e.currentTarget as HTMLImageElement).style.display = 'none'
|
|
1177
|
-
}}
|
|
1178
|
-
/>
|
|
1179
|
-
</div>
|
|
1180
|
-
)
|
|
1190
|
+
return <ImageCell value={imageValue} getImageUrl={getImageUrl} />
|
|
1181
1191
|
}
|
|
1182
1192
|
|
|
1183
1193
|
default: {
|
package/src/dynamic-form.tsx
CHANGED
|
@@ -28,12 +28,14 @@ import { DynamicLineItems } from './dynamic-line-items'
|
|
|
28
28
|
import { DynamicSelectField } from './dynamic-select-field'
|
|
29
29
|
import { DynamicDateField } from './dynamic-date-field'
|
|
30
30
|
import { UploadField } from './upload-field'
|
|
31
|
+
import { IconPickerField } from './icon-picker-field'
|
|
31
32
|
|
|
32
33
|
export { buildZodSchema, resolveWidget }
|
|
33
34
|
export { DynamicLineItems } from './dynamic-line-items'
|
|
34
35
|
export { DynamicSelectField } from './dynamic-select-field'
|
|
35
36
|
export { DynamicDateField } from './dynamic-date-field'
|
|
36
37
|
export { UploadField } from './upload-field'
|
|
38
|
+
export { IconPickerField } from './icon-picker-field'
|
|
37
39
|
|
|
38
40
|
export interface DynamicFormProps {
|
|
39
41
|
fields: ActionFieldDef[]
|
|
@@ -264,6 +266,12 @@ function FieldRenderer({
|
|
|
264
266
|
if (widget === 'upload') {
|
|
265
267
|
return <UploadField field={field} value={value} onChange={onChange} />
|
|
266
268
|
}
|
|
269
|
+
// Icon picker → lucide glyph search grid, with an "Imagen" mode that
|
|
270
|
+
// delegates to the same UploadField as `upload`. Stores a plain string:
|
|
271
|
+
// lucide name or uploaded url/path.
|
|
272
|
+
if (widget === 'icon') {
|
|
273
|
+
return <IconPickerField field={field} value={value} onChange={onChange} />
|
|
274
|
+
}
|
|
267
275
|
// Ref-driven select: hook into useOptionsResolver so the canonical
|
|
268
276
|
// /api/options/<ref>?field=id endpoint feeds the dropdown. This is
|
|
269
277
|
// the path the kernel auto-derives for FK columns; legacy callers
|
package/src/dynamic-icon.tsx
CHANGED
|
@@ -9,19 +9,37 @@ export interface DynamicIconProps {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function DynamicIcon({ name, className }: DynamicIconProps) {
|
|
12
|
-
const
|
|
12
|
+
const resolved = resolveLucideIconName(name) ?? name
|
|
13
|
+
const Icon = (icons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[resolved]
|
|
13
14
|
if (!Icon) return null
|
|
14
15
|
return <Icon className={className} />
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// resolveLucideIconName — canonical PascalCase lucide name for a value that is
|
|
19
|
+
// either already PascalCase ("CreditCard") or the kebab slug lucide documents
|
|
20
|
+
// ("credit-card"). Returns null for anything that is not a real glyph: empty,
|
|
21
|
+
// path-like strings (slash, dot, scheme), or the generic "Icon" base export.
|
|
22
|
+
export function resolveLucideIconName(value: unknown): string | null {
|
|
23
|
+
if (typeof value !== 'string' || value === '' || value === 'Icon') return null
|
|
24
|
+
if (/[/\\.:\s]/.test(value)) return null
|
|
25
|
+
let name = value
|
|
26
|
+
if (/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {
|
|
27
|
+
name = value
|
|
28
|
+
.split('-')
|
|
29
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
30
|
+
.join('')
|
|
31
|
+
}
|
|
32
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) return null
|
|
33
|
+
return (icons as unknown as Record<string, unknown>)[name] ? name : null
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
// isLucideIconName — true when a string is a lucide-react icon name
|
|
18
|
-
// ("Banknote", "CreditCard"). Lets image-ish
|
|
19
|
-
// from an image path/URL: addons declare
|
|
20
|
-
// as OptionDef.icon), so a column
|
|
21
|
-
// strings (slash, dot, scheme)
|
|
22
|
-
//
|
|
37
|
+
// ("Banknote", "CreditCard", or the kebab slug "credit-card"). Lets image-ish
|
|
38
|
+
// renderers tell an icon name apart from an image path/URL: addons declare
|
|
39
|
+
// icons by lucide slug (same convention as OptionDef.icon), so a column
|
|
40
|
+
// inferred as `image` may carry one. Path-like strings (slash, dot, scheme)
|
|
41
|
+
// are rejected before the registry lookup; "Icon" itself is the generic base
|
|
42
|
+
// component, not a real glyph.
|
|
23
43
|
export function isLucideIconName(value: unknown): value is string {
|
|
24
|
-
|
|
25
|
-
if (!/^[A-Z][A-Za-z0-9]*$/.test(value)) return false
|
|
26
|
-
return Boolean((icons as unknown as Record<string, unknown>)[value])
|
|
44
|
+
return resolveLucideIconName(value) !== null
|
|
27
45
|
}
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
} from '@asteby/metacore-ui/primitives'
|
|
38
38
|
import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react'
|
|
39
39
|
import { resolveColorCss } from '@asteby/metacore-ui/lib'
|
|
40
|
-
import { DynamicIcon } from './dynamic-icon'
|
|
40
|
+
import { DynamicIcon, isLucideIconName } from './dynamic-icon'
|
|
41
41
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
42
42
|
import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema'
|
|
43
43
|
import type { ActionFieldDef } from './types'
|
|
@@ -71,6 +71,15 @@ export function OptionThumb({
|
|
|
71
71
|
if (!image) {
|
|
72
72
|
return <InitialsAvatar name={name} size={size} rounded="sm" tone="neutral" />
|
|
73
73
|
}
|
|
74
|
+
// A lucide icon name stored where an image url/path is expected (the `icon`
|
|
75
|
+
// form widget's icon mode) → render the glyph instead of a broken <img>.
|
|
76
|
+
if (isLucideIconName(image)) {
|
|
77
|
+
return (
|
|
78
|
+
<span className="flex shrink-0 items-center justify-center rounded-sm bg-muted" style={box} aria-hidden>
|
|
79
|
+
<DynamicIcon name={image} className="size-4" />
|
|
80
|
+
</span>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
74
83
|
return (
|
|
75
84
|
<img
|
|
76
85
|
src={image}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// IconPickerField — the `icon` widget renderer for DynamicForm. Two modes:
|
|
2
|
+
//
|
|
3
|
+
// - "Ícono" (default): search box + grid of lucide glyphs filtered by name.
|
|
4
|
+
// Clicking one stores the PascalCase lucide name ("CreditCard") as the
|
|
5
|
+
// field value — the same convention OptionDef.icon and image-ish cell
|
|
6
|
+
// renderers already understand (see dynamic-icon.tsx).
|
|
7
|
+
// - "Imagen": delegates to the existing UploadField untouched, so the value
|
|
8
|
+
// is the uploaded file url/path exactly as the `upload` widget stores it.
|
|
9
|
+
//
|
|
10
|
+
// The stored value stays a plain string (retrocompatible): a lucide name when
|
|
11
|
+
// an icon was picked, a url/path when an image was uploaded. When editing, the
|
|
12
|
+
// initial mode is inferred from the current value — path-like ("/" or ".")
|
|
13
|
+
// means image, anything else means icon.
|
|
14
|
+
import { useMemo, useState } from 'react'
|
|
15
|
+
import { icons } from 'lucide-react'
|
|
16
|
+
import { Button, Input } from '@asteby/metacore-ui/primitives'
|
|
17
|
+
import { DynamicIcon, resolveLucideIconName } from './dynamic-icon'
|
|
18
|
+
import { UploadField } from './upload-field'
|
|
19
|
+
import type { ActionFieldDef } from './types'
|
|
20
|
+
|
|
21
|
+
export interface IconPickerFieldProps {
|
|
22
|
+
field: ActionFieldDef
|
|
23
|
+
value: any
|
|
24
|
+
onChange: (v: any) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Max glyphs shown in the grid at once — keeps the DOM light while searching. */
|
|
28
|
+
const MAX_RESULTS = 48
|
|
29
|
+
|
|
30
|
+
const ALL_ICON_NAMES = Object.keys(icons)
|
|
31
|
+
|
|
32
|
+
/** True when a stored value looks like an image url/path rather than an icon name. */
|
|
33
|
+
export function looksLikeImageValue(value: unknown): boolean {
|
|
34
|
+
return typeof value === 'string' && value !== '' && /[/.]/.test(value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function IconPickerField({ field, value, onChange }: IconPickerFieldProps) {
|
|
38
|
+
const [mode, setMode] = useState<'icon' | 'image'>(() =>
|
|
39
|
+
looksLikeImageValue(value) ? 'image' : 'icon',
|
|
40
|
+
)
|
|
41
|
+
const [query, setQuery] = useState('')
|
|
42
|
+
|
|
43
|
+
const selected = mode === 'icon' ? resolveLucideIconName(value) : null
|
|
44
|
+
|
|
45
|
+
const results = useMemo(() => {
|
|
46
|
+
const q = query.trim().toLowerCase().replace(/[\s_-]/g, '')
|
|
47
|
+
const names = q
|
|
48
|
+
? ALL_ICON_NAMES.filter((n) => n.toLowerCase().includes(q))
|
|
49
|
+
: ALL_ICON_NAMES
|
|
50
|
+
// Keep the current selection visible even when it doesn't match the query.
|
|
51
|
+
if (selected && !names.slice(0, MAX_RESULTS).includes(selected)) {
|
|
52
|
+
return [selected, ...names.filter((n) => n !== selected)].slice(0, MAX_RESULTS)
|
|
53
|
+
}
|
|
54
|
+
return names.slice(0, MAX_RESULTS)
|
|
55
|
+
}, [query, selected])
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex flex-col gap-2">
|
|
59
|
+
<div className="flex gap-1" role="tablist" aria-label="Tipo de ícono">
|
|
60
|
+
<Button
|
|
61
|
+
type="button"
|
|
62
|
+
role="tab"
|
|
63
|
+
aria-selected={mode === 'icon'}
|
|
64
|
+
variant={mode === 'icon' ? 'secondary' : 'ghost'}
|
|
65
|
+
size="sm"
|
|
66
|
+
onClick={() => setMode('icon')}
|
|
67
|
+
>
|
|
68
|
+
Ícono
|
|
69
|
+
</Button>
|
|
70
|
+
<Button
|
|
71
|
+
type="button"
|
|
72
|
+
role="tab"
|
|
73
|
+
aria-selected={mode === 'image'}
|
|
74
|
+
variant={mode === 'image' ? 'secondary' : 'ghost'}
|
|
75
|
+
size="sm"
|
|
76
|
+
onClick={() => setMode('image')}
|
|
77
|
+
>
|
|
78
|
+
Imagen
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
{mode === 'image' ? (
|
|
82
|
+
<UploadField field={field} value={value} onChange={onChange} />
|
|
83
|
+
) : (
|
|
84
|
+
<div className="flex flex-col gap-2">
|
|
85
|
+
<div className="flex items-center gap-3">
|
|
86
|
+
<Input
|
|
87
|
+
id={field.key}
|
|
88
|
+
value={query}
|
|
89
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
|
|
90
|
+
placeholder="Buscar ícono..."
|
|
91
|
+
aria-label="Buscar ícono"
|
|
92
|
+
/>
|
|
93
|
+
{selected && (
|
|
94
|
+
<div
|
|
95
|
+
data-testid="icon-picker-preview"
|
|
96
|
+
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md border bg-muted"
|
|
97
|
+
title={selected}
|
|
98
|
+
>
|
|
99
|
+
<DynamicIcon name={selected} className="h-6 w-6" />
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
<div
|
|
104
|
+
className="grid max-h-56 grid-cols-8 gap-1 overflow-y-auto rounded-md border p-2"
|
|
105
|
+
role="listbox"
|
|
106
|
+
aria-label="Íconos"
|
|
107
|
+
>
|
|
108
|
+
{results.map((name) => (
|
|
109
|
+
<button
|
|
110
|
+
key={name}
|
|
111
|
+
type="button"
|
|
112
|
+
role="option"
|
|
113
|
+
aria-selected={name === selected}
|
|
114
|
+
aria-label={name}
|
|
115
|
+
title={name}
|
|
116
|
+
onClick={() => onChange(name)}
|
|
117
|
+
className={`flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent ${
|
|
118
|
+
name === selected ? 'bg-accent ring-1 ring-primary' : ''
|
|
119
|
+
}`}
|
|
120
|
+
>
|
|
121
|
+
<DynamicIcon name={name} className="h-4 w-4" />
|
|
122
|
+
</button>
|
|
123
|
+
))}
|
|
124
|
+
{results.length === 0 && (
|
|
125
|
+
<span className="col-span-8 py-4 text-center text-sm text-muted-foreground">
|
|
126
|
+
Sin resultados
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|