@asteby/metacore-runtime-react 17.0.4 → 18.0.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - ce9dd72: `DynamicSelectField` (the searchable FK / option picker) now renders each
8
+ option's leading visual: a photo thumbnail (FK relations with an image), else a
9
+ declared icon, else a colored dot for enum/status options that carry a `color`.
10
+ Previously only image thumbnails showed, so enum selects (state, origin, …) read
11
+ as plain text. Plain options with no image/color/icon stay plain.
12
+ - Updated dependencies [8439e9e]
13
+ - @asteby/metacore-ui@2.5.0
14
+
3
15
  ## 17.0.4
4
16
 
5
17
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgD7C,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,+BA2KrF;AAED,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AA+F7C,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,+BA0KrF;AAED,eAAe,kBAAkB,CAAA"}
@@ -25,6 +25,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
25
  import { useEffect, useState } from 'react';
26
26
  import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
27
27
  import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react';
28
+ import { resolveColorCss } from '@asteby/metacore-ui/lib';
29
+ import { DynamicIcon } from './dynamic-icon';
28
30
  import { useOptionsResolver } from './use-options-resolver';
29
31
  import { getFieldRef } from './dynamic-form-schema';
30
32
  /**
@@ -46,6 +48,29 @@ function OptionThumb({ image, size = 20 }) {
46
48
  e.currentTarget.style.visibility = 'hidden';
47
49
  } }));
48
50
  }
51
+ /**
52
+ * Leading visual for an option: a photo thumbnail (FK relations with an image),
53
+ * else a declared icon, else a color dot (enum/status options with a color).
54
+ * Returns null when the option carries none, so plain text options stay plain.
55
+ */
56
+ function OptionLead({ option, size = 20, }) {
57
+ if (!option)
58
+ return null;
59
+ if (option.image)
60
+ return _jsx(OptionThumb, { image: option.image, size: size });
61
+ if (option.icon) {
62
+ return (_jsx("span", { className: "flex shrink-0 items-center justify-center", style: { width: size, height: size, color: option.color ? resolveColorCss(option.color) : undefined }, "aria-hidden": true, children: _jsx(DynamicIcon, { name: option.icon, className: "size-4" }) }));
63
+ }
64
+ if (option.color) {
65
+ return (_jsx("span", { className: "shrink-0 rounded-full", style: { width: Math.round(size * 0.5), height: Math.round(size * 0.5), background: resolveColorCss(option.color) }, "aria-hidden": true }));
66
+ }
67
+ return null;
68
+ }
69
+ /** True when any option (or the selected one) carries a renderable visual. */
70
+ function optionsHaveVisual(options, selected) {
71
+ const has = (o) => !!(o && (o.image || o.color || o.icon));
72
+ return has(selected) || options.some(has);
73
+ }
49
74
  function useDebounced(value, ms) {
50
75
  const [debounced, setDebounced] = useState(value);
51
76
  useEffect(() => {
@@ -87,7 +112,7 @@ export function DynamicSelectField({ field, value, onChange }) {
87
112
  // Only switch the picker into "with thumbnails" mode when the data actually
88
113
  // carries images — a relation whose options have no `image` keeps the plain
89
114
  // text list it had before (no empty placeholder column).
90
- const hasImages = !!selectedOption?.image || options.some((o) => !!o.image);
115
+ const hasVisual = optionsHaveVisual(options, selectedOption);
91
116
  const handlePick = (opt) => {
92
117
  setPicked(opt);
93
118
  onChange(String(opt.id));
@@ -119,12 +144,12 @@ export function DynamicSelectField({ field, value, onChange }) {
119
144
  // to the cell. Without min-w-0 the combobox+button row sizes to its content
120
145
  // (the long empty-state placeholder) and overflows the column, pushing the
121
146
  // "+" off-screen — it only "fit" once a short value was selected.
122
- return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [hasImages && value ? (_jsx(OptionThumb, { image: selectedOption?.image, size: 20 })) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
147
+ return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [hasVisual && value ? (_jsx(OptionLead, { option: selectedOption, size: 20 })) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
123
148
  // Match the trigger width without an arbitrary Tailwind class
124
149
  // (those don't always survive a consuming app's Tailwind scan).
125
150
  style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: field.placeholder || 'Buscar…', value: search, onValueChange: setSearch }), _jsxs(CommandList, { children: [loading && (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm", children: [_jsx(Loader2, { className: "size-4 animate-spin" }), "Buscando\u2026"] })), !loading && options.length === 0 && (_jsx(CommandEmpty, { children: debounced ? 'Sin resultados' : 'Escribí para buscar…' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
126
151
  const isSel = String(opt.id) === String(value);
127
- return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0') }), hasImages && (_jsx(OptionThumb, { image: opt.image, size: 24 })), _jsxs("div", { className: "ml-2 flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] })] }, String(opt.id)));
152
+ return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0') }), hasVisual && (_jsx(OptionLead, { option: opt, size: 24 })), _jsxs("div", { className: "ml-2 flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] })] }, String(opt.id)));
128
153
  }) }))] })] }) })] }), fieldRef && (_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: openCreate, title: `Crear ${field.label ?? fieldRef}`, "aria-label": `Crear ${field.label ?? fieldRef}`, children: _jsx(Plus, { className: "size-4" }) }))] }));
