@asteby/metacore-runtime-react 18.18.0 → 18.20.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,38 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6ec7baf: CollectionCell is now locale-aware: jsonb/array popover headers and the item
8
+ count noun render in the org's language. Resolution per key: host `t(rawKey)`
9
+ override → built-in es/en dictionary of common data/commerce keys (product_id,
10
+ quantity, price, total, name, sku, …) → snake→Title prettify fallback. Count
11
+ noun localizes (es: ítem/ítems). Locale + translator are threaded from the
12
+ dynamic columns factory; defaults to English when absent.
13
+
14
+ ## 18.19.0
15
+
16
+ ### Minor Changes
17
+
18
+ - de0b4bb: Add a generic `CollectionCell` renderer for jsonb / array / object table-cell values.
19
+
20
+ Previously the `default:` branch of `defaultGetDynamicColumns` rendered such
21
+ values as raw `JSON.stringify(value)`, which was unreadable. Every jsonb column
22
+ now renders a compact, brand-neutral, dark-mode-friendly cell with no per-addon
23
+ config:
24
+ - **Array of objects** (e.g. line items): a count Badge (`2 ítems`) that opens a
25
+ Popover mini-table — columns are the prettified union of row keys, cells go
26
+ through a shared `formatScalar` (uuid/long strings truncated, booleans as
27
+ ✓/✗, nested shapes summarized).
28
+ - **Array of scalars**: first few joined inline with a `+N` overflow, full list
29
+ in the popover.
30
+ - **Plain object**: first few `key: value` pairs inline, all pairs in the popover.
31
+ - **null / empty**: muted `-`.
32
+ - JSON-string values are defensively parsed; unparseable strings are truncated.
33
+
34
+ Exports `CollectionCell`, `formatScalar`, `prettifyKey`, and `CollectionCellProps`.
35
+
3
36
  ## 18.18.0
4
37
 
5
38
  ### Minor Changes
