@asteby/metacore-runtime-react 29.2.4 → 29.2.5

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,21 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 29.2.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 428f5b0: fix(dashboard): skeleton vacío = 3 cards estáticas a ancho completo
8
+
9
+ Sin animación de reflow. El stack ya no usa `mc-demock-tile` (absolute), así
10
+ que no se colapsa en una franja chica en móvil / con sidebar.
11
+
12
+ - 48cf4e4: El botón de escaneo también aparece siempre (sin `isCameraScanSupported()`) en
13
+ los campos `dynamic_select` (`dynamic-select-field`) y en el modal de
14
+ crear/editar registro (`dialogs/dynamic-record`), no sólo en el input de texto.
15
+ El renglón de una orden de compra usa `dynamic_select`, así que ahí el icono de
16
+ cámara no aparecía en escritorio pese a declarar `scan`. Ahora los tres
17
+ renderers son consistentes con el POS.
18
+
3
19
  ## 29.2.4
4
20
 
5
21
  ### Patch Changes
@@ -24,15 +24,12 @@ export declare function computeLayoutRects(layout: LayoutSpec, gap?: number): Mo
24
24
  /** The precomputed rects for all four layouts (exported for the overlap test). */
25
25
  export declare function mockupLayoutRects(gap?: number): MockupRect[][];
26
26
  /**
27
- * Full-bleed skeleton dashboard that reflows through four layouts on a loop.
28
- * Overlap-free by construction (see the invariant note above). Purely
29
- * decorative — always `aria-hidden`, no text.
27
+ * Full-bleed skeleton dashboard: 3 static cards stacked full-width.
28
+ * No reflow animation size and layout stability matter more than motion.
29
+ * Purely decorative — always `aria-hidden`, no text.
30
30
  *
31
- * Contract: tiles are positioned with percentage heights, so the CONTAINER
32
- * must have a DEFINITE height (e.g. `h-[…]`, or a flex child with `flex-1`).
33
- * A parent with only `min-height` leaves the box auto-height and the tiles
34
- * collapse into a strip. The dashboard empty state mounts it inside a
35
- * definite-height box for exactly this reason.
31
+ * The layout-math helpers above (`computeLayoutRects`, etc.) stay exported for
32
+ * the overlap unit test / future reuse; the visible UI is the stack only.
36
33
  */
