@asteby/metacore-runtime-react 29.2.2 → 29.2.4

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
+ ## 29.2.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 99c22b4: El botón de escaneo por cámara en el form declarativo (auto-CRUD) ahora aparece
8
+ siempre que el campo declara `scan`, igual que el POS, en vez de esconderse
9
+ cuando el navegador no expone `BarcodeDetector`. En escritorio el usuario veía
10
+ el icono en el POS pero no al crear un producto (CÓDIGO/SKU) ni en el renglón de
11
+ compra; ahora es consistente. El BarcodeScanner ya degrada con un mensaje cuando
12
+ no hay soporte de cámara.
13
+
14
+ ## 29.2.3
15
+
16
+ ### Patch Changes
17
+
18
+ - 53f6efa: Line-items declarativos (`DynamicLineItems`) responsive en móvil: la tabla de N columnas no cabía en un teléfono (obligaba a scroll horizontal renglón por renglón). En móvil cada renglón se vuelve una **card apilada** con sus columnas como campos etiquetados —mismo widget de celda, editable con el pulgar—, el botón "Agregar renglón" ocupa el ancho completo y los totales se muestran como resumen. De `sm` en adelante se conserva la tabla. Beneficia a todo modal con renglones (crear orden de compra, recepción de mercancía, asientos contables).
19
+
3
20
  ## 29.2.2
4
21
 
5
22
  ### Patch Changes
@@ -8,7 +8,7 @@ import { FieldSection, WizardProgress } from './form-layout-ui';
8
8
  import { Input, Textarea, Label, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
9
9
  import { buildZodSchema, resolveWidget, isLineItemsField, evaluateBalance, applyOptionWhen, getDependsOn, getVisibleWhen, evaluateVisibleWhen, } from './dynamic-form-schema';
10
10
  import { ScanLine } from 'lucide-react';
11
- import { BarcodeScanner, isCameraScanSupported } from './barcode-scanner';
11
+ import { BarcodeScanner } from './barcode-scanner';
12
12
  import { useOptionsResolver } from './use-options-resolver';
13
13
  import { DynamicLineItems } from './dynamic-line-items';
14
14
  import { DynamicSelectField } from './dynamic-select-field';
@@ -253,7 +253,14 @@ function FieldRenderer({ field, value, onChange, initialValues, effectiveOptions
253
253
  */
254
254
  function ScannableInput({ field, value, onChange, type, }) {
255
255
  const [scanOpen, setScanOpen] = useState(false);
256
- const scanEnabled = !!(field.scan ?? field.scannable) && isCameraScanSupported();
256
+ // El botón aparece SIEMPRE que el campo declara `scan`, igual que el POS
257
+ // (que gatea por su handler, no por soporte). No lo escondemos por
258
+ // `isCameraScanSupported()`: en un navegador sin `BarcodeDetector` el botón
259
+ // seguía sin verse en escritorio aunque el campo lo pidiera, y el usuario no
260
+ // entendía por qué el POS sí lo mostraba y el form no. El BarcodeScanner ya
261
+ // degrada con un mensaje ("usá un lector físico / escribí a mano") cuando no
262
+ // hay soporte, así que mostrar el botón es seguro y consistente.
263
+ const scanEnabled = !!(field.scan ?? field.scannable);
257
264
  const input = (_jsx(Input, { id: field.key, type: type, value: value ?? '', onChange: (e) => onChange(type === 'number' ? e.target.valueAsNumber || '' : e.target.value), placeholder: field.placeholder }));
258
265
  if (!scanEnabled)
259
266
  return input;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACnC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,EAAE,qBAAqB,+BA8J/G"}
1
+ {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACnC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,EAAE,qBAAqB,+BA6O/G"}
@@ -63,7 +63,8 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
63
63
  }
64
64
  updateCell(idx, key, cellValue);
65
65
  };
66
- return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsx("div", { className: "overflow-x-auto rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
66
+ const totalCols = itemFields.filter((c) => c.total);
67
+ return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsxs("div", { className: "grid gap-2 sm:hidden", children: [rows.length === 0 && (_jsx("div", { className: "text-muted-foreground rounded-md border px-3 py-4 text-center text-sm", children: "Sin renglones" })), rows.map((row, idx) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-muted-foreground text-xs font-medium", children: ["Rengl\u00F3n ", idx + 1] }), !lockRows && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), _jsx("div", { className: "grid gap-2.5", children: itemFields.map((col) => (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row })] }, col.key))) })] }, idx))), hasTotals && rows.length > 0 && (_jsx("div", { className: "bg-muted/30 grid gap-1.5 rounded-md border p-3", children: totalCols.map((col) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: col.label }), _jsx("span", { className: "font-semibold tabular-nums", children: fmtNumber(totals[col.key] ?? 0) })] }, col.key))) }))] }), _jsx("div", { className: "hidden overflow-x-auto rounded-md border sm:block", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
67
68
  (isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), !lockRows && _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + (lockRows ? 0 : 1), className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => (_jsx("td", { className: "px-2 py-1.5", children: _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row }) }, col.key))), !lockRows && (_jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) }))] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
68
69
  if (ci === 0) {
69
70
  return (_jsx("td", { className: "px-3 py-2 text-left font-medium text-muted-foreground", children: "Totales" }, col.key));
@@ -72,7 +73,7 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
72
73
  (col.total
73
74
  ? 'text-right font-semibold tabular-nums'
74
75
  : ''), children: col.total ? fmtNumber(totals[col.key] ?? 0) : null }, col.key));