@@ -0,0 +1,39 @@
1
+ import * as React from 'react';
2
+ /** Host i18n translator (react-i18next `t`), as threaded into the columns factory. */
3
+ export type Translate = (key: string, options?: any) => string;
4
+ /**
5
+ * Localized key label for a popover column header. Resolution order:
6
+ * (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
7
+ * (b) the built-in generic es/en data/commerce dictionary;
8
+ * (c) snake_case → Title Case prettify (`product_id` → "Product ID").
9
+ * `locale` defaults to 'en' when absent.
10
+ */
11
+ export declare function prettifyKey(key: string, locale?: string, t?: Translate): string;
12
+ /**
13
+ * Localized, pluralized count noun. Prefers the host i18n `t` (react-i18next
14
+ * count plural via `defaultValue`); otherwise the built-in es/en noun map.
15
+ */
16
+ export declare function countLabel(count: number, locale?: string, t?: Translate): string;
17
+ /**
18
+ * Render a single scalar (or near-scalar) value for compact display.
19
+ * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
20
+ * - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
21
+ * - nested object → "{…}", nested array → "[N]"
22
+ * - null / undefined / "" → "-"
23
+ */
24
+ export declare function formatScalar(value: unknown): string;
25
+ export interface CollectionCellProps {
26
+ value: unknown;
27
+ /** Max items previewed inline for scalar arrays. */
28
+ maxInline?: number;
29
+ /** Org/UI language tag (e.g. `es`, `en-US`). Defaults to `'en'`. */
30
+ locale?: string;
31
+ /** Host i18n translator; takes precedence over the built-in dictionary. */
32
+ t?: Translate;
33
+ }
34
+ /**
35
+ * Generic renderer for jsonb / array / object cell values. Brand-neutral,
36
+ * compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
37
+ */
38
+ export declare function CollectionCell({ value, maxInline, locale, t, }: CollectionCellProps): React.JSX.Element;
39
+ //# sourceMappingURL=collection-cell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoB9B,sFAAsF;AACtF,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,CAAA;AAiE9D;;;;;;GAMG;AACH,wBAAgB,WAAW,CACvB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,EACf,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAWR;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACtB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,EACf,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAcR;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAYnD;AAyJD,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,OAAO,CAAA;IACd,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2EAA2E;IAC3E,CAAC,CAAC,EAAE,SAAS,CAAA;CAChB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAa,EACb,MAAM,EACN,CAAC,GACJ,EAAE,mBAAmB,qBAgFrB"}
@@ -0,0 +1,236 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { List } from 'lucide-react';
3
+ import { Badge, Popover, PopoverContent, PopoverTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@asteby/metacore-ui';
4
+ import { humanizeToken } from './dynamic-columns-helpers';
5
+ const UUID_LIKE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+ /** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
7
+ function baseLang(locale) {
8
+ return (locale || 'en').toLowerCase().split('-')[0];
9
+ }
10
+ // Built-in generic data/commerce vocabulary. Localizes the common jsonb keys
11
+ // (line-item rows, config blobs) to the org language out of the box. This is
12
+ // intentionally GENERIC — no domain-specific narrative — and a host i18n bundle
13
+ // can override any key via the `t` resolver (which takes precedence). Keys not
14
+ // found here fall back to snake→Title prettify, so unknown shapes still read.
15
+ const KEY_DICTIONARY = {
16
+ es: {
17
+ product_id: 'Producto',
18
+ product: 'Producto',
19
+ quantity: 'Cantidad',
20
+ qty: 'Cantidad',
21
+ unit_cost: 'Costo unitario',
22
+ cost: 'Costo',
23
+ price: 'Precio',
24
+ total: 'Total',
25
+ subtotal: 'Subtotal',
26
+ amount: 'Importe',
27
+ name: 'Nombre',
28
+ sku: 'SKU',
29
+ code: 'Código',
30
+ date: 'Fecha',
31
+ notes: 'Notas',
32
+ reason: 'Motivo',
33
+ delta: 'Variación',
34
+ warehouse: 'Almacén',
35
+ description: 'Descripción',
36
+ id: 'ID',
37
+ },
38
+ en: {
39
+ product_id: 'Product',
40
+ product: 'Product',
41
+ quantity: 'Quantity',
42
+ qty: 'Quantity',
43
+ unit_cost: 'Unit Cost',
44
+ cost: 'Cost',
45
+ price: 'Price',
46
+ total: 'Total',
47
+ subtotal: 'Subtotal',
48
+ amount: 'Amount',
49
+ name: 'Name',
50
+ sku: 'SKU',
51
+ code: 'Code',
52
+ date: 'Date',
53
+ notes: 'Notes',
54
+ reason: 'Reason',
55
+ delta: 'Delta',
56
+ warehouse: 'Warehouse',
57
+ description: 'Description',
58
+ id: 'ID',
59
+ },
60
+ };
61
+ /** Localized count noun for the array-of-objects badge (`1 ítem` / `2 ítems`). */
62
+ const ITEM_NOUN = {
63
+ es: { one: 'ítem', other: 'ítems' },
64
+ en: { one: 'item', other: 'items' },
65
+ };
66
+ /**
67
+ * Localized key label for a popover column header. Resolution order:
68
+ * (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
69
+ * (b) the built-in generic es/en data/commerce dictionary;
70
+ * (c) snake_case → Title Case prettify (`product_id` → "Product ID").
71
+ * `locale` defaults to 'en' when absent.
72
+ */
73
+ export function prettifyKey(key, locale, t) {
74
+ if (t) {
75
+ const translated = t(key);
76
+ if (translated && translated !== key)
77
+ return translated;
78
+ }
79
+ const lang = baseLang(locale);
80
+ const dict = KEY_DICTIONARY[lang] ?? KEY_DICTIONARY.en;
81
+ const hit = dict[key.toLowerCase()];
82
+ if (hit)
83
+ return hit;
84
+ const pretty = humanizeToken(key);
85
+ return pretty || key;
86
+ }
87
+ /**
88
+ * Localized, pluralized count noun. Prefers the host i18n `t` (react-i18next
89
+ * count plural via `defaultValue`); otherwise the built-in es/en noun map.
90
+ */
91
+ export function countLabel(count, locale, t) {
92
+ const lang = baseLang(locale);
93
+ const noun = ITEM_NOUN[lang] ?? ITEM_NOUN.en;
94
+ const fallback = `${count} ${count === 1 ? noun.one : noun.other}`;
95
+ if (t) {
96
+ const translated = t('runtime.collectionCell.itemCount', {
97
+ count,
98
+ defaultValue: fallback,
99
+ });
100
+ if (translated && translated !== 'runtime.collectionCell.itemCount') {
101
+ return translated;
102
+ }
103
+ }
104
+ return fallback;
105
+ }
106
+ /**
107
+ * Render a single scalar (or near-scalar) value for compact display.
108
+ * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
109
+ * - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
110
+ * - nested object → "{…}", nested array → "[N]"
111
+ * - null / undefined / "" → "-"
112
+ */
113
+ export function formatScalar(value) {
114
+ if (value === null || value === undefined)
115
+ return '-';
116
+ if (typeof value === 'boolean')
117
+ return value ? '✓' : '✗';
118
+ if (typeof value === 'number')
119
+ return String(value);
120
+ if (Array.isArray(value))
121
+ return `[${value.length}]`;
122
+ if (typeof value === 'object')
123
+ return '{…}';
124
+ const str = String(value);
125
+ if (str === '')
126
+ return '-';
127
+ if (UUID_LIKE_RE.test(str) || str.length >= 32) {
128
+ return `${str.slice(0, 8)}…`;
129
+ }
130
+ return str;
131
+ }
132
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
133
+ /**
134
+ * Defensively coerce a raw cell value into something renderable. Strings that
135
+ * look like JSON (`[`/`{` start) are parsed; everything else is passed through.
136
+ */
137
+ function parseValue(value) {
138
+ if (typeof value !== 'string')
139
+ return value;
140
+ const trimmed = value.trim();
141
+ if ((trimmed.startsWith('[') && trimmed.endsWith(']')) ||
142
+ (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
143
+ try {
144
+ return JSON.parse(trimmed);
145
+ }
146
+ catch {
147
+ return value;
148
+ }
149
+ }
150
+ return value;
151
+ }
152
+ /** Stable union of keys across an array of row objects, first-seen order. */
153
+ function unionKeys(rows) {
154
+ const seen = [];
155
+ const set = new Set();
156
+ for (const row of rows) {
157
+ for (const key of Object.keys(row)) {
158
+ if (!set.has(key)) {
159
+ set.add(key);
160
+ seen.push(key);
161
+ }
162
+ }
163
+ }
164
+ return seen;
165
+ }
166
+ const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0';
167
+ function MiniTable({ rows, locale, t, }) {
168
+ const keys = unionKeys(rows);
169
+ if (keys.length === 0) {
170
+ return _jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "-" });
171
+ }
172
+ return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: keys.map((key) => (_jsx(TableHead, { className: "text-xs whitespace-nowrap", children: prettifyKey(key, locale, t) }, key))) }) }), _jsx(TableBody, { children: rows.map((row, i) => (_jsx(TableRow, { children: keys.map((key) => (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: formatScalar(row[key]) }, key))) }, i))) })] }));
173
+ }
174
+ function ScalarList({ values }) {
175
+ return (_jsx("ul", { className: "p-3 space-y-1", children: values.map((v, i) => (_jsx("li", { className: "text-xs text-foreground", children: formatScalar(v) }, i))) }));
176
+ }
177
+ function PairList({ entries, locale, t, }) {
178
+ return (_jsx("ul", { className: "p-3 space-y-1", children: entries.map(([key, v]) => (_jsxs("li", { className: "text-xs", children: [_jsxs("span", { className: "text-muted-foreground", children: [prettifyKey(key, locale, t), ":"] }), ' ', _jsx("span", { className: "text-foreground", children: formatScalar(v) })] }, key))) }));
179
+ }
180
+ /** Compact badge trigger that opens a popover panel. */
181
+ function PopoverShell({ label, title, children, icon = true, }) {
182
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Badge, { variant: "secondary", className: "cursor-pointer gap-1 font-normal", title: title, children: [icon ? _jsx(List, { className: "h-3 w-3" }) : null, label] }) }), _jsx(PopoverContent, { align: "start", className: cn(PANEL_CLASS), children: children })] }));
183
+ }
184
+ /**
185
+ * Generic renderer for jsonb / array / object cell values. Brand-neutral,
186
+ * compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
187
+ */
188
+ export function CollectionCell({ value, maxInline = 3, locale, t, }) {
189
+ const parsed = parseValue(value);
190
+ // Empty-ish → muted dash.
191
+ if (parsed === null ||
192
+ parsed === undefined ||
193
+ parsed === '' ||
194
+ (Array.isArray(parsed) && parsed.length === 0) ||
195
+ (isPlainObject(parsed) && Object.keys(parsed).length === 0)) {
196
+ return _jsx("span", { className: "text-muted-foreground text-xs", children: "-" });
197
+ }
198
+ // Non-collection scalar fell through here (e.g. unparseable string): truncate.
199
+ if (!Array.isArray(parsed) && !isPlainObject(parsed)) {
200
+ const str = String(parsed);
201
+ return (_jsx("span", { className: "text-muted-foreground text-xs truncate block max-w-[300px]", title: str, children: str.length > 80 ? `${str.slice(0, 80)}…` : str }));
202
+ }
203
+ // ARRAY ------------------------------------------------------------------
204
+ if (Array.isArray(parsed)) {
205
+ const allObjects = parsed.every((item) => isPlainObject(item));
206
+ if (allObjects) {
207
+ const rows = parsed;
208
+ const count = rows.length;
209
+ const label = countLabel(count, locale, t);
210
+ const title = rows
211
+ .map((row) => Object.entries(row)
212
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
213
+ .join(', '))
214
+ .join(' | ');
215
+ return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows, locale: locale, t: t }) }));
216
+ }
217
+ // Array of scalars (or mixed): preview first N joined, "+N" overflow.
218
+ const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ');
219
+ const overflow = parsed.length - maxInline;
220
+ const label = overflow > 0 ? `${preview} +${overflow}` : preview;
221
+ const title = parsed.map(formatScalar).join(', ');
222
+ return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(ScalarList, { values: parsed }) }));
223
+ }
224
+ // PLAIN OBJECT -----------------------------------------------------------
225
+ const entries = Object.entries(parsed);
226
+ const inline = entries
227
+ .slice(0, maxInline)
228
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
229
+ .join(', ');
230
+ const overflow = entries.length - maxInline;
231
+ const label = overflow > 0 ? `${inline} +${overflow}` : inline;
232
+ const title = entries
233
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
234
+ .join(', ');
235
+ return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(PairList, { entries: entries, locale: locale, t: t }) }));
236
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAgC9C,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;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;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;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA2nBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAiC9C,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;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;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;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA6nBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -24,6 +24,7 @@ import { Progress } from './dialogs/_primitives';
24
24
  import { humanizeToken } from './dynamic-columns-helpers';