129
154
  }
130
155
  export default DynamicSelectField;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "17.0.4",
3
+ "version": "18.0.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "date-fns": ">=3",
35
35
  "react-day-picker": ">=8",
36
36
  "@asteby/metacore-sdk": "^3.2.0",
37
- "@asteby/metacore-ui": "^2.4.2"
37
+ "@asteby/metacore-ui": "^2.5.0"
38
38
  },
39
39
  "peerDependenciesMeta": {
40
40
  "@tanstack/react-router": {
@@ -62,7 +62,7 @@
62
62
  "vitest": "^4.0.0",
63
63
  "zustand": "^5.0.0",
64
64
  "@asteby/metacore-sdk": "3.2.0",
65
- "@asteby/metacore-ui": "2.4.2"
65
+ "@asteby/metacore-ui": "2.5.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc -p tsconfig.json",
@@ -35,6 +35,8 @@ import {
35
35
  PopoverTrigger,
36
36
  } from '@asteby/metacore-ui/primitives'
37
37
  import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react'
38
+ import { resolveColorCss } from '@asteby/metacore-ui/lib'
39
+ import { DynamicIcon } from './dynamic-icon'
38
40
  import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
39
41
  import { getFieldRef } from './dynamic-form-schema'
40
42
  import type { ActionFieldDef } from './types'
@@ -76,6 +78,53 @@ function OptionThumb({ image, size = 20 }: { image?: string | null; size?: numbe
76
78
  )
77
79
  }
78
80
 
81
+ /**
82
+ * Leading visual for an option: a photo thumbnail (FK relations with an image),
83
+ * else a declared icon, else a color dot (enum/status options with a color).
84
+ * Returns null when the option carries none, so plain text options stay plain.
85
+ */
86
+ function OptionLead({
87
+ option,
88
+ size = 20,
89
+ }: {
90
+ option?: Pick<ResolvedOption, 'image' | 'color' | 'icon'> | null
91
+ size?: number
92
+ }) {
93
+ if (!option) return null
94
+ if (option.image) return <OptionThumb image={option.image} size={size} />
95
+ if (option.icon) {
96
+ return (
97
+ <span
98
+ className="flex shrink-0 items-center justify-center"
99
+ style={{ width: size, height: size, color: option.color ? resolveColorCss(option.color) : undefined }}
100
+ aria-hidden
101
+ >
102
+ <DynamicIcon name={option.icon} className="size-4" />
103
+ </span>
104
+ )
105
+ }
106
+ if (option.color) {
107
+ return (
108
+ <span
109
+ className="shrink-0 rounded-full"
110
+ style={{ width: Math.round(size * 0.5), height: Math.round(size * 0.5), background: resolveColorCss(option.color) }}
111
+ aria-hidden
112
+ />
113
+ )
114
+ }
115
+ return null
116
+ }
117
+
118
+ /** True when any option (or the selected one) carries a renderable visual. */
119
+ function optionsHaveVisual(
120
+ options: ReadonlyArray<Pick<ResolvedOption, 'image' | 'color' | 'icon'>>,
121
+ selected?: Pick<ResolvedOption, 'image' | 'color' | 'icon'> | null,
122
+ ): boolean {
123
+ const has = (o?: Pick<ResolvedOption, 'image' | 'color' | 'icon'> | null) =>
124
+ !!(o && (o.image || o.color || o.icon))
125
+ return has(selected) || options.some(has)
126
+ }
127
+
79
128
  function useDebounced<T>(value: T, ms: number): T {
80
129
  const [debounced, setDebounced] = useState(value)
81
130
  useEffect(() => {
@@ -130,8 +179,7 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
130
179
  // Only switch the picker into "with thumbnails" mode when the data actually
131
180
  // carries images — a relation whose options have no `image` keeps the plain
132
181
  // text list it had before (no empty placeholder column).
133
- const hasImages =
134
- !!selectedOption?.image || options.some((o) => !!o.image)
182
+ const hasVisual = optionsHaveVisual(options, selectedOption)
135
183
 
136
184
  const handlePick = (opt: ResolvedOption) => {
137
185
  setPicked(opt)
@@ -181,8 +229,8 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
181
229
  data-empty={!value}
182
230
  >
183
231
  <span className="flex min-w-0 flex-1 items-center gap-2 text-left">
184
- {hasImages && value ? (
185
- <OptionThumb image={selectedOption?.image} size={20} />
232
+ {hasVisual && value ? (
233
+ <OptionLead option={selectedOption} size={20} />
186
234
  ) : null}
187
235
  <span className={'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground')}>
188
236
  {selectedLabel || field.placeholder || 'Buscar…'}
@@ -227,8 +275,8 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
227
275
  onSelect={() => handlePick(opt)}
228
276
  >
229
277
  <Check className={'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0')} />
230
- {hasImages && (
231
- <OptionThumb image={opt.image} size={24} />
278
+ {hasVisual && (
279
+ <OptionLead option={opt} size={24} />
232
280
  )}
233
281
  <div className="ml-2 flex min-w-0 flex-col">
234
282
  <span className="truncate">{opt.label}</span>