37
34
  export declare function DashboardEmptyMockup({ className }: {
38
35
  className?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-empty-mockup.d.ts","sourceRoot":"","sources":["../src/dashboard-empty-mockup.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,wEAAwE;AACxE,MAAM,WAAW,UAAU;IACvB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACZ;AAKD,UAAU,UAAU;IAChB,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAID,eAAO,MAAM,cAAc,EAAE,UAAU,EAKtC,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,SAAa,GAAG,UAAU,EAAE,CAerF;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,GAAG,SAAa,GAAG,UAAU,EAAE,EAAE,CAElE;AA4HD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAyCzE"}
1
+ {"version":3,"file":"dashboard-empty-mockup.d.ts","sourceRoot":"","sources":["../src/dashboard-empty-mockup.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,wEAAwE;AACxE,MAAM,WAAW,UAAU;IACvB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACZ;AAKD,UAAU,UAAU;IAChB,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAID,eAAO,MAAM,cAAc,EAAE,UAAU,EAKtC,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,UAAU,IAAI,CAAA;AAE3B;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,SAAa,GAAG,UAAU,EAAE,CAerF;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,GAAG,SAAa,GAAG,UAAU,EAAE,EAAE,CAElE;AA0HD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,qBAuBzE"}
@@ -112,7 +112,9 @@ function useMockupStyles() {
112
112
  document.head.appendChild(el);
113
113
  }, []);
114
114
  }
115
- const tileBase = 'rounded-lg border border-border/60 bg-card mc-demock-tile mc-demock-shimmer text-foreground';
115
+ // Static stack cards never `mc-demock-tile` (that class is position:absolute
116
+ // for the old animated grid and collapsed cards into a tiny top-left strip).
117
+ const stackTileBase = 'relative w-full rounded-xl border border-border/60 bg-card mc-demock-shimmer text-foreground';
116
118
  // Header shared by every card kind — icon chip + title bar + subtitle bar,
117
119
  // mirroring the real WidgetCard chrome (widgets/widget-card.tsx).
118
120
  function SkeletonHeader() {
@@ -122,28 +124,17 @@ function SkeletonHeader() {
122
124
  function TileGlyph({ kind }) {
123
125
  return (_jsxs("div", { className: "flex h-full flex-col gap-3 overflow-hidden p-3", children: [_jsx(SkeletonHeader, {}), _jsxs("div", { className: "min-h-0 flex-1", children: [kind === 'stat' && (_jsxs("div", { className: "flex h-full flex-col justify-center gap-2", children: [_jsx("div", { className: "h-6 w-1/2 rounded-md bg-foreground/15" }), _jsx("div", { className: "h-4 w-14 rounded-full bg-foreground/10" })] })), kind === 'chart' && (_jsx("div", { className: "flex h-full items-end gap-1.5", children: [46, 72, 38, 84, 58, 92, 50].map((h, i) => (_jsx("div", { className: "w-full rounded-sm bg-foreground/10", style: { height: `${h}%` } }, i))) })), kind === 'list' && (_jsx("div", { className: "flex h-full flex-col justify-center gap-2.5", children: [28, 40, 34].map((w, i) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "size-5 shrink-0 rounded-full bg-foreground/10" }), _jsx("div", { className: "h-2 flex-1 rounded-full bg-foreground/10" }), _jsx("div", { className: "h-2 shrink-0 rounded-full bg-foreground/12", style: { width: w } })] }, i))) })), kind === 'bar' && (_jsxs("div", { className: "flex h-full flex-col justify-center gap-3", children: [_jsx("div", { className: "h-2.5 w-full rounded-full bg-foreground/10", children: _jsx("div", { className: "h-full w-3/5 rounded-full bg-foreground/20" }) }), _jsx("div", { className: "h-2.5 w-full rounded-full bg-foreground/10", children: _jsx("div", { className: "h-full w-2/5 rounded-full bg-foreground/20" }) })] }))] })] }));
124
126
  }
125
- // Kind per tile, in the [c0-top, c0-bottom, c1-top, c1-bottom, c2-top, c2-bottom]
126
- // order used by computeLayoutRects.
127
- const TILE_KINDS = ['stat', 'chart', 'chart', 'list', 'stat', 'bar'];
128
127
  /**
129
- * Full-bleed skeleton dashboard that reflows through four layouts on a loop.
130
- * Overlap-free by construction (see the invariant note above). Purely
131
- * decorative — always `aria-hidden`, no text.
128
+ * Full-bleed skeleton dashboard: 3 static cards stacked full-width.
129
+ * No reflow animation size and layout stability matter more than motion.
130
+ * Purely decorative — always `aria-hidden`, no text.
132
131
  *
133
- * Contract: tiles are positioned with percentage heights, so the CONTAINER
134
- * must have a DEFINITE height (e.g. `h-[…]`, or a flex child with `flex-1`).
135
- * A parent with only `min-height` leaves the box auto-height and the tiles
136
- * collapse into a strip. The dashboard empty state mounts it inside a
137
- * definite-height box for exactly this reason.
132
+ * The layout-math helpers above (`computeLayoutRects`, etc.) stay exported for
133
+ * the overlap unit test / future reuse; the visible UI is the stack only.
138
134
  */