25
25
  import { OptionsContext } from './options-context';
26
26
  import { DynamicIcon, isLucideIconName } from './dynamic-icon';
27
+ import { CollectionCell } from './collection-cell';
27
28
  import { isNilUuid, normalizeNilUuid } from './nil-uuid';
28
29
  import { isColumnVisibleInTable } from './column-visibility';
29
30
  const defaultGetImageUrl = (path) => path;
@@ -735,7 +736,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
735
736
  }
736
737
  default: {
737
738
  if (typeof value === 'object' && value !== null) {
738
- return (_jsx("span", { className: "text-muted-foreground text-xs", children: JSON.stringify(value) }));
739
+ return (_jsx(CollectionCell, { value: value, locale: currentLanguage, t: t }));
739
740
  }
740
741
  if (col.key === 'description' ||
741
742
  col.key === 'features' ||
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ export * from './dynamic-icon';
22
22
  export type { ColumnFilterConfig, FilterOption as DynamicColumnFilterOption, GetDynamicColumns, DynamicIconComponent, } from './dynamic-columns-shim';
23
23
  export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, type DynamicColumnsHelpers, } from './dynamic-columns';
24
24
  export { humanizeToken } from './dynamic-columns-helpers';
25
+ export { CollectionCell, formatScalar, prettifyKey, countLabel, type CollectionCellProps, type Translate as CollectionCellTranslate, } from './collection-cell';
25
26
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
26
27
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
27
28
  export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederation
26
26
  export * from './dynamic-icon';
27
27
  export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, } from './dynamic-columns';