75
- }), !lockRows && _jsx("td", {})] }) }))] }) }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [lockRows ? (_jsx("span", {})) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addRow, disabled: disabled, children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Agregar rengl\u00F3n"] })), balance && _jsx(BalanceBadge, { state: balance })] })] }));
76
+ }), !lockRows && _jsx("td", {})] }) }))] }) }), _jsxs("div", { className: "flex flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:justify-between", children: [lockRows ? (_jsx("span", {})) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addRow, disabled: disabled, className: "w-full sm:w-auto", children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Agregar rengl\u00F3n"] })), balance && _jsx(BalanceBadge, { state: balance })] })] }));
76
77
  }
77
78
  function BalanceBadge({ state, }) {
78
79
  if (state.balanced) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "29.2.2",
3
+ "version": "29.2.4",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,7 +28,7 @@ import {
28
28
  evaluateVisibleWhen,
29
29
  } from './dynamic-form-schema'
30
30
  import { ScanLine } from 'lucide-react'
31
- import { BarcodeScanner, isCameraScanSupported } from './barcode-scanner'
31
+ import { BarcodeScanner } from './barcode-scanner'
32
32
  import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
33
33
  import { DynamicLineItems } from './dynamic-line-items'
34
34
  import { DynamicSelectField } from './dynamic-select-field'
@@ -442,7 +442,14 @@ function ScannableInput({
442
442
  type,
443
443
  }: FieldRendererProps & { type: string }) {
444
444
  const [scanOpen, setScanOpen] = useState(false)
445
- const scanEnabled = !!(field.scan ?? field.scannable) && isCameraScanSupported()
445
+ // El botón aparece SIEMPRE que el campo declara `scan`, igual que el POS
446
+ // (que gatea por su handler, no por soporte). No lo escondemos por
447
+ // `isCameraScanSupported()`: en un navegador sin `BarcodeDetector` el botón
448
+ // seguía sin verse en escritorio aunque el campo lo pidiera, y el usuario no
449
+ // entendía por qué el POS sí lo mostraba y el form no. El BarcodeScanner ya
450
+ // degrada con un mensaje ("usá un lector físico / escribí a mano") cuando no
451
+ // hay soporte, así que mostrar el botón es seguro y consistente.
452
+ const scanEnabled = !!(field.scan ?? field.scannable)
446
453
  const input = (
447
454
  <Input
448
455
  id={field.key}
@@ -113,9 +113,81 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
113
113
  updateCell(idx, key, cellValue)
114
114
  }
115
115
 
116
+ const totalCols = itemFields.filter((c) => c.total)
117
+
116
118
  return (
117
119
  <div className="grid gap-2" data-widget="line_items">
118
- <div className="overflow-x-auto rounded-md border">
120
+ {/* Móvil (<sm): una tabla de N columnas no cabe en un teléfono —
121
+ obliga a scroll horizontal renglón por renglón. Cada renglón se
122
+ vuelve una CARD apilada con sus columnas como campos etiquetados
123
+ (mismo CellRenderer), legible y editable con el pulgar. La tabla
124
+ se conserva de sm en adelante. */}
125
+ <div className="grid gap-2 sm:hidden">
126
+ {rows.length === 0 && (
127
+ <div className="text-muted-foreground rounded-md border px-3 py-4 text-center text-sm">
128
+ Sin renglones
129
+ </div>
130
+ )}
131
+ {rows.map((row, idx) => (
132
+ <div key={idx} className="rounded-md border p-3">
133
+ <div className="mb-2 flex items-center justify-between">
134
+ <span className="text-muted-foreground text-xs font-medium">
135
+ Renglón {idx + 1}
136
+ </span>
137
+ {!lockRows && (
138
+ <Button
139
+ type="button"
140
+ variant="ghost"
141
+ size="icon"
142
+ className="size-7"
143
+ onClick={() => removeRow(idx)}
144
+ disabled={disabled}
145
+ aria-label="Eliminar renglón"
146
+ >
147
+ <Trash2 className="h-4 w-4 text-red-500" />
148
+ </Button>
149
+ )}
150
+ </div>
151
+ <div className="grid gap-2.5">
152
+ {itemFields.map((col) => (
153
+ <div key={col.key} className="grid gap-1">
154
+ <span className="text-xs font-medium">
155
+ {col.label}
156
+ {col.required && (
157
+ <span className="ml-1 text-red-500">*</span>
158
+ )}
159
+ </span>
160
+ <CellRenderer
161
+ field={col}
162
+ value={row?.[col.key]}
163
+ onChange={(v: any) => handleCell(idx, col.key, v)}
164
+ disabled={disabled}
165
+ formValues={formValues}
166
+ rowValues={row}
167
+ />
168
+ </div>
169
+ ))}
170
+ </div>
171
+ </div>
172
+ ))}
173
+ {hasTotals && rows.length > 0 && (
174
+ <div className="bg-muted/30 grid gap-1.5 rounded-md border p-3">
175
+ {totalCols.map((col) => (
176
+ <div
177
+ key={col.key}
178
+ className="flex items-center justify-between text-sm"
179
+ >
180
+ <span className="text-muted-foreground">{col.label}</span>
181
+ <span className="font-semibold tabular-nums">
182
+ {fmtNumber(totals[col.key] ?? 0)}
183
+ </span>
184
+ </div>
185
+ ))}
186
+ </div>
187
+ )}
188
+ </div>
189
+
190
+ <div className="hidden overflow-x-auto rounded-md border sm:block">
119
191
  <table className="w-full text-sm">
120
192
  <thead className="bg-muted/50">
121
193
  <tr>
@@ -210,11 +282,18 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
210
282
  )}
211
283
  </table>
212
284
  </div>
213
- <div className="flex items-center justify-between gap-2">
285
+ <div className="flex flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:justify-between">
214
286
  {lockRows ? (
215
287
  <span />
216
288
  ) : (
217
- <Button type="button" variant="outline" size="sm" onClick={addRow} disabled={disabled}>
289
+ <Button
290
+ type="button"
291
+ variant="outline"
292
+ size="sm"
293
+ onClick={addRow}
294
+ disabled={disabled}
295
+ className="w-full sm:w-auto"
296
+ >
218
297
  <Plus className="mr-1 h-4 w-4" />
219
298
  Agregar renglón
220
299
  </Button>