139
135
  export function DashboardEmptyMockup({ className }) {
140
136
  useMockupStyles();
141
- const layoutA = React.useMemo(() => computeLayoutRects(MOCKUP_LAYOUTS[0]), []);
142
- return (_jsxs("div", { "aria-hidden": "true", "data-testid": "dashboard-empty-mockup", className: cn('pointer-events-none select-none', className), children: [_jsx("div", { className: "flex min-h-full flex-col gap-3 sm:hidden", children: MOBILE_TILE_KINDS.map((kind, i) => (_jsx("div", { className: cn(tileBase, 'mc-demock-shimmer relative min-h-[132px] flex-1'), children: _jsx(TileGlyph, { kind: kind }) }, i))) }), _jsx("div", { className: "mc-demock hidden h-full sm:block", children: layoutA.map((r, i) => (_jsx("div", { "data-mockup-tile": "tile", className: cn(tileBase, `mc-demock-t${i}`),
143
- // Base position = layout A, so reduced-motion (animation off)
144
- // shows the full static composition.
145
- style: { left: `${r.x}%`, top: `${r.y}%`, width: `${r.w}%`, height: `${r.h}%` }, children: _jsx(TileGlyph, { kind: TILE_KINDS[i] }) }, i))) })] }));
137
+ return (_jsx("div", { "aria-hidden": "true", "data-testid": "dashboard-empty-mockup", className: cn('pointer-events-none w-full select-none', className), children: _jsx("div", { className: "flex w-full flex-col gap-4", "data-testid": "dashboard-empty-mockup-stack", children: STACK_TILE_KINDS.map((kind, i) => (_jsx("div", { className: cn(stackTileBase, 'min-h-[200px]'), children: _jsx(TileGlyph, { kind: kind }) }, i))) }) }));
146
138
  }
147
- // En móvil mostramos 3 cards apiladas (stat + chart + list) a ancho completo
148
- // suficiente para comunicar "tablero cargando/ vacío" sin apretar 6 tiles.
149
- const MOBILE_TILE_KINDS = ['stat', 'chart', 'list'];
139
+ // 3 cards a ancho completo stat + chart + list. Sin animación de reflow.
140
+ const STACK_TILE_KINDS = ['stat', 'chart', 'list'];
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAY1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBAuJpB"}
1
+ {"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAY1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBA4IpB"}
@@ -121,21 +121,10 @@ export function DashboardGrid({ groups, widgets, loadData, isAdmin, locale, curr
121
121
  // Global empty state (no widgets at all / none visible after gating).
122
122
  if (visibleGroups.length === 0) {
123
123
  return (_jsx("div", { "data-testid": "dashboard-empty", className: cn(
124
- // Full-bleed: the mockup fills the whole dashboard area, not a
125
- // centered illustration. No caption the animation speaks for
126
- // itself (and dodges shipping un-localized copy).
127
- //
128
- // DEFINITE height (clamp), not min-height: the mockup positions
129
- // its tiles with percentage heights, which only resolve against a
130
- // parent whose height is definite. A min-height alone leaves the
131
- // box auto-height and the tiles collapse into a strip — so we own
132
- // the height here instead of hoping the dashboard layout supplies
133
- // one.
134
- // No dashed border / padding wrapper: the skeleton tiles ARE
135
- // cards (same anatomy as the real widgets), so the mockup
136
- // should float exactly like the loaded dashboard, not sit
137
- // inside a boxed placeholder. Keep only the definite height.
138
- 'h-[clamp(420px,60vh,720px)] w-full', className), children: _jsx(DashboardEmptyMockup, { className: "h-full" }) }));
124
+ // Full-bleed stack of static skeleton cards (no dashed box,
125
+ // no caption). Height follows the 3 tall cards — no absolute
126
+ // %-height grid that needed a clamp wrapper.
127
+ 'w-full', className), children: _jsx(DashboardEmptyMockup, {}) }));
139
128
  }