28
28
  export { humanizeToken } from './dynamic-columns-helpers';
29
+ export { CollectionCell, formatScalar, prettifyKey, countLabel, } from './collection-cell';
29
30
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
30
31
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
31
32
  export { CreateRecordDialog } from './dialogs/create-record-dialog';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.18.0",
3
+ "version": "18.20.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,178 @@
1
+ // @vitest-environment happy-dom
2
+ //
3
+ // CollectionCell contract: the generic jsonb / array / object cell renderer.
4
+ // - array of objects → count badge + popover mini-table (title carries rows)
5
+ // - array of scalars → inline preview + "+N" overflow
6
+ // - plain object → inline key: value pairs
7
+ // - null / empty → "-"
8
+ // - JSON-string value is defensively parsed
9
+ // - locale-aware: count noun + header keys render in the org language; the
10
+ // host `t` overrides; unknown keys fall back to snake→Title prettify.
11
+ import { afterEach, describe, expect, it } from 'vitest'
12
+ import { cleanup, render, screen } from '@testing-library/react'
13
+
14
+ // Sin `globals: true` en vitest, RTL no auto-limpia entre tests.
15
+ afterEach(cleanup)
16
+
17
+ import {
18
+ CollectionCell,
19
+ countLabel,
20
+ formatScalar,
21
+ prettifyKey,
22
+ } from '../collection-cell'
23
+
24
+ describe('formatScalar', () => {
25
+ it('truncates uuid-like / long strings to first 8 chars + ellipsis', () => {
26
+ expect(formatScalar('550e8400-e29b-41d4-a716-446655440000')).toBe(
27
+ '550e8400…'
28
+ )
29
+ expect(formatScalar('x'.repeat(40))).toBe(`${'x'.repeat(8)}…`)
30
+ })
31
+
32
+ it('passes numbers through and renders booleans as check/cross', () => {
33
+ expect(formatScalar(42)).toBe('42')
34
+ expect(formatScalar(true)).toBe('✓')
35
+ expect(formatScalar(false)).toBe('✗')
36
+ })
37
+
38
+ it('summarizes nested object / array and empties as dash', () => {
39
+ expect(formatScalar({ a: 1 })).toBe('{…}')
40
+ expect(formatScalar([1, 2, 3])).toBe('[3]')
41
+ expect(formatScalar(null)).toBe('-')
42
+ expect(formatScalar('')).toBe('-')
43
+ })
44
+ })
45
+
46
+ describe('prettifyKey', () => {
47
+ it('localizes common data/commerce keys to Spanish', () => {
48
+ expect(prettifyKey('product_id', 'es')).toBe('Producto')
49
+ expect(prettifyKey('quantity', 'es')).toBe('Cantidad')
50
+ })
51
+
52
+ it('localizes common keys to English (default locale)', () => {
53
+ expect(prettifyKey('product_id')).toBe('Product')
54
+ expect(prettifyKey('product_id', 'en')).toBe('Product')
55
+ expect(prettifyKey('quantity', 'en')).toBe('Quantity')
56
+ })
57
+
58
+ it('accepts a regional tag and normalizes to base language', () => {
59
+ expect(prettifyKey('quantity', 'es-MX')).toBe('Cantidad')
60
+ })
61
+
62
+ it('prefers a host `t` translation over the built-in dictionary', () => {
63
+ const t = (k: string) => (k === 'quantity' ? 'Piezas' : k)
64
+ expect(prettifyKey('quantity', 'es', t)).toBe('Piezas')
65
+ })
66
+
67
+ it('falls back to snake→Title prettify for unknown keys', () => {
68
+ expect(prettifyKey('shelf_position', 'es')).toBe('Shelf Position')
69
+ expect(prettifyKey('warehouse_bin', 'en')).toBe('Warehouse Bin')
70
+ })
71
+ })
72
+
73
+ describe('countLabel', () => {
74
+ it('pluralizes the count noun per locale', () => {
75
+ expect(countLabel(1, 'es')).toBe('1 ítem')
76
+ expect(countLabel(2, 'es')).toBe('2 ítems')
77
+ expect(countLabel(1, 'en')).toBe('1 item')
78
+ expect(countLabel(3, 'en')).toBe('3 items')
79
+ expect(countLabel(1)).toBe('1 item') // default → en
80
+ })
81
+
82
+ it('prefers a host `t` count plural', () => {
83
+ const t = (_k: string, o?: any) =>
84
+ o?.count === 1 ? '1 renglón' : `${o?.count} renglones`
85
+ expect(countLabel(2, 'es', t)).toBe('2 renglones')
86
+ })
87
+ })
88
+
89
+ describe('CollectionCell', () => {
90
+ it('renders an English count badge by default for an array of objects', () => {
91
+ render(
92
+ <CollectionCell
93
+ value={[
94
+ { product_id: 'abc', quantity: 2 },
95
+ { product_id: 'def', quantity: 5 },
96
+ ]}
97
+ />
98
+ )
99
+ // Default locale → English plural.
100
+ expect(screen.getByText('2 items')).toBeTruthy()
101
+ // The trigger's title carries the localized rows for the no-JS fallback.
102
+ const badge = screen.getByText('2 items').closest('[title]')
103
+ expect(badge).toBeTruthy()
104
+ const title = badge!.getAttribute('title') ?? ''
105
+ expect(title).toContain('Quantity: 2')
106
+ expect(title).toContain('Quantity: 5')
107
+ expect(title).toContain('Product:') // product_id → "Product" (en)
108
+ })
109
+
110
+ it('renders Spanish count + headers when locale is es', () => {
111
+ render(
112
+ <CollectionCell
113
+ locale="es"
114
+ value={[
115
+ { product_id: 'abc', quantity: 2 },
116
+ { product_id: 'def', quantity: 5 },
117
+ ]}
118
+ />
119
+ )
120
+ expect(screen.getByText('2 ítems')).toBeTruthy()
121
+ const title =
122
+ screen.getByText('2 ítems').closest('[title]')!.getAttribute('title') ??
123
+ ''
124
+ expect(title).toContain('Producto:')
125
+ expect(title).toContain('Cantidad: 2')
126
+ })
127
+
128
+ it('renders the singular Spanish noun for a single-item array', () => {
129
+ render(<CollectionCell locale="es" value={[{ sku: 'A1' }]} />)
130
+ expect(screen.getByText('1 ítem')).toBeTruthy()
131
+ })
132
+
133
+ it('renders the singular English noun for a single-item array', () => {
134
+ render(<CollectionCell value={[{ sku: 'A1' }]} />)
135
+ expect(screen.getByText('1 item')).toBeTruthy()
136
+ })
137
+
138
+ it('previews the first scalars with overflow for a scalar array', () => {
139
+ render(<CollectionCell value={['a', 'b', 'c', 'd', 'e']} />)
140
+ expect(screen.getByText('a, b, c +2')).toBeTruthy()
141
+ })
142
+
143
+ it('renders inline key: value pairs (localized) for a plain object', () => {
144
+ render(
145
+ <CollectionCell locale="es" value={{ price: 10, quantity: 20 }} />
146
+ )
147
+ expect(screen.getByText(/Precio: 10, Cantidad: 20/)).toBeTruthy()
148
+ })
149
+
150
+ it('renders a dash for null / empty values', () => {
151
+ const { container } = render(<CollectionCell value={null} />)
152
+ expect(container.textContent).toBe('-')
153
+ })
154
+
155
+ it('renders a dash for an empty array', () => {
156
+ const { container } = render(<CollectionCell value={[]} />)
157
+ expect(container.textContent).toBe('-')
158
+ })
159
+
160
+ it('parses a JSON-string value into a collection', () => {
161
+ render(
162
+ <CollectionCell
163
+ locale="es"
164
+ value={'[{"product_id":"abc","quantity":1}]'}
165
+ />
166
+ )
167
+ expect(screen.getByText('1 ítem')).toBeTruthy()
168
+ const badge = screen.getByText('1 ítem').closest('[title]')
169
+ expect(badge!.getAttribute('title')).toContain('Cantidad: 1')
170
+ })
171
+
172
+ it('truncates an unparseable string instead of crashing', () => {
173
+ const { container } = render(
174
+ <CollectionCell value={'{not valid json'} />
175
+ )
176
+ expect(container.textContent).toContain('{not valid json')
177
+ })
178
+ })
@@ -0,0 +1,413 @@
1
+ // Generic, brand-neutral table-cell renderer for jsonb / array / object column
2
+ // values. Kernel-derived dynamic tables surface raw jsonb columns (line items,
3
+ // nested config blobs, scalar arrays) with no per-column metadata; without this
4
+ // they rendered as raw `JSON.stringify(value)` which is unreadable. This renders
5
+ // a compact trigger (count badge / inline pairs) plus a Popover with a clean
6
+ // mini-table — no per-addon config required, safe on any shape.
7
+
8
+ import * as React from 'react'
9
+ import { List } from 'lucide-react'
10
+ import {
11
+ Badge,
12
+ Popover,
13
+ PopoverContent,
14
+ PopoverTrigger,
15
+ Table,
16
+ TableBody,
17
+ TableCell,
18
+ TableHead,
19
+ TableHeader,
20
+ TableRow,
21
+ cn,
22
+ } from '@asteby/metacore-ui'
23
+ import { humanizeToken } from './dynamic-columns-helpers'
24
+
25
+ const UUID_LIKE_RE =
26
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
27
+
28
+ /** Host i18n translator (react-i18next `t`), as threaded into the columns factory. */
29
+ export type Translate = (key: string, options?: any) => string
30
+
31
+ /** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
32
+ function baseLang(locale?: string): string {
33
+ return (locale || 'en').toLowerCase().split('-')[0]
34
+ }
35
+
36
+ // Built-in generic data/commerce vocabulary. Localizes the common jsonb keys
37
+ // (line-item rows, config blobs) to the org language out of the box. This is
38
+ // intentionally GENERIC — no domain-specific narrative — and a host i18n bundle
39
+ // can override any key via the `t` resolver (which takes precedence). Keys not
40
+ // found here fall back to snake→Title prettify, so unknown shapes still read.
41
+ const KEY_DICTIONARY: Record<string, Record<string, string>> = {
42
+ es: {
43
+ product_id: 'Producto',
44
+ product: 'Producto',
45
+ quantity: 'Cantidad',
46
+ qty: 'Cantidad',
47
+ unit_cost: 'Costo unitario',
48
+ cost: 'Costo',
49
+ price: 'Precio',
50
+ total: 'Total',
51
+ subtotal: 'Subtotal',
52
+ amount: 'Importe',
53
+ name: 'Nombre',
54
+ sku: 'SKU',
55
+ code: 'Código',
56
+ date: 'Fecha',
57
+ notes: 'Notas',
58
+ reason: 'Motivo',
59
+ delta: 'Variación',
60
+ warehouse: 'Almacén',
61
+ description: 'Descripción',
62
+ id: 'ID',
63
+ },
64
+ en: {
65
+ product_id: 'Product',
66
+ product: 'Product',
67
+ quantity: 'Quantity',
68
+ qty: 'Quantity',
69
+ unit_cost: 'Unit Cost',
70
+ cost: 'Cost',
71
+ price: 'Price',
72
+ total: 'Total',
73
+ subtotal: 'Subtotal',
74
+ amount: 'Amount',
75
+ name: 'Name',
76
+ sku: 'SKU',
77
+ code: 'Code',
78
+ date: 'Date',
79
+ notes: 'Notes',
80
+ reason: 'Reason',
81
+ delta: 'Delta',
82
+ warehouse: 'Warehouse',
83
+ description: 'Description',
84
+ id: 'ID',
85
+ },
86
+ }
87
+
88
+ /** Localized count noun for the array-of-objects badge (`1 ítem` / `2 ítems`). */
89
+ const ITEM_NOUN: Record<string, { one: string; other: string }> = {
90
+ es: { one: 'ítem', other: 'ítems' },
91
+ en: { one: 'item', other: 'items' },
92
+ }
93
+
94
+ /**
95
+ * Localized key label for a popover column header. Resolution order:
96
+ * (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
97
+ * (b) the built-in generic es/en data/commerce dictionary;
98
+ * (c) snake_case → Title Case prettify (`product_id` → "Product ID").
99
+ * `locale` defaults to 'en' when absent.
100
+ */
101
+ export function prettifyKey(
102
+ key: string,
103
+ locale?: string,
104
+ t?: Translate,
105
+ ): string {
106
+ if (t) {
107
+ const translated = t(key)
108
+ if (translated && translated !== key) return translated
109
+ }
110
+ const lang = baseLang(locale)
111
+ const dict = KEY_DICTIONARY[lang] ?? KEY_DICTIONARY.en
112
+ const hit = dict[key.toLowerCase()]
113
+ if (hit) return hit
114
+ const pretty = humanizeToken(key)
115
+ return pretty || key
116
+ }
117
+
118
+ /**
119
+ * Localized, pluralized count noun. Prefers the host i18n `t` (react-i18next
120
+ * count plural via `defaultValue`); otherwise the built-in es/en noun map.
121
+ */
122
+ export function countLabel(
123
+ count: number,
124
+ locale?: string,
125
+ t?: Translate,
126
+ ): string {
127
+ const lang = baseLang(locale)
128
+ const noun = ITEM_NOUN[lang] ?? ITEM_NOUN.en
129
+ const fallback = `${count} ${count === 1 ? noun.one : noun.other}`
130
+ if (t) {
131
+ const translated = t('runtime.collectionCell.itemCount', {
132
+ count,
133
+ defaultValue: fallback,
134
+ })
135
+ if (translated && translated !== 'runtime.collectionCell.itemCount') {
136
+ return translated
137
+ }
138
+ }
139
+ return fallback
140
+ }
141
+
142
+ /**
143
+ * Render a single scalar (or near-scalar) value for compact display.
144
+ * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
145
+ * - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
146
+ * - nested object → "{…}", nested array → "[N]"
147
+ * - null / undefined / "" → "-"
148
+ */
149
+ export function formatScalar(value: unknown): string {
150
+ if (value === null || value === undefined) return '-'
151
+ if (typeof value === 'boolean') return value ? '✓' : '✗'
152
+ if (typeof value === 'number') return String(value)
153
+ if (Array.isArray(value)) return `[${value.length}]`
154
+ if (typeof value === 'object') return '{…}'
155
+ const str = String(value)
156
+ if (str === '') return '-'
157
+ if (UUID_LIKE_RE.test(str) || str.length >= 32) {
158
+ return `${str.slice(0, 8)}…`
159
+ }
160
+ return str
161
+ }
162
+
163
+ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
164
+ typeof v === 'object' && v !== null && !Array.isArray(v)
165
+
166
+ /**
167
+ * Defensively coerce a raw cell value into something renderable. Strings that
168
+ * look like JSON (`[`/`{` start) are parsed; everything else is passed through.
169
+ */
170
+ function parseValue(value: unknown): unknown {
171
+ if (typeof value !== 'string') return value
172
+ const trimmed = value.trim()
173
+ if (
174
+ (trimmed.startsWith('[') && trimmed.endsWith(']')) ||
175
+ (trimmed.startsWith('{') && trimmed.endsWith('}'))
176
+ ) {
177
+ try {
178
+ return JSON.parse(trimmed)
179
+ } catch {
180
+ return value
181
+ }
182
+ }
183
+ return value
184
+ }
185
+
186
+ /** Stable union of keys across an array of row objects, first-seen order. */
187
+ function unionKeys(rows: Record<string, unknown>[]): string[] {
188
+ const seen: string[] = []
189
+ const set = new Set<string>()
190
+ for (const row of rows) {
191
+ for (const key of Object.keys(row)) {
192
+ if (!set.has(key)) {
193
+ set.add(key)
194
+ seen.push(key)
195
+ }
196
+ }
197
+ }
198
+ return seen
199
+ }
200
+
201
+ const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0'
202
+
203
+ function MiniTable({
204
+ rows,
205
+ locale,
206
+ t,
207
+ }: {
208
+ rows: Record<string, unknown>[]
209
+ locale?: string
210
+ t?: Translate
211
+ }) {
212
+ const keys = unionKeys(rows)
213
+ if (keys.length === 0) {
214
+ return <div className="p-3 text-xs text-muted-foreground">-</div>
215
+ }
216
+ return (
217
+ <Table>
218
+ <TableHeader>
219
+ <TableRow>
220
+ {keys.map((key) => (
221
+ <TableHead key={key} className="text-xs whitespace-nowrap">
222
+ {prettifyKey(key, locale, t)}
223
+ </TableHead>
224
+ ))}
225
+ </TableRow>
226
+ </TableHeader>
227
+ <TableBody>
228
+ {rows.map((row, i) => (
229
+ <TableRow key={i}>
230
+ {keys.map((key) => (
231
+ <TableCell
232
+ key={key}
233
+ className="text-xs whitespace-nowrap"
234
+ >
235
+ {formatScalar(row[key])}
236
+ </TableCell>
237
+ ))}
238
+ </TableRow>
239
+ ))}
240
+ </TableBody>
241
+ </Table>
242
+ )
243
+ }
244
+
245
+ function ScalarList({ values }: { values: unknown[] }) {
246
+ return (
247
+ <ul className="p-3 space-y-1">
248
+ {values.map((v, i) => (
249
+ <li key={i} className="text-xs text-foreground">
250
+ {formatScalar(v)}
251
+ </li>
252
+ ))}
253
+ </ul>
254
+ )
255
+ }
256
+
257
+ function PairList({
258
+ entries,
259
+ locale,
260
+ t,
261
+ }: {
262
+ entries: [string, unknown][]
263
+ locale?: string
264
+ t?: Translate
265
+ }) {
266
+ return (
267
+ <ul className="p-3 space-y-1">
268
+ {entries.map(([key, v]) => (
269
+ <li key={key} className="text-xs">
270
+ <span className="text-muted-foreground">
271
+ {prettifyKey(key, locale, t)}:
272
+ </span>{' '}
273
+ <span className="text-foreground">{formatScalar(v)}</span>
274
+ </li>
275
+ ))}
276
+ </ul>
277
+ )
278
+ }
279
+
280
+ /** Compact badge trigger that opens a popover panel. */
281
+ function PopoverShell({
282
+ label,
283
+ title,
284
+ children,
285
+ icon = true,
286
+ }: {
287
+ label: string
288
+ title: string
289
+ children: React.ReactNode
290
+ icon?: boolean
291
+ }) {
292
+ return (
293
+ <Popover>
294
+ <PopoverTrigger asChild>
295
+ <Badge
296
+ variant="secondary"
297
+ className="cursor-pointer gap-1 font-normal"
298
+ title={title}
299
+ >
300
+ {icon ? <List className="h-3 w-3" /> : null}
301
+ {label}
302
+ </Badge>
303
+ </PopoverTrigger>
304
+ <PopoverContent
305
+ align="start"
306
+ className={cn(PANEL_CLASS)}
307
+ >
308
+ {children}
309
+ </PopoverContent>
310
+ </Popover>
311
+ )
312
+ }
313
+
314
+ export interface CollectionCellProps {
315
+ value: unknown
316
+ /** Max items previewed inline for scalar arrays. */
317
+ maxInline?: number
318
+ /** Org/UI language tag (e.g. `es`, `en-US`). Defaults to `'en'`. */
319
+ locale?: string
320
+ /** Host i18n translator; takes precedence over the built-in dictionary. */
321
+ t?: Translate
322
+ }
323
+
324
+ /**
325
+ * Generic renderer for jsonb / array / object cell values. Brand-neutral,
326
+ * compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
327
+ */
328
+ export function CollectionCell({
329
+ value,
330
+ maxInline = 3,
331
+ locale,
332
+ t,
333
+ }: CollectionCellProps) {
334
+ const parsed = parseValue(value)
335
+
336
+ // Empty-ish → muted dash.
337
+ if (
338
+ parsed === null ||
339
+ parsed === undefined ||
340
+ parsed === '' ||
341
+ (Array.isArray(parsed) && parsed.length === 0) ||
342
+ (isPlainObject(parsed) && Object.keys(parsed).length === 0)
343
+ ) {
344
+ return <span className="text-muted-foreground text-xs">-</span>
345
+ }
346
+
347
+ // Non-collection scalar fell through here (e.g. unparseable string): truncate.
348
+ if (!Array.isArray(parsed) && !isPlainObject(parsed)) {
349
+ const str = String(parsed)
350
+ return (
351
+ <span
352
+ className="text-muted-foreground text-xs truncate block max-w-[300px]"
353
+ title={str}
354
+ >
355
+ {str.length > 80 ? `${str.slice(0, 80)}…` : str}
356
+ </span>
357
+ )
358
+ }
359
+
360
+ // ARRAY ------------------------------------------------------------------
361
+ if (Array.isArray(parsed)) {
362
+ const allObjects = parsed.every((item) => isPlainObject(item))
363
+ if (allObjects) {
364
+ const rows = parsed as Record<string, unknown>[]
365
+ const count = rows.length
366
+ const label = countLabel(count, locale, t)
367
+ const title = rows
368
+ .map((row) =>
369
+ Object.entries(row)
370
+ .map(
371
+ ([k, v]) =>
372
+ `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`
373
+ )
374
+ .join(', ')
375
+ )
376
+ .join(' | ')
377
+ return (
378
+ <PopoverShell label={label} title={title}>
379
+ <MiniTable rows={rows} locale={locale} t={t} />
380
+ </PopoverShell>
381
+ )
382
+ }
383
+
384
+ // Array of scalars (or mixed): preview first N joined, "+N" overflow.
385
+ const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ')
386
+ const overflow = parsed.length - maxInline
387
+ const label =
388
+ overflow > 0 ? `${preview} +${overflow}` : preview
389
+ const title = parsed.map(formatScalar).join(', ')
390
+ return (
391
+ <PopoverShell label={label} title={title} icon={false}>
392
+ <ScalarList values={parsed} />
393
+ </PopoverShell>
394
+ )
395
+ }
396
+
397
+ // PLAIN OBJECT -----------------------------------------------------------
398
+ const entries = Object.entries(parsed)
399
+ const inline = entries
400
+ .slice(0, maxInline)
401
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
402
+ .join(', ')
403
+ const overflow = entries.length - maxInline
404
+ const label = overflow > 0 ? `${inline} +${overflow}` : inline
405
+ const title = entries
406
+ .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
407
+ .join(', ')
408
+ return (
409
+ <PopoverShell label={label} title={title} icon={false}>
410
+ <PairList entries={entries} locale={locale} t={t} />
411
+ </PopoverShell>
412
+ )
413
+ }
@@ -45,6 +45,7 @@ import { Progress } from './dialogs/_primitives'
45
45
  import { humanizeToken } from './dynamic-columns-helpers'
