// IconPickerField — the `icon` widget renderer for DynamicForm. Two modes: // // - "Ícono" (default): search box + grid of lucide glyphs filtered by name. // Clicking one stores the PascalCase lucide name ("CreditCard") as the // field value — the same convention OptionDef.icon and image-ish cell // renderers already understand (see dynamic-icon.tsx). // - "Imagen": delegates to the existing UploadField untouched, so the value // is the uploaded file url/path exactly as the `upload` widget stores it. // // The stored value stays a plain string (retrocompatible): a lucide name when // an icon was picked, a url/path when an image was uploaded. When editing, the // initial mode is inferred from the current value — path-like ("/" or ".") // means image, anything else means icon. import { useMemo, useState } from 'react' import { icons } from 'lucide-react' import { ChevronsUpDown } from 'lucide-react' import { Button, Input, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives' import { DynamicIcon, resolveLucideIconName } from './dynamic-icon' import { UploadField } from './upload-field' import type { ActionFieldDef } from './types' export interface IconPickerFieldProps { field: ActionFieldDef value: any onChange: (v: any) => void } /** Icons rendered per "page" — grows as the user scrolls to the bottom. Keeps * the DOM light: the lucide catalog is ~1500 glyphs, so we never mount them all. */ const PAGE = 60 const ALL_ICON_NAMES = Object.keys(icons) /** True when a stored value looks like an image url/path rather than an icon name. */ export function looksLikeImageValue(value: unknown): boolean { return typeof value === 'string' && value !== '' && /[/.]/.test(value) } export function IconPickerField({ field, value, onChange }: IconPickerFieldProps) { const [mode, setMode] = useState<'icon' | 'image'>(() => looksLikeImageValue(value) ? 'image' : 'icon', ) const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const [limit, setLimit] = useState(PAGE) const selected = mode === 'icon' ? resolveLucideIconName(value) : null // Full match list (names only) — filtered by query, not yet capped. const matches = useMemo(() => { const q = query.trim().toLowerCase().replace(/[\s_-]/g, '') return q ? ALL_ICON_NAMES.filter((n) => n.toLowerCase().includes(q)) : ALL_ICON_NAMES }, [query]) // Visible slice — grows on scroll (see onScroll below). const visible = matches.slice(0, limit) const pick = (name: string) => { onChange(name) setOpen(false) } return (
{mode === 'image' ? ( ) : ( { setOpen(o) if (o) { // Fresh open: reset the search + paging. setQuery('') setLimit(PAGE) } }} >
) => { setQuery(e.target.value) setLimit(PAGE) }} placeholder="Buscar ícono..." aria-label="Buscar ícono" />
) => { const el = e.currentTarget // Near the bottom → reveal the next page. if ( el.scrollTop + el.clientHeight >= el.scrollHeight - 24 && limit < matches.length ) { setLimit((n) => n + PAGE) } }} > {visible.map((name) => ( ))} {visible.length === 0 && (
Sin resultados
)}
)}
) }