@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 +16 -0
- package/dist/dashboard-empty-mockup.d.ts +5 -8
- package/dist/dashboard-empty-mockup.d.ts.map +1 -1
- package/dist/dashboard-empty-mockup.js +11 -20
- package/dist/dashboard-grid.d.ts.map +1 -1
- package/dist/dashboard-grid.js +4 -15
- package/dist/dialogs/dynamic-record.js +5 -3
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +8 -5
- package/package.json +1 -1
- package/src/__tests__/dashboard-grid.test.tsx +4 -4
- package/src/dashboard-empty-mockup.tsx +18 -42
- package/src/dashboard-grid.tsx +5 -16
- package/src/dialogs/dynamic-record.tsx +8 -5
- package/src/dynamic-select-field.tsx +8 -5
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
|
|
28
|
-
*
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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;
|
|
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
|
-
|
|
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
|
|
130
|
-
*
|
|
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
|
-
*
|
|
134
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
148
|
-
|
|
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,
|
|
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"}
|
package/dist/dashboard-grid.js
CHANGED
|
@@ -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
|
|
125
|
-
//
|
|
126
|
-
//
|
|
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
|
|
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,+
|
|
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
|
|
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`)
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
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
|
@@ -156,16 +156,16 @@ describe('DashboardGrid render', () => {
|
|
|
156
156
|
expect(screen.getByTestId('dashboard-empty')).toBeTruthy()
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
-
it('renders the
|
|
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
|
-
//
|
|
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 —
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
217
|
-
*
|
|
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
|
-
*
|
|
221
|
-
*
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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(
|
|
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
|
-
//
|
|
270
|
-
|
|
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']
|
package/src/dashboard-grid.tsx
CHANGED
|
@@ -154,25 +154,14 @@ export function DashboardGrid({
|
|
|
154
154
|
<div
|
|
155
155
|
data-testid="dashboard-empty"
|
|
156
156
|
className={cn(
|
|
157
|
-
// Full-bleed
|
|
158
|
-
//
|
|
159
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
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
|
|
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`)
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
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)
|