46
46
  import { OptionsContext } from './options-context'
47
47
  import { DynamicIcon, isLucideIconName } from './dynamic-icon'
48
+ import { CollectionCell } from './collection-cell'
48
49
  import { isNilUuid, normalizeNilUuid } from './nil-uuid'
49
50
  import type { TableMetadata, ColumnDefinition } from './types'
50
51
  import { isColumnVisibleInTable } from './column-visibility'
@@ -1174,9 +1175,11 @@ export function makeDefaultGetDynamicColumns(
1174
1175
  default: {
1175
1176
  if (typeof value === 'object' && value !== null) {
1176
1177
  return (
1177
- <span className="text-muted-foreground text-xs">
1178
- {JSON.stringify(value)}
1179
- </span>
1178
+ <CollectionCell
1179
+ value={value}
1180
+ locale={currentLanguage}
1181
+ t={t}
1182
+ />
1180
1183
  )
1181
1184
  }
1182
1185
  if (
package/src/index.ts CHANGED
@@ -101,6 +101,14 @@ export {
101
101
  type DynamicColumnsHelpers,
102
102
  } from './dynamic-columns'
103
103
  export { humanizeToken } from './dynamic-columns-helpers'
104
+ export {
105
+ CollectionCell,
106
+ formatScalar,
107
+ prettifyKey,
108
+ countLabel,
109
+ type CollectionCellProps,
110
+ type Translate as CollectionCellTranslate,
111
+ } from './collection-cell'
104
112
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid'
105
113
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record'
106
114
  export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record'