140
129
  return (_jsx("div", { "data-testid": "dashboard-grid", className: cn(
141
130
  // Masonry: balanced CSS columns. Cards take their natural height
@@ -18,7 +18,7 @@ import { toast } from 'sonner';
18
18
  import { format, parseISO } from 'date-fns';
19
19
  import { es } from 'date-fns/locale';
20
20
  import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon, ScanLine } from 'lucide-react';
21
- import { BarcodeScanner, isCameraScanSupported } from '../barcode-scanner';
21
+ import { BarcodeScanner } from '../barcode-scanner';
22
22
  import { useApi } from '../api-context';
23
23
  import { toastServerError, extractFieldErrors, localizeFieldIssue } from '../server-error';
24
24
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
@@ -1026,9 +1026,11 @@ export function EditField({ field, value, onChange, record }) {
1026
1026
  */
1027
1027
  function ScannableRecordInput({ field, value, onChange, inputType, }) {
1028
1028
  const [scanOpen, setScanOpen] = useState(false);
1029
+ // El botón de escaneo aparece siempre que el campo declara `scan` (como el
1030
+ // POS), sin esconderse por `isCameraScanSupported()`. El BarcodeScanner ya
1031
+ // muestra un mensaje amable cuando el navegador no tiene cámara.
1029
1032
  const scanEnabled = !!(field.scan ??
1030
- field.scannable) &&
1031
- isCameraScanSupported();
1033
+ field.scannable);
1032
1034
  const input = (_jsx(Input, { type: inputType, value: value ?? '', onChange: (e) => onChange(inputType === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value), placeholder: field.placeholder }));
1033
1035
  if (!scanEnabled)
1034
1036
  return input;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAyCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,IAAS,GACZ,EAAE;IACC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,+BA6BA;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCA4BA;AAWD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,GACnB,EAAE,uBAAuB,+BAsQzB;AAED,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAyCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EACxB,KAAK,EACL,IAAI,EACJ,IAAS,GACZ,EAAE;IACC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,+BA6BA;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCA4BA;AAWD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,GACnB,EAAE,uBAAuB,+BAyQzB;AAED,eAAe,kBAAkB,CAAA"}
@@ -26,7 +26,7 @@ import { useEffect, useRef, useState } from 'react';
26
26
  import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, InitialsAvatar, } from '@asteby/metacore-ui/primitives';
27
27
  import { Check, ChevronsUpDown, Loader2, Plus, ScanLine } from 'lucide-react';
28
28
  import { resolveColorCss } from '@asteby/metacore-ui/lib';
29
- import { BarcodeScanner, isCameraScanSupported } from './barcode-scanner';
29
+ import { BarcodeScanner } from './barcode-scanner';
30
30
  import { DynamicIcon, isLucideIconName } from './dynamic-icon';
31
31
  import { useOptionsResolver } from './use-options-resolver';
32
32
  import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema';
@@ -98,10 +98,13 @@ export function DynamicSelectField({ field, value, onChange, seedOption, depends
98
98
  const [search, setSearch] = useState('');
99
99
  const [scanOpen, setScanOpen] = useState(false);
100
100
  const debounced = useDebounced(search, 250);
101
- // Escaneo por cámara para "llenar rápido": opt-in por campo (`scan`) y sólo
102
- // si el navegador soporta BarcodeDetector. Un código escaneado alimenta la
103
- // búsqueda y abre el picker para elegir la referencia sin tipear el UUID.
104
- const scanEnabled = !!(field.scan ?? field.scannable) && isCameraScanSupported();
101
+ // Escaneo por cámara para "llenar rápido": opt-in por campo (`scan`). El
102
+ // botón aparece SIEMPRE que el campo lo declara (igual que el POS), sin
103
+ // esconderse por `isCameraScanSupported()` en escritorio el botón no salía
104
+ // aunque el campo lo pidiera. El BarcodeScanner ya degrada con un mensaje
105
+ // cuando no hay cámara. Un código escaneado alimenta la búsqueda y abre el
106
+ // picker para elegir la referencia sin tipear el UUID.
107
+ const scanEnabled = !!(field.scan ?? field.scannable);
105
108
  const handleScanDetected = (code) => {
106
109
  setSearch(code);
107
110
  setOpen(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "29.2.4",
3
+ "version": "29.2.5",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -156,16 +156,16 @@ describe('DashboardGrid render', () => {
156
156
  expect(screen.getByTestId('dashboard-empty')).toBeTruthy()
157
157
  })
158
158
 
159
- it('renders the animated mockup (decorative) in the empty state', () => {
159
+ it('renders the static full-width skeleton stack (decorative) in the empty state', () => {
160
160
  render(<DashboardGrid widgets={[]} loadData={loaderOf({})} />)
161
161
  const mock = screen.getByTestId('dashboard-empty-mockup')
162
162
  expect(mock).toBeTruthy()
163
+ expect(screen.getByTestId('dashboard-empty-mockup-stack')).toBeTruthy()
163
164
  // Purely decorative → hidden from the a11y tree.
164
165
  expect(mock.getAttribute('aria-hidden')).toBe('true')
165
- // Keyframes are injected once into the document head.
166
+ // Shimmer styles still inject once (no reflow keyframes required).
166
167
  expect(document.getElementById('mc-dashboard-empty-mockup-style')).toBeTruthy()
167
- // No caption text — the animation carries the meaning (and avoids
168
- // shipping un-localized copy from the package).
168
+ // No caption text — avoids shipping un-localized copy from the package.
169
169
  expect(screen.queryByText(/dashboard/i)).toBeNull()
170
170
  })
171
171
 
@@ -137,8 +137,10 @@ function useMockupStyles() {
137
137
  }, [])
138
138
  }
139
139
 
140
- const tileBase =
141
- 'rounded-lg border border-border/60 bg-card mc-demock-tile mc-demock-shimmer text-foreground'
140
+ // Static stack cards — never `mc-demock-tile` (that class is position:absolute
141
+ // for the old animated grid and collapsed cards into a tiny top-left strip).
142
+ const stackTileBase =
143
+ 'relative w-full rounded-xl border border-border/60 bg-card mc-demock-shimmer text-foreground'
142
144
 
143
145
  type Glyph = 'chart' | 'stat' | 'list' | 'bar'
144
146
 
@@ -208,64 +210,38 @@ function TileGlyph({ kind }: { kind: Glyph }) {
208
210
  )
209
211
  }
210
212
 
211
- // Kind per tile, in the [c0-top, c0-bottom, c1-top, c1-bottom, c2-top, c2-bottom]
212
- // order used by computeLayoutRects.
213
- const TILE_KINDS: Glyph[] = ['stat', 'chart', 'chart', 'list', 'stat', 'bar']
214
-
215
213
  /**
216
- * Full-bleed skeleton dashboard that reflows through four layouts on a loop.
217
- * Overlap-free by construction (see the invariant note above). Purely
218
- * decorative — always `aria-hidden`, no text.
214
+ * Full-bleed skeleton dashboard: 3 static cards stacked full-width.
215
+ * No reflow animation size and layout stability matter more than motion.
216
+ * Purely decorative — always `aria-hidden`, no text.
219
217
  *
220
- * Contract: tiles are positioned with percentage heights, so the CONTAINER
221
- * must have a DEFINITE height (e.g. `h-[…]`, or a flex child with `flex-1`).
222
- * A parent with only `min-height` leaves the box auto-height and the tiles
223
- * collapse into a strip. The dashboard empty state mounts it inside a
224
- * definite-height box for exactly this reason.
218
+ * The layout-math helpers above (`computeLayoutRects`, etc.) stay exported for
219
+ * the overlap unit test / future reuse; the visible UI is the stack only.
225
220
  */
226
221
  export function DashboardEmptyMockup({ className }: { className?: string }) {
227
222
  useMockupStyles()
228
- const layoutA = React.useMemo(() => computeLayoutRects(MOCKUP_LAYOUTS[0]), [])
229
223
  return (
230
224
  <div
231
225
  aria-hidden="true"
232
226
  data-testid="dashboard-empty-mockup"
233
- className={cn('pointer-events-none select-none', className)}
227
+ className={cn('pointer-events-none w-full select-none', className)}
234
228
  >
235
- {/* Móvil (<sm): las 3 columnas absolutas quedan angostas y apretadas
236
- en un teléfono. Un STACK vertical de cards a ancho completo se lee
237
- claro. Toggle por CSS (sin JS, SSR-safe): el reflow animado sólo
238
- aparece de sm hacia arriba, donde hay ancho para las 3 columnas. */}
239
- <div className="flex min-h-full flex-col gap-3 sm:hidden">
240
- {MOBILE_TILE_KINDS.map((kind, i) => (
229
+ <div
230
+ className="flex w-full flex-col gap-4"
231
+ data-testid="dashboard-empty-mockup-stack"
232
+ >
233
+ {STACK_TILE_KINDS.map((kind, i) => (
241
234
  <div
242
235
  key={i}
243
- className={cn(tileBase, 'mc-demock-shimmer relative min-h-[132px] flex-1')}
236
+ className={cn(stackTileBase, 'min-h-[200px]')}
244
237
  >
245
238
  <TileGlyph kind={kind} />
246
239
  </div>
247
240
  ))}
248
241
  </div>
249
-
250
- {/* Tablet/escritorio: el mockup de 3 columnas que reorganiza en bucle. */}
251
- <div className="mc-demock hidden h-full sm:block">
252
- {layoutA.map((r, i) => (
253
- <div
254
- key={i}
255
- data-mockup-tile="tile"
256
- className={cn(tileBase, `mc-demock-t${i}`)}
257
- // Base position = layout A, so reduced-motion (animation off)
258
- // shows the full static composition.
259
- style={{ left: `${r.x}%`, top: `${r.y}%`, width: `${r.w}%`, height: `${r.h}%` }}
260
- >
261
- <TileGlyph kind={TILE_KINDS[i]} />
262
- </div>
263
- ))}
264
- </div>
265
242
  </div>
266
243
  )
267
244
  }
268
245
 
269
- // En móvil mostramos 3 cards apiladas (stat + chart + list) a ancho completo
270
- // suficiente para comunicar "tablero cargando/ vacío" sin apretar 6 tiles.
271
- const MOBILE_TILE_KINDS: Glyph[] = ['stat', 'chart', 'list']
246
+ // 3 cards a ancho completo stat + chart + list. Sin animación de reflow.
247
+ const STACK_TILE_KINDS: Glyph[] = ['stat', 'chart', 'list']
@@ -154,25 +154,14 @@ export function DashboardGrid({
154
154
  <div
155
155
  data-testid="dashboard-empty"
156
156
  className={cn(
157
- // Full-bleed: the mockup fills the whole dashboard area, not a
158
- // centered illustration. No caption the animation speaks for
159
- // itself (and dodges shipping un-localized copy).
160
- //
161
- // DEFINITE height (clamp), not min-height: the mockup positions
162
- // its tiles with percentage heights, which only resolve against a
163
- // parent whose height is definite. A min-height alone leaves the
164
- // box auto-height and the tiles collapse into a strip — so we own
165
- // the height here instead of hoping the dashboard layout supplies
166
- // one.
167
- // No dashed border / padding wrapper: the skeleton tiles ARE
168
- // cards (same anatomy as the real widgets), so the mockup
169
- // should float exactly like the loaded dashboard, not sit
170
- // inside a boxed placeholder. Keep only the definite height.
171
- 'h-[clamp(420px,60vh,720px)] w-full',
157
+ // Full-bleed stack of static skeleton cards (no dashed box,
158
+ // no caption). Height follows the 3 tall cards — no absolute
159
+ // %-height grid that needed a clamp wrapper.
160
+ 'w-full',
172
161
  className,
173
162
  )}
174
163
  >
175
- <DashboardEmptyMockup className="h-full" />
164
+ <DashboardEmptyMockup />
176
165
  </div>
177
166
  )
178
167
  }
@@ -47,7 +47,7 @@ import { toast } from 'sonner'
47
47
  import { format, parseISO } from 'date-fns'
48
48
  import { es } from 'date-fns/locale'
49
49
  import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon, ScanLine } from 'lucide-react'
50
- import { BarcodeScanner, isCameraScanSupported } from '../barcode-scanner'
50
+ import { BarcodeScanner } from '../barcode-scanner'
51
51
  import { useApi } from '../api-context'
52
52
  import { toastServerError, extractFieldErrors, localizeFieldIssue } from '../server-error'
53
53
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field'
@@ -1848,10 +1848,13 @@ function ScannableRecordInput({
1848
1848
  inputType: string
1849
1849
  }) {
1850
1850
  const [scanOpen, setScanOpen] = useState(false)
1851
- const scanEnabled =
1852
- !!((field as { scan?: boolean; scannable?: boolean }).scan ??
1853
- (field as { scan?: boolean; scannable?: boolean }).scannable) &&
1854
- isCameraScanSupported()
1851
+ // El botón de escaneo aparece siempre que el campo declara `scan` (como el
1852
+ // POS), sin esconderse por `isCameraScanSupported()`. El BarcodeScanner ya
1853
+ // muestra un mensaje amable cuando el navegador no tiene cámara.
1854
+ const scanEnabled = !!(
1855
+ (field as { scan?: boolean; scannable?: boolean }).scan ??
1856
+ (field as { scan?: boolean; scannable?: boolean }).scannable
1857
+ )
1855
1858
  const input = (
1856
1859
  <Input
1857
1860
  type={inputType}
@@ -37,7 +37,7 @@ import {
37
37
  } from '@asteby/metacore-ui/primitives'
38
38
  import { Check, ChevronsUpDown, Loader2, Plus, ScanLine } from 'lucide-react'
39
39
  import { resolveColorCss } from '@asteby/metacore-ui/lib'
40
- import { BarcodeScanner, isCameraScanSupported } from './barcode-scanner'
40
+ import { BarcodeScanner } from './barcode-scanner'
41
41
  import { DynamicIcon, isLucideIconName } from './dynamic-icon'
42
42
  import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
43
43
  import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema'
@@ -193,10 +193,13 @@ export function DynamicSelectField({
193
193
  const [scanOpen, setScanOpen] = useState(false)
194
194
  const debounced = useDebounced(search, 250)
195
195
 
196
- // Escaneo por cámara para "llenar rápido": opt-in por campo (`scan`) y sólo
197
- // si el navegador soporta BarcodeDetector. Un código escaneado alimenta la
198
- // búsqueda y abre el picker para elegir la referencia sin tipear el UUID.
199
- const scanEnabled = !!(field.scan ?? field.scannable) && isCameraScanSupported()
196
+ // Escaneo por cámara para "llenar rápido": opt-in por campo (`scan`). El
197
+ // botón aparece SIEMPRE que el campo lo declara (igual que el POS), sin
198
+ // esconderse por `isCameraScanSupported()` en escritorio el botón no salía
199
+ // aunque el campo lo pidiera. El BarcodeScanner ya degrada con un mensaje
200
+ // cuando no hay cámara. Un código escaneado alimenta la búsqueda y abre el
201
+ // picker para elegir la referencia sin tipear el UUID.
202
+ const scanEnabled = !!(field.scan ?? field.scannable)
200
203
  const handleScanDetected = (code: string) => {
201
204
  setSearch(code)
202
205
  setOpen(true)