@asteby/metacore-runtime-react 28.3.1 → 28.3.3

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 28.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 99c476a: El selector de íconos es ahora un combobox: un botón que muestra el ícono y
8
+ nombre elegido y abre un popover al click (no la grilla siempre abierta), con
9
+ buscador, cada opción con su glifo y su nombre, y carga incremental al hacer
10
+ scroll (el catálogo lucide son ~1500 íconos).
11
+
12
+ ## 28.3.2
13
+
14
+ ### Patch Changes
15
+
16
+ - 0fad478: El modal de crear/editar registro (DynamicRecordDialog) ahora renderiza el
17
+ selector de íconos para campos con tipo/widget "icon". Antes solo dynamic-form
18
+ lo manejaba, así que en el modal el campo caía a un input de texto vacío.
19
+
3
20
  ## 28.3.1
4
21
 
5
22
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAwC1C,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAiBjF,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnE,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,SAAS,EAAE,CAAA;CAC5B;AAiCD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAK1D;AAaD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,CAgBtE;AAID,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,GAAG,SAAS,CAExE;AAQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CASrE;AASD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,cAAc,GAAG,IAAI,CAa5F;AA6FD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAUjE;AAMD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GACjC,QAAQ,EAAE,CAMZ;AAED,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,aAAa,EACb,WAA8B,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACX,EAAE,wBAAwB,+BAoc1B;AA+DD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,GAAG,CAAA;CAAE,+BAWlF;AAyDD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,KAAK,EAAE,QAAQ,EACf,MAAM,EACN,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,GACzB,EAAE;IACC,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,+BAgQA;AAsID,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;IAC1D,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC5B,iFAAiF;IACjF,MAAM,CAAC,EAAE,GAAG,CAAA;CACf,+BA0MA"}
@@ -28,6 +28,7 @@ import { FieldCell } from '../field-grid';
28
28
  import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
29
29
  import { normalizeRefFieldsForSubmit } from './normalize-submit';
30
30
  import { DynamicIcon, isLucideIconName } from '../dynamic-icon';
31
+ import { IconPickerField } from '../icon-picker-field';
31
32
  import { humanizeToken } from '../dynamic-columns-helpers';
32
33
  import { formatDateCell } from '../dynamic-columns';
33
34
  import { OptionBadge, statusColorFor, useIsDarkTheme, } from '../display-value';
@@ -874,6 +875,16 @@ export function EditField({ field, value, onChange, record }) {
874
875
  if (field.type === 'textarea') {
875
876
  return (_jsx(Textarea, { value: value ?? '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, rows: 4 }));
876
877
  }
878
+ // Icon picker: the kernel serves `type: 'icon'` (or an explicit
879
+ // `widget: 'icon'`) when the manifest column declares `widget: "icon"`.
880
+ // This modal renderer is SEPARATE from dynamic-form's FieldRenderer, so it
881
+ // needs its own branch — without it the field fell through to a raw text
882
+ // input. Same lucide-search-or-upload picker used elsewhere.
883
+ if (field.type === 'icon' || field.widget === 'icon') {
884
+ // IconPickerField types `field` as ActionFieldDef; the modal's FieldDef
885
+ // is structurally compatible for the props it reads (key/label/etc).
886
+ return (_jsx(IconPickerField, { field: field, value: value, onChange: onChange }));
887
+ }
877
888
  // Media widgets: the kernel may serve an explicit `widget: 'upload'` (or the
878
889
  // `image` type) for a file/photo column.
879
890
  if (field.type === 'image' || field.widget === 'upload') {
@@ -1 +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"}
1
+ {"version":3,"file":"icon-picker-field.d.ts","sourceRoot":"","sources":["../src/icon-picker-field.tsx"],"names":[],"mappings":"AAyBA,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;AAQD,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,oBAAoB,+BAgJ/E"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // IconPickerField — the `icon` widget renderer for DynamicForm. Two modes:
3
3
  //
4
4
  // - "Ícono" (default): search box + grid of lucide glyphs filtered by name.
@@ -14,11 +14,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  // means image, anything else means icon.
15
15
  import { useMemo, useState } from 'react';
16
16
  import { icons } from 'lucide-react';
17
- import { Button, Input } from '@asteby/metacore-ui/primitives';
17
+ import { ChevronsUpDown } from 'lucide-react';
18
+ import { Button, Input, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
18
19
  import { DynamicIcon, resolveLucideIconName } from './dynamic-icon';
19
20
  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;
21
+ /** Icons rendered per "page" grows as the user scrolls to the bottom. Keeps
22
+ * the DOM light: the lucide catalog is ~1500 glyphs, so we never mount them all. */
23
+ const PAGE = 60;
22
24
  const ALL_ICON_NAMES = Object.keys(icons);
23
25
  /** True when a stored value looks like an image url/path rather than an icon name. */
24
26
  export function looksLikeImageValue(value) {
@@ -26,18 +28,39 @@ export function looksLikeImageValue(value) {
26
28
  }
27
29
  export function IconPickerField({ field, value, onChange }) {
28
30
  const [mode, setMode] = useState(() => looksLikeImageValue(value) ? 'image' : 'icon');
31
+ const [open, setOpen] = useState(false);
29
32
  const [query, setQuery] = useState('');
33
+ const [limit, setLimit] = useState(PAGE);
30
34
  const selected = mode === 'icon' ? resolveLucideIconName(value) : null;
31
- const results = useMemo(() => {
35
+ // Full match list (names only) filtered by query, not yet capped.
36
+ const matches = useMemo(() => {
32
37
  const q = query.trim().toLowerCase().replace(/[\s_-]/g, '');
33
- const names = q
38
+ return q
34
39
  ? ALL_ICON_NAMES.filter((n) => n.toLowerCase().includes(q))
35
40
  : 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" }))] })] }))] }));
41
+ }, [query]);
42
+ // Visible slice — grows on scroll (see onScroll below).
43
+ const visible = matches.slice(0, limit);
44
+ const pick = (name) => {
45
+ onChange(name);
46
+ setOpen(false);
47
+ };
48
+ 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(Popover, { open: open, onOpenChange: (o) => {
49
+ setOpen(o);
50
+ if (o) {
51
+ // Fresh open: reset the search + paging.
52
+ setQuery('');
53
+ setLimit(PAGE);
54
+ }
55
+ }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { id: field.key, type: "button", variant: "outline", role: "combobox", "aria-expanded": open, className: "w-full justify-between font-normal", children: [_jsx("span", { className: "flex min-w-0 items-center gap-2", children: selected ? (_jsxs(_Fragment, { children: [_jsx(DynamicIcon, { name: selected, className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "truncate", children: selected })] })) : (_jsx("span", { className: "text-muted-foreground", children: "Selecciona un \u00EDcono\u2026" })) }), _jsx(ChevronsUpDown, { className: "h-4 w-4 shrink-0 opacity-50" })] }) }), _jsxs(PopoverContent, { className: "w-[--radix-popover-trigger-width] p-0", align: "start", children: [_jsx("div", { className: "p-2", children: _jsx(Input, { autoFocus: true, value: query, onChange: (e) => {
56
+ setQuery(e.target.value);
57
+ setLimit(PAGE);
58
+ }, placeholder: "Buscar \u00EDcono...", "aria-label": "Buscar \u00EDcono" }) }), _jsxs("div", { className: "max-h-64 overflow-y-auto", role: "listbox", "aria-label": "\u00CDconos", onScroll: (e) => {
59
+ const el = e.currentTarget;
60
+ // Near the bottom → reveal the next page.
61
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 24 &&
62
+ limit < matches.length) {
63
+ setLimit((n) => n + PAGE);
64
+ }
65
+ }, children: [visible.map((name) => (_jsxs("button", { type: "button", role: "option", "aria-selected": name === selected, "aria-label": name, onClick: () => pick(name), className: `flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-sm hover:bg-accent ${name === selected ? 'bg-accent' : ''}`, children: [_jsx(DynamicIcon, { name: name, className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "truncate", children: name })] }, name))), visible.length === 0 && (_jsx("div", { className: "py-6 text-center text-sm text-muted-foreground", children: "Sin resultados" }))] })] })] }))] }));
43
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "28.3.1",
3
+ "version": "28.3.3",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,26 +20,31 @@ afterEach(cleanup)
20
20
  const field: ActionFieldDef = { key: 'icon', label: 'Ícono', type: 'text', widget: 'icon' }
21
21
 
22
22
  describe('IconPickerField', () => {
23
- it('busca "credit", muestra CreditCard y al click emite onChange con el nombre', () => {
23
+ // The icon mode is a combobox: the list lives in a popover that opens on
24
+ // click. Open it, then search, then pick.
25
+ const openPopover = () =>
26
+ fireEvent.click(screen.getByRole('combobox'))
27
+
28
+ it('abre el popover, busca "credit" y al click emite onChange con el nombre', () => {
24
29
  const onChange = vi.fn()
25
30
  render(<IconPickerField field={field} value="" onChange={onChange} />)
31
+ openPopover()
26
32
  fireEvent.change(screen.getByLabelText('Buscar ícono'), { target: { value: 'credit' } })
27
33
  const option = screen.getByRole('option', { name: 'CreditCard' })
28
34
  fireEvent.click(option)
29
35
  expect(onChange).toHaveBeenCalledWith('CreditCard')
30
36
  })
31
37
 
32
- it('resalta el seleccionado y muestra el preview grande', () => {
38
+ it('el trigger muestra el ícono y nombre seleccionado', () => {
33
39
  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')
40
+ // The combobox trigger shows the selected name even before opening.
41
+ expect(screen.getByRole('combobox').textContent).toContain('CreditCard')
38
42
  })
39
43
 
40
- it('limita el grid a 48 resultados', () => {
44
+ it('pagina: arranca con a lo sumo 60 opciones visibles', () => {
41
45
  render(<IconPickerField field={field} value="" onChange={() => {}} />)
42
- expect(screen.getAllByRole('option').length).toBeLessThanOrEqual(48)
46
+ openPopover()
47
+ expect(screen.getAllByRole('option').length).toBeLessThanOrEqual(60)
43
48
  })
44
49
 
45
50
  it('modo imagen delega a UploadField con los mismos props', () => {
@@ -57,6 +57,7 @@ import { FieldCell } from '../field-grid'
57
57
  import { isNilUuid, normalizeNilUuid } from '../nil-uuid'
58
58
  import { normalizeRefFieldsForSubmit } from './normalize-submit'
59
59
  import { DynamicIcon, isLucideIconName } from '../dynamic-icon'
60
+ import { IconPickerField } from '../icon-picker-field'
60
61
  import { humanizeToken } from '../dynamic-columns-helpers'
61
62
  import { formatDateCell } from '../dynamic-columns'
62
63
  import {
@@ -1537,6 +1538,23 @@ export function EditField({ field, value, onChange, record }: {
1537
1538
  )
1538
1539
  }
1539
1540
 
1541
+ // Icon picker: the kernel serves `type: 'icon'` (or an explicit
1542
+ // `widget: 'icon'`) when the manifest column declares `widget: "icon"`.
1543
+ // This modal renderer is SEPARATE from dynamic-form's FieldRenderer, so it
1544
+ // needs its own branch — without it the field fell through to a raw text
1545
+ // input. Same lucide-search-or-upload picker used elsewhere.
1546
+ if (field.type === 'icon' || field.widget === 'icon') {
1547
+ // IconPickerField types `field` as ActionFieldDef; the modal's FieldDef
1548
+ // is structurally compatible for the props it reads (key/label/etc).
1549
+ return (
1550
+ <IconPickerField
1551
+ field={field as unknown as ActionFieldDef}
1552
+ value={value}
1553
+ onChange={onChange}
1554
+ />
1555
+ )
1556
+ }
1557
+
1540
1558
  // Media widgets: the kernel may serve an explicit `widget: 'upload'` (or the
1541
1559
  // `image` type) for a file/photo column.
1542
1560
  if (field.type === 'image' || field.widget === 'upload') {
@@ -13,7 +13,14 @@
13
13
  // means image, anything else means icon.
14
14
  import { useMemo, useState } from 'react'
15
15
  import { icons } from 'lucide-react'
16
- import { Button, Input } from '@asteby/metacore-ui/primitives'
16
+ import { ChevronsUpDown } from 'lucide-react'
17
+ import {
18
+ Button,
19
+ Input,
20
+ Popover,
21
+ PopoverContent,
22
+ PopoverTrigger,
23
+ } from '@asteby/metacore-ui/primitives'
17
24
  import { DynamicIcon, resolveLucideIconName } from './dynamic-icon'
18
25
  import { UploadField } from './upload-field'
19
26
  import type { ActionFieldDef } from './types'
@@ -24,8 +31,9 @@ export interface IconPickerFieldProps {
24
31
  onChange: (v: any) => void
25
32
  }
26
33
 
27
- /** Max glyphs shown in the grid at once keeps the DOM light while searching. */
28
- const MAX_RESULTS = 48
34
+ /** Icons rendered per "page" grows as the user scrolls to the bottom. Keeps
35
+ * the DOM light: the lucide catalog is ~1500 glyphs, so we never mount them all. */
36
+ const PAGE = 60
29
37
 
30
38
  const ALL_ICON_NAMES = Object.keys(icons)
31
39
 
@@ -38,21 +46,27 @@ export function IconPickerField({ field, value, onChange }: IconPickerFieldProps
38
46
  const [mode, setMode] = useState<'icon' | 'image'>(() =>
39
47
  looksLikeImageValue(value) ? 'image' : 'icon',
40
48
  )
49
+ const [open, setOpen] = useState(false)
41
50
  const [query, setQuery] = useState('')
51
+ const [limit, setLimit] = useState(PAGE)
42
52
 
43
53
  const selected = mode === 'icon' ? resolveLucideIconName(value) : null
44
54
 
45
- const results = useMemo(() => {
55
+ // Full match list (names only) filtered by query, not yet capped.
56
+ const matches = useMemo(() => {
46
57
  const q = query.trim().toLowerCase().replace(/[\s_-]/g, '')
47
- const names = q
58
+ return q
48
59
  ? ALL_ICON_NAMES.filter((n) => n.toLowerCase().includes(q))
49
60
  : 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])
61
+ }, [query])
62
+
63
+ // Visible slice grows on scroll (see onScroll below).
64
+ const visible = matches.slice(0, limit)
65
+
66
+ const pick = (name: string) => {
67
+ onChange(name)
68
+ setOpen(false)
69
+ }
56
70
 
57
71
  return (
58
72
  <div className="flex flex-col gap-2">
@@ -81,53 +95,94 @@ export function IconPickerField({ field, value, onChange }: IconPickerFieldProps
81
95
  {mode === 'image' ? (
82
96
  <UploadField field={field} value={value} onChange={onChange} />
83
97
  ) : (
84
- <div className="flex flex-col gap-2">
85
- <div className="flex items-center gap-3">
86
- <Input
98
+ <Popover
99
+ open={open}
100
+ onOpenChange={(o: boolean) => {
101
+ setOpen(o)
102
+ if (o) {
103
+ // Fresh open: reset the search + paging.
104
+ setQuery('')
105
+ setLimit(PAGE)
106
+ }
107
+ }}
108
+ >
109
+ <PopoverTrigger asChild>
110
+ <Button
87
111
  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
112
+ type="button"
113
+ variant="outline"
114
+ role="combobox"
115
+ aria-expanded={open}
116
+ className="w-full justify-between font-normal"
117
+ >
118
+ <span className="flex min-w-0 items-center gap-2">
119
+ {selected ? (
120
+ <>
121
+ <DynamicIcon name={selected} className="h-4 w-4 shrink-0" />
122
+ <span className="truncate">{selected}</span>
123
+ </>
124
+ ) : (
125
+ <span className="text-muted-foreground">Selecciona un ícono…</span>
126
+ )}
127
127
  </span>
128
- )}
129
- </div>
130
- </div>
128
+ <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
129
+ </Button>
130
+ </PopoverTrigger>
131
+ <PopoverContent
132
+ className="w-[--radix-popover-trigger-width] p-0"
133
+ align="start"
134
+ >
135
+ <div className="p-2">
136
+ <Input
137
+ autoFocus
138
+ value={query}
139
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
140
+ setQuery(e.target.value)
141
+ setLimit(PAGE)
142
+ }}
143
+ placeholder="Buscar ícono..."
144
+ aria-label="Buscar ícono"
145
+ />
146
+ </div>
147
+ <div
148
+ className="max-h-64 overflow-y-auto"
149
+ role="listbox"
150
+ aria-label="Íconos"
151
+ onScroll={(e: React.UIEvent<HTMLDivElement>) => {
152
+ const el = e.currentTarget
153
+ // Near the bottom → reveal the next page.
154
+ if (
155
+ el.scrollTop + el.clientHeight >= el.scrollHeight - 24 &&
156
+ limit < matches.length
157
+ ) {
158
+ setLimit((n) => n + PAGE)
159
+ }
160
+ }}
161
+ >
162
+ {visible.map((name) => (
163
+ <button
164
+ key={name}
165
+ type="button"
166
+ role="option"
167
+ aria-selected={name === selected}
168
+ aria-label={name}
169
+ onClick={() => pick(name)}
170
+ className={`flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-sm hover:bg-accent ${
171
+ name === selected ? 'bg-accent' : ''
172
+ }`}
173
+ >
174
+ <DynamicIcon name={name} className="h-4 w-4 shrink-0" />
175
+ <span className="truncate">{name}</span>
176
+ </button>
177
+ ))}
178
+ {visible.length === 0 && (
179
+ <div className="py-6 text-center text-sm text-muted-foreground">
180
+ Sin resultados
181
+ </div>
182
+ )}
183
+ </div>
184
+ </PopoverContent>
185
+ </Popover>
131
186
  )}
132
187
  </div>
133
188
  )