@asteby/metacore-runtime-react 49.5.0 → 49.6.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/business/customer-picker.d.ts +8 -2
  3. package/dist/business/customer-picker.d.ts.map +1 -1
  4. package/dist/business/customer-picker.js +30 -27
  5. package/dist/business/index.d.ts +1 -0
  6. package/dist/business/index.d.ts.map +1 -1
  7. package/dist/business/index.js +1 -0
  8. package/dist/business/line-product-cell.d.ts +8 -10
  9. package/dist/business/line-product-cell.d.ts.map +1 -1
  10. package/dist/business/line-product-cell.js +13 -90
  11. package/dist/business/product-options.d.ts +41 -0
  12. package/dist/business/product-options.d.ts.map +1 -0
  13. package/dist/business/product-options.js +58 -0
  14. package/dist/business/product-picker.d.ts +5 -1
  15. package/dist/business/product-picker.d.ts.map +1 -1
  16. package/dist/business/product-picker.js +26 -35
  17. package/dist/business/relate-documents.d.ts +1 -1
  18. package/dist/business/relate-documents.d.ts.map +1 -1
  19. package/dist/business/relate-documents.js +6 -8
  20. package/dist/business/vehicle-picker.d.ts +8 -2
  21. package/dist/business/vehicle-picker.d.ts.map +1 -1
  22. package/dist/business/vehicle-picker.js +28 -28
  23. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  24. package/dist/dialogs/dynamic-record.js +28 -43
  25. package/dist/dynamic-multi-select-field.d.ts.map +1 -1
  26. package/dist/dynamic-multi-select-field.js +17 -5
  27. package/dist/dynamic-select-field.d.ts +7 -23
  28. package/dist/dynamic-select-field.d.ts.map +1 -1
  29. package/dist/dynamic-select-field.js +67 -161
  30. package/dist/entity-select.d.ts +5 -2
  31. package/dist/entity-select.d.ts.map +1 -1
  32. package/dist/entity-select.js +23 -116
  33. package/dist/index.d.ts +7 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +8 -1
  36. package/dist/record-picker-actions.d.ts +40 -2
  37. package/dist/record-picker-actions.d.ts.map +1 -1
  38. package/dist/record-picker-actions.js +44 -4
  39. package/dist/record-picker-dialog.d.ts +23 -0
  40. package/dist/record-picker-dialog.d.ts.map +1 -0
  41. package/dist/record-picker-dialog.js +37 -0
  42. package/dist/record-picker-option.d.ts +25 -0
  43. package/dist/record-picker-option.d.ts.map +1 -0
  44. package/dist/record-picker-option.js +55 -0
  45. package/dist/record-picker.d.ts +149 -0
  46. package/dist/record-picker.d.ts.map +1 -0
  47. package/dist/record-picker.js +372 -0
  48. package/package.json +1 -1
  49. package/src/__tests__/business-pickers.test.tsx +6 -3
  50. package/src/__tests__/document-editor-inline-selects.test.tsx +2 -1
  51. package/src/__tests__/document-form-dialog.test.tsx +4 -3
  52. package/src/__tests__/document-lines-grid.test.tsx +2 -1
  53. package/src/__tests__/record-picker.test.tsx +469 -0
  54. package/src/business/customer-picker.tsx +87 -98
  55. package/src/business/index.ts +1 -0
  56. package/src/business/line-product-cell.tsx +55 -180
  57. package/src/business/product-options.tsx +119 -0
  58. package/src/business/product-picker.tsx +63 -126
  59. package/src/business/relate-documents.tsx +36 -45
  60. package/src/business/vehicle-picker.tsx +76 -99
  61. package/src/dialogs/dynamic-record.tsx +44 -97
  62. package/src/dynamic-multi-select-field.tsx +39 -13
  63. package/src/dynamic-select-field.tsx +143 -368
  64. package/src/entity-select.tsx +53 -202
  65. package/src/index.ts +27 -2
  66. package/src/record-picker-actions.tsx +101 -8
  67. package/src/record-picker-dialog.tsx +80 -0
  68. package/src/record-picker-option.tsx +96 -0
  69. package/src/record-picker.tsx +971 -0
  70. package/dist/business/use-async-search.d.ts +0 -10
  71. package/dist/business/use-async-search.d.ts.map +0 -1
  72. package/dist/business/use-async-search.js +0 -38
  73. package/src/business/use-async-search.ts +0 -43
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 49.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 046d56f: `RecordPicker`: un solo selector de registro para todo el runtime. Búsqueda asíncrona (`search` con debounce/cancelación o `items` del resolver de opciones), estados cargando/vacío/error, selección simple y múltiple, crear (el «+» unido al trigger y «Crear …» al pie prellenado con el texto buscado) y editar el seleccionado (lápiz), opciones enriquecidas por props (avatar/iniciales, subtítulo, precio/stock), variantes `field`/`cell` y trigger `button`/`input` (texto libre), lista siempre en portal collision-aware por encima de los diálogos y teclado completo (↑/↓/Home/End/Enter/Esc/Tab) con ARIA combobox/listbox. `DynamicSelectField`, `EntitySelect`, `CustomerPicker`, `ProductPicker`, `VehiclePicker`, `LineProductCell`, `RelateDocuments`, `DynamicMultiSelectField` y el campo `search` legado del modal genérico pasan a ser configuraciones finas del primitivo (mismos props y exports; los wrappers quedan deprecados en JSDoc). Nuevos exports: `RecordPicker`, `useRecordSearch`, `useLatestSearch`, `useRecordPickerDialog`, `requestRecordCreate`/`requestRecordEdit`, `recordLabel`, `withSearchPrefill`, `OptionLead`/`OptionThumb`, `useProductSearch`/`ProductHitRow`. Las opciones son `role="option"` clicables sin `<button>` anidado (los tests que buscaban ese botón deben hacer click en la opción). Customer/Vehicle pickers suman el lápiz de edición del elegido (gateado por `<model>.update`).
8
+
3
9
  ## 49.5.0
4
10
 
5
11
  ### Minor Changes
@@ -17,7 +17,7 @@ export interface CustomerPickerProps {
17
17
  value: CustomerResult | null;
18
18
  /** Evento único de cambio: cliente elegido/creado, o `null` al limpiar. */
19
19
  onChange: (customer: CustomerResult | null) => void;
20
- /** Búsqueda (nombre, ID fiscal, teléfono, placa, VIN). Estable (useCallback). */
20
+ /** Búsqueda (nombre, ID fiscal, teléfono, placa, VIN). */
21
21
  search: (q: string, signal: AbortSignal) => Promise<CustomerResult[]>;
22
22
  /** Modelo del kernel para el alta rápida. Default `Customer`. */
23
23
  model?: string;
@@ -29,11 +29,17 @@ export interface CustomerPickerProps {
29
29
  validateTaxId?: (taxId: string) => string | undefined;
30
30
  /** Overrides de permiso (default `<model>.create` con useCan). */
31
31
  canCreate?: boolean;
32
+ /** Override de permiso para editar al elegido (default `<model>.update`). */
33
+ canEdit?: boolean;
32
34
  /** Valores iniciales del alta rápida (p. ej. el texto buscado como nombre). */
33
35
  createDefaults?: Record<string, unknown>;
34
36
  currency?: string;
35
37
  disabled?: boolean;
36
38
  placeholder?: string;
37
39
  }
38
- export declare function CustomerPicker({ value, onChange, search, model, endpoint, mapRecord, validateTaxId, canCreate, createDefaults, currency, disabled, placeholder, }: CustomerPickerProps): import("react").JSX.Element;
40
+ /**
41
+ * @deprecated Configuración fina de {@link RecordPicker}; se conserva para no
42
+ * romper consumidores.
43
+ */
44
+ export declare function CustomerPicker({ value, onChange, search, model, endpoint, mapRecord, validateTaxId, canCreate, canEdit, createDefaults, currency, disabled, placeholder, }: CustomerPickerProps): import("react").JSX.Element;
39
45
  //# sourceMappingURL=customer-picker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"customer-picker.d.ts","sourceRoot":"","sources":["../../src/business/customer-picker.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAA;IAC5B,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD,iFAAiF;IACjF,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;IACrE,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,cAAc,CAAA;IAC5D,sFAAsF;IACtF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACrD,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAkB,EAClB,QAAQ,EACR,SAAS,EACT,aAAa,EACb,SAAS,EACT,cAAc,EACd,QAAQ,EACR,QAAQ,EACR,WAAW,GACd,EAAE,mBAAmB,+BAkIrB"}
1
+ {"version":3,"file":"customer-picker.d.ts","sourceRoot":"","sources":["../../src/business/customer-picker.tsx"],"names":[],"mappings":"AAYA,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAA;IAC5B,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD,0DAA0D;IAC1D,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;IACrE,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,cAAc,CAAA;IAC5D,sFAAsF;IACtF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACrD,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAQD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAkB,EAClB,QAAQ,EACR,SAAS,EACT,aAAa,EACb,SAAS,EACT,OAAO,EACP,cAAc,EACd,QAAQ,EACR,QAAQ,EACR,WAAW,GACd,EAAE,mBAAmB,+BAgHrB"}
@@ -1,35 +1,33 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // CustomerPicker + alta rápida — busca por nombre, RFC/ID fiscal, teléfono, placa
3
- // o VIN; muestra saldo, crédito y alertas (benchmark §7). El alta rápida reutiliza
3
+ // o VIN; muestra saldo, crédito y alertas (benchmark §7). Configuración del
4
+ // <RecordPicker> compartido: el alta rápida (y la edición del elegido) reutiliza
4
5
  // el <CreateRecordDialog> dinámico del modelo (formulario desde metadata).
5
6
  import { useCallback, useState } from 'react';
6
7
  import { useTranslation } from 'react-i18next';
7
- import { Plus, Search, X } from 'lucide-react';
8
- import { Badge, Button, Input } from '@asteby/metacore-ui';
9
- import { CreateRecordDialog } from '../dialogs/create-record-dialog';
10
- import { useApi } from '../api-context';
8
+ import { Badge } from '@asteby/metacore-ui';
9
+ import { RecordPicker, useLatestSearch } from '../record-picker';
10
+ import { useRecordPickerDialog } from '../record-picker-dialog';
11
11
  import { useCan } from '../permissions-context';
12
12
  import { useFormatter } from './format';
13
- import { useAsyncSearch } from './use-async-search';
14
- import { EmptyState } from './feedback';
15
13
  function snake(model) {
16
14
  return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
17
15
  }
18
- export function CustomerPicker({ value, onChange, search, model = 'Customer', endpoint, mapRecord, validateTaxId, canCreate, createDefaults, currency, disabled, placeholder, }) {
16
+ const customerLine = (c) => [c.tax_id, c.phone, c.plate, c.vin].filter(Boolean).join(' · ');
17
+ /**
18
+ * @deprecated Configuración fina de {@link RecordPicker}; se conserva para no
19
+ * romper consumidores.
20
+ */
21
+ export function CustomerPicker({ value, onChange, search, model = 'Customer', endpoint, mapRecord, validateTaxId, canCreate, canEdit, createDefaults, currency, disabled, placeholder, }) {
19
22
  const { t } = useTranslation();
20
- const api = useApi();
21
23
  const can = useCan();
22
24
  const fmt = useFormatter({ currency });
23
25
  const [text, setText] = useState('');
24
- const [creating, setCreating] = useState(false);
25
- const { results, loading, error } = useAsyncSearch(text, search, { minChars: 2 });
26
+ const [open, setOpen] = useState(false);
27
+ const searchFn = useLatestSearch(search);
26
28
  const mayCreate = canCreate ?? can(`${snake(model)}.create`);
27
- const base = endpoint ?? `/data/${model}/me`;
29
+ const mayEdit = canEdit ?? can(`${snake(model)}.update`);
28
30
  const taxIdError = value?.tax_id && validateTaxId ? validateTaxId(value.tax_id) : undefined;
29
- const pick = (c) => {
30
- onChange(c);
31
- setText('');
32
- };
33
31
  const toResult = useCallback((rec) => mapRecord
34
32
  ? mapRecord(rec)
35
33
  : {
@@ -38,14 +36,19 @@ export function CustomerPicker({ value, onChange, search, model = 'Customer', en
38
36
  tax_id: typeof rec.tax_id === 'string' ? rec.tax_id : undefined,
39
37
  phone: typeof rec.phone === 'string' ? rec.phone : undefined,
40
38
  }, [mapRecord]);
41
- if (value) {
42
- const overLimit = value.credit_limit != null && (value.balance ?? 0) > value.credit_limit;
43
- return (_jsxs("div", { "data-slot": "customer-picker", className: "space-y-1 rounded-md border p-2", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium", children: value.name }), _jsx("p", { className: "text-xs text-muted-foreground", children: [value.tax_id, value.phone, value.plate, value.vin].filter(Boolean).join(' · ') })] }), !disabled && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => onChange(null), "aria-label": t('customerPicker.clear', { defaultValue: 'Quitar cliente' }), children: _jsx(X, { className: "size-4" }) }))] }), _jsxs("div", { className: "flex flex-wrap gap-1.5", children: [value.balance != null && value.balance !== 0 && (_jsxs(Badge, { variant: overLimit ? 'danger' : 'warning', children: [t('customerPicker.balance', { defaultValue: 'Adeudo' }), " ", fmt.money(value.balance)] })), value.credit_limit != null && (_jsxs(Badge, { variant: "muted", children: [t('customerPicker.credit', { defaultValue: 'Crédito' }), " ", fmt.money(value.credit_limit)] })), value.alerts?.map((a) => (_jsx(Badge, { variant: "danger", children: a }, a)))] }), taxIdError && _jsx("p", { role: "alert", className: "text-xs text-destructive", children: taxIdError })] }));
44
- }
45
- return (_jsxs("div", { "data-slot": "customer-picker", className: "space-y-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { className: "pl-8", disabled: disabled, value: text, placeholder: placeholder ?? t('customerPicker.placeholder', { defaultValue: 'Nombre, RFC, teléfono, placa o VIN' }), onChange: (e) => setText(e.target.value) })] }), mayCreate && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: () => setCreating(true), "aria-label": t('customerPicker.create', { defaultValue: 'Nuevo cliente' }), children: _jsx(Plus, { className: "size-4" }) }))] }), loading && _jsx("p", { className: "text-sm text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) }), error && !loading && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: t('customerPicker.error', { defaultValue: 'No se pudo buscar clientes. Inténtalo de nuevo.' }) })), !loading && !error && text.trim().length >= 2 && results.length === 0 && (_jsx(EmptyState, { title: t('customerPicker.empty', { defaultValue: 'No encontramos a ese cliente' }), action: mayCreate ? { label: t('customerPicker.createAction', { defaultValue: 'Crear cliente' }), onClick: () => setCreating(true) } : undefined })), results.length > 0 && (_jsx("ul", { role: "listbox", className: "max-h-64 divide-y overflow-auto rounded-md border", children: results.map((c) => (_jsx("li", { role: "option", "aria-selected": false, children: _jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-2 px-2 py-1.5 text-left hover:bg-accent", onClick: () => pick(c), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: c.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: [c.tax_id, c.phone, c.plate, c.vin].filter(Boolean).join(' · ') })] }), c.balance != null && c.balance > 0 && _jsx(Badge, { variant: "warning", children: fmt.money(c.balance) })] }) }, c.id))) })), creating && (_jsx(CreateRecordDialog, { modelKey: model, open: creating, onOpenChange: setCreating, endpoint: base, defaults: createDefaults ?? (text.trim() ? { name: text.trim() } : undefined), onCreate: async (data) => {
46
- const res = await api.post(base, data);
47
- const rec = (res.data?.data ?? res.data);
48
- pick(toResult(rec));
49
- return rec.id != null ? { id: String(rec.id) } : undefined;
50
- } }))] }));
39
+ const { openCreate, openEdit, dialog } = useRecordPickerDialog({
40
+ model,
41
+ endpoint,
42
+ prefillField: 'name',
43
+ createDefaults,
44
+ onSaved: (rec, kind) => {
45
+ const next = toResult(rec);
46
+ // Al editar se conservan saldo/crédito/alertas que el registro no trae.
47
+ onChange(kind === 'edit' && value ? { ...value, ...next } : next);
48
+ setText('');
49
+ },
50
+ });
51
+ const overLimit = !!value && value.credit_limit != null && (value.balance ?? 0) > value.credit_limit;
52
+ const below = value ? (_jsxs(_Fragment, { children: [(value.balance != null && value.balance !== 0) || value.credit_limit != null || value.alerts?.length ? (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [value.balance != null && value.balance !== 0 && (_jsxs(Badge, { variant: overLimit ? 'danger' : 'warning', children: [t('customerPicker.balance', { defaultValue: 'Adeudo' }), " ", fmt.money(value.balance)] })), value.credit_limit != null && (_jsxs(Badge, { variant: "muted", children: [t('customerPicker.credit', { defaultValue: 'Crédito' }), " ", fmt.money(value.credit_limit)] })), value.alerts?.map((a) => (_jsx(Badge, { variant: "danger", children: a }, a)))] })) : null, taxIdError && _jsx("p", { role: "alert", className: "text-xs text-destructive", children: taxIdError })] })) : null;
53
+ return (_jsxs(_Fragment, { children: [_jsx(RecordPicker, { trigger: "input", slot: "customer-picker", search: searchFn, minChars: 2, query: text, onQueryChange: setText, open: open, onOpenChange: setOpen, getKey: (c) => c.id, getLabel: (c) => c.name, renderItem: (c) => (_jsxs("span", { className: "flex w-full min-w-0 items-center justify-between gap-2", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate font-medium", children: c.name }), _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: customerLine(c) })] }), c.balance != null && c.balance > 0 && _jsx(Badge, { variant: "warning", children: fmt.money(c.balance) })] })), value: value?.id ?? null, selected: value, onSelect: (c) => onChange(c), onClear: disabled ? undefined : () => onChange(null), clearLabel: t('customerPicker.clear', { defaultValue: 'Quitar cliente' }), renderValue: (c) => c ? (_jsxs("span", { className: "block min-w-0", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: c.name }), customerLine(c) && _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: customerLine(c) })] })) : null, below: below, disabled: disabled, placeholder: placeholder ?? t('customerPicker.placeholder', { defaultValue: 'Nombre, RFC, teléfono, placa o VIN' }), loadingText: t('common.searching', { defaultValue: 'Buscando…' }), errorText: t('customerPicker.error', { defaultValue: 'No se pudo buscar clientes. Inténtalo de nuevo.' }), emptyText: t('customerPicker.empty', { defaultValue: 'No encontramos a ese cliente' }), entityLabel: t('customerPicker.entity', { defaultValue: 'cliente' }), createLabel: t('customerPicker.create', { defaultValue: 'Nuevo cliente' }), editLabel: t('customerPicker.edit', { defaultValue: 'Editar cliente' }), createFooter: "empty", createFooterLabel: () => t('customerPicker.createAction', { defaultValue: 'Crear cliente' }), onCreate: mayCreate ? (q) => openCreate(q) : undefined, onEdit: mayEdit && value ? () => openEdit(value.id) : undefined }), dialog] }));
51
54
  }
@@ -11,6 +11,7 @@ export * from './refund';
11
11
  export * from './refund-destination';
12
12
  export * from './product-search';
13
13
  export * from './product-picker';
14
+ export * from './product-options';
14
15
  export * from './customer-picker';
15
16
  export * from './catalog';
16
17
  export * from './return';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA"}
@@ -11,6 +11,7 @@ export * from './refund';
11
11
  export * from './refund-destination';
12
12
  export * from './product-search';
13
13
  export * from './product-picker';
14
+ export * from './product-options';
14
15
  export * from './customer-picker';
15
16
  export * from './catalog';
16
17
  export * from './return';
@@ -1,12 +1,9 @@
1
1
  import { type KeyboardEvent } from 'react';
2
- import { type ProductQuery, type ProductResult, type ProductVariant } from './product-search';
3
- export type LineProductSearch = (query: Exclude<ProductQuery, {
4
- kind: 'empty';
5
- }>, signal: AbortSignal) => Promise<ProductResult[]>;
6
- export interface LineProductHit {
7
- product: ProductResult;
8
- variant?: ProductVariant;
9
- }
2
+ import { type ProductHit, type ProductSearchFn } from './product-options';
3
+ import type { ProductResult, ProductVariant } from './product-search';
4
+ export type LineProductSearch = ProductSearchFn;
5
+ /** @deprecated Alias de {@link ProductHit}. */
6
+ export type LineProductHit = ProductHit;
10
7
  export interface LineProductCellProps {
11
8
  search: LineProductSearch;
12
9
  /** Texto de la celda (la descripción del renglón, o el borrador del renglón vacío). */
@@ -14,8 +11,9 @@ export interface LineProductCellProps {
14
11
  onTextChange: (text: string) => void;
15
12
  onPick: (product: ProductResult, variant?: ProductVariant) => void;
16
13
  /**
17
- * Teclas que el buscador no consume (lista cerrada): el grid aplica sus
18
- * comandos (Enter agrega, Supr en celda vacía borra…).
14
+ * Teclas que el buscador no consume (lista cerrada, Enter sin
15
+ * coincidencias…): el grid aplica sus comandos (Enter agrega, Supr en
16
+ * celda vacía borra…).
19
17
  */
20
18
  onKeyDownClosed?: (e: KeyboardEvent<HTMLInputElement>) => void;
21
19
  warehouseId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"line-product-cell.d.ts","sourceRoot":"","sources":["../../src/business/line-product-cell.tsx"],"names":[],"mappings":"AASA,OAAO,EAAgD,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAOxF,OAAO,EAAqC,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEhI,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;AAElI,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,aAAa,CAAA;IACtB,OAAO,CAAC,EAAE,cAAc,CAAA;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,iBAAiB,CAAA;IACzB,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IAClE;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAA;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,eAAe,mHA2K1B,CAAA"}
1
+ {"version":3,"file":"line-product-cell.d.ts","sourceRoot":"","sources":["../../src/business/line-product-cell.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAGhE,OAAO,EAKH,KAAK,UAAU,EACf,KAAK,eAAe,EACvB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAErE,MAAM,MAAM,iBAAiB,GAAG,eAAe,CAAA;AAE/C,+CAA+C;AAC/C,MAAM,MAAM,cAAc,GAAG,UAAU,CAAA;AAEvC,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,iBAAiB,CAAA;IACzB,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IAClE;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAA;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,eAAe,mHA0C1B,CAAA"}
@@ -1,100 +1,23 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  // LineProductCell — buscador de producto DENTRO de la celda «Descripción» de un
3
- // renglón (patrón de editor de facturas pro: Odoo/Holded/Linear). Ocupa solo su
4
- // celda; la lista de resultados va en un portal (Popover de Radix anclado al
5
- // input): nunca la recorta el overflow de la tabla ni del modal, queda por
6
- // encima del diálogo y se abre hacia arriba si abajo no hay espacio.
3
+ // renglón (patrón de editor de facturas pro: Odoo/Holded/Linear). Es una
4
+ // configuración del <RecordPicker> compartido (trigger «input», texto libre,
5
+ // variante celda): ocupa solo su celda y la lista va en un portal — nunca la
6
+ // recorta el overflow de la tabla ni del modal, queda por encima del diálogo y
7
+ // se abre hacia arriba si abajo no hay espacio.
7
8
  //
8
9
  // Teclado: ↑/↓ recorren, Enter elige (el grid pasa el foco a «Cant.»), Esc
9
10
  // cierra, Tab cierra y avanza normal. El texto escrito sin elegir queda como
10
11
  // descripción libre (el renglón libre sigue funcionando igual).
11
- import { forwardRef, useId, useMemo, useRef, useState } from 'react';
12
+ import { forwardRef, useState } from 'react';
12
13
  import { useTranslation } from 'react-i18next';
13
- import { Loader2, Search } from 'lucide-react';
14
- import { Badge, Input, Popover, PopoverAnchor, PopoverContent } from '@asteby/metacore-ui/primitives';
15
- import { useFormatter } from './format';
16
- import { useAsyncSearch } from './use-async-search';
17
- import { useDebouncedValue } from '../use-debounced-value';
18
- import { availableStock, parseProductQuery } from './product-search';
14
+ import { RecordPicker } from '../record-picker';
15
+ import { ProductHitRow, productHitKey, productHitLabel, useProductSearch, } from './product-options';
19
16
  export const LineProductCell = forwardRef(function LineProductCell({ search, text, onTextChange, onPick, onKeyDownClosed, warehouseId, currency, placeholder, ariaLabel, disabled, invalid, className, dataCell }, ref) {
20
17
  const { t } = useTranslation();
21
- const fmt = useFormatter({ currency });
22
- const listId = useId();
23
- const anchorRef = useRef(null);
24
- // Solo se abre mientras se escribe: enfocar un renglón libre ya capturado no
25
- // despliega «sin resultados» encima de la tabla.
18
+ // Solo se busca/abre mientras se escribe: enfocar un renglón libre ya
19
+ // capturado no despliega «sin resultados» encima de la tabla.
26
20
  const [typing, setTyping] = useState(false);
27
- const [active, setActive] = useState(0);
28
- const parsed = parseProductQuery(text);
29
- const run = useMemo(() => (q, signal) => {
30
- const p = parseProductQuery(q);
31
- return p.kind === 'empty' ? Promise.resolve([]) : search(p, signal);
32
- }, [search]);
33
- const minChars = parsed.kind === 'barcode' ? 8 : 2;
34
- const delay = parsed.kind === 'barcode' ? 0 : 250;
35
- const { results, loading: fetching } = useAsyncSearch(typing ? text : '', run, { minChars, delay });
36
- // Mientras el debounce no alcanza lo escrito, la lista dice «Buscando…» y no
37
- // un «sin coincidencias» prematuro.
38
- const settled = useDebouncedValue(text.trim(), delay) === text.trim();
39
- const loading = fetching || !settled;
40
- const flat = useMemo(() => results.flatMap((p) => (p.variants?.length ?? 0) > 0 ? p.variants.map((v) => ({ product: p, variant: v })) : [{ product: p }]), [results]);
41
- const queryReady = typing && text.trim().length >= minChars;
42
- const open = queryReady && (loading || flat.length > 0 || text.trim().length >= 2);
43
- const hasHits = flat.length > 0;
44
- const close = () => {
45
- setTyping(false);
46
- setActive(0);
47
- };
48
- const choose = (idx) => {
49
- const hit = flat[idx];
50
- if (!hit)
51
- return;
52
- close();
53
- onPick(hit.product, hit.variant);
54
- };
55
- const onKeyDown = (e) => {
56
- if (open && hasHits) {
57
- if (e.key === 'ArrowDown') {
58
- e.preventDefault();
59
- setActive((n) => Math.min(flat.length - 1, n + 1));
60
- return;
61
- }
62
- if (e.key === 'ArrowUp') {
63
- e.preventDefault();
64
- setActive((n) => Math.max(0, n - 1));
65
- return;
66
- }
67
- if (e.key === 'Enter') {
68
- e.preventDefault();
69
- choose(active);
70
- return;
71
- }
72
- }
73
- if (open && e.key === 'Escape') {
74
- // Cierra la lista sin cerrar el diálogo que la contiene.
75
- e.preventDefault();
76
- e.stopPropagation();
77
- close();
78
- return;
79
- }
80
- if (e.key === 'Tab')
81
- close();
82
- onKeyDownClosed?.(e);
83
- };
84
- return (_jsxs(Popover, { open: open, onOpenChange: (o) => !o && close(), children: [_jsx(PopoverAnchor, { asChild: true, children: _jsxs("div", { ref: anchorRef, className: "relative w-full min-w-0", "data-slot": "line-product-cell", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { ref: ref, value: text, disabled: disabled, role: "combobox", "aria-autocomplete": "list", "aria-expanded": open, "aria-controls": open ? listId : undefined, "aria-activedescendant": open && hasHits ? `${listId}-${active}` : undefined, "aria-label": ariaLabel, "aria-invalid": invalid || undefined, autoComplete: "off", "data-cell": dataCell, placeholder: placeholder ?? t('lineItems.searchPlaceholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), className: 'h-8 pl-8 ' + (className ?? ''), onChange: (e) => {
85
- onTextChange(e.target.value);
86
- setTyping(true);
87
- setActive(0);
88
- }, onBlur: () => close(), onKeyDown: onKeyDown })] }) }), _jsx(PopoverContent, { align: "start", sideOffset: 4, collisionPadding: 8, className: "p-1", "data-slot": "line-product-results",
89
- // El foco se queda en la celda: se sigue escribiendo mientras se ve la lista.
90
- onOpenAutoFocus: (e) => e.preventDefault(), onCloseAutoFocus: (e) => e.preventDefault(), onInteractOutside: (e) => {
91
- if (anchorRef.current?.contains(e.target))
92
- e.preventDefault();
93
- }, style: { width: 'max(var(--radix-popover-trigger-width), 22rem)', maxWidth: 'calc(100vw - 1rem)' }, children: hasHits ? (_jsx("ul", { id: listId, role: "listbox", "aria-label": ariaLabel, className: "max-h-72 overflow-auto", children: flat.map((hit, idx) => {
94
- const price = hit.variant?.price ?? hit.product.price;
95
- const stock = availableStock(hit.variant ?? hit.product, warehouseId);
96
- const meta = [hit.variant?.sku ?? hit.product.sku, hit.product.tire?.normalized].filter(Boolean).join(' · ');
97
- return (_jsx("li", { id: `${listId}-${idx}`, role: "option", "aria-selected": idx === active, children: _jsxs("button", { type: "button", tabIndex: -1, className: 'flex w-full items-center justify-between gap-3 rounded-sm px-2 py-1.5 text-left text-sm outline-none hover:bg-accent hover:text-accent-foreground ' +
98
- (idx === active ? 'bg-accent text-accent-foreground' : ''), onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => setActive(idx), onClick: () => choose(idx), children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate font-medium", children: hit.variant ? `${hit.product.name} · ${hit.variant.label}` : hit.product.name }), meta && _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: meta })] }), _jsxs("span", { className: "flex shrink-0 items-center gap-2", children: [stock != null && (_jsx(Badge, { variant: "secondary", className: "font-normal tabular-nums", children: stock })), price != null && _jsx("span", { className: "tabular-nums", children: fmt.money(price) })] })] }) }, `${hit.product.id}:${hit.variant?.id ?? ''}`));
99
- }) })) : loading ? (_jsxs("p", { className: "flex items-center justify-center gap-2 py-4 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }), t('common.searching', { defaultValue: 'Buscando…' })] })) : (_jsx("p", { className: "px-2 py-3 text-sm text-muted-foreground", children: t('lineItems.noProduct', { defaultValue: 'Sin coincidencias. Se usará como descripción libre.' }) })) })] }));
21
+ const { hits, loading, minChars } = useProductSearch(typing ? text : '', search, typing);
22
+ return (_jsx(RecordPicker, { trigger: "input", variant: "cell", freeText: true, anchorSlot: "line-product-cell", contentSlot: "line-product-results", minListWidth: "22rem", items: hits, loading: loading, minChars: minChars, query: text, onQueryChange: onTextChange, open: typing, onOpenChange: setTyping, getKey: productHitKey, getLabel: productHitLabel, renderItem: (hit) => _jsx(ProductHitRow, { hit: hit, warehouseId: warehouseId, currency: currency, compact: true }), onSelect: (hit) => onPick(hit.product, hit.variant), onUnhandledKeyDown: onKeyDownClosed, inputRef: ref, ariaLabel: ariaLabel, listLabel: ariaLabel, disabled: disabled, invalid: invalid, triggerClassName: className, triggerProps: { 'data-cell': dataCell }, placeholder: placeholder ?? t('lineItems.searchPlaceholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), loadingText: t('common.searching', { defaultValue: 'Buscando…' }), emptyText: t('lineItems.noProduct', { defaultValue: 'Sin coincidencias. Se usará como descripción libre.' }) }));
100
23
  });
@@ -0,0 +1,41 @@
1
+ import { type ProductQuery, type ProductResult, type ProductVariant } from './product-search';
2
+ /** A pickable row: a product, or one concrete variant of it. */
3
+ export interface ProductHit {
4
+ product: ProductResult;
5
+ variant?: ProductVariant;
6
+ }
7
+ export type ProductSearchFn = (query: Exclude<ProductQuery, {
8
+ kind: 'empty';
9
+ }>, signal: AbortSignal) => Promise<ProductResult[]>;
10
+ /** Products with variants expand to one row per variant. */
11
+ export declare function flattenProductHits(results: readonly ProductResult[]): ProductHit[];
12
+ export declare const productHitKey: (h: ProductHit) => string;
13
+ export declare const productHitLabel: (h: ProductHit) => string;
14
+ export declare const productHitMeta: (h: ProductHit) => string;
15
+ /**
16
+ * Classified product search: a barcode is exact (8+ digits, no debounce), the
17
+ * rest waits for 2 characters and 250ms. `search` needn't be memoized.
18
+ */
19
+ export declare function useProductSearch(text: string, search: ProductSearchFn, enabled?: boolean): {
20
+ parsed: ProductQuery;
21
+ results: ProductResult[];
22
+ hits: ProductHit[];
23
+ loading: boolean;
24
+ error: boolean;
25
+ minChars: number;
26
+ };
27
+ /** Stock + price at the right of a product row. `compact` → bare stock count (table cell). */
28
+ export declare function ProductHitTrailing({ hit, warehouseId, currency, compact, }: {
29
+ hit: ProductHit;
30
+ warehouseId?: string;
31
+ currency?: string;
32
+ compact?: boolean;
33
+ }): import("react").JSX.Element;
34
+ /** Option body: bold name (· variant), muted meta line, stock/price right. */
35
+ export declare function ProductHitRow({ hit, warehouseId, currency, compact }: {
36
+ hit: ProductHit;
37
+ warehouseId?: string;
38
+ currency?: string;
39
+ compact?: boolean;
40
+ }): import("react").JSX.Element;
41
+ //# sourceMappingURL=product-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-options.d.ts","sourceRoot":"","sources":["../../src/business/product-options.tsx"],"names":[],"mappings":"AAUA,OAAO,EAGH,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACtB,MAAM,kBAAkB,CAAA;AAEzB,gEAAgE;AAChE,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,aAAa,CAAA;IACtB,OAAO,CAAC,EAAE,cAAc,CAAA;CAC3B;AAED,MAAM,MAAM,eAAe,GAAG,CAC1B,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,EAC/C,MAAM,EAAE,WAAW,KAClB,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;AAE7B,4DAA4D;AAC5D,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,UAAU,EAAE,CAIlF;AAED,eAAO,MAAM,aAAa,GAAI,GAAG,UAAU,KAAG,MAAkD,CAAA;AAChG,eAAO,MAAM,eAAe,GAAI,GAAG,UAAU,KAAG,MACyB,CAAA;AACzE,eAAO,MAAM,cAAc,GAAI,GAAG,UAAU,KAAG,MAO3B,CAAA;AAEpB;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,UAAO;;;;;;;EAerF;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAAC,EAC/B,GAAG,EACH,WAAW,EACX,QAAQ,EACR,OAAe,GAClB,EAAE;IACC,GAAG,EAAE,UAAU,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,+BAsBA;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;IAAE,GAAG,EAAE,UAAU,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,+BAarJ"}
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Product options for <RecordPicker>: one search (products, tires, variants)
3
+ // and one option row (name · variant, SKU / tire size / supplier code, stock and
4
+ // price on the right) shared by ProductPicker and the DocumentEditor's
5
+ // LineProductCell — so both product pickers are configurations of the same
6
+ // primitive instead of two hand-rolled lists.
7
+ import { useMemo } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { Badge } from '@asteby/metacore-ui/primitives';
10
+ import { useLatestSearch, useRecordSearch } from '../record-picker';
11
+ import { useFormatter } from './format';
12
+ import { availableStock, parseProductQuery, } from './product-search';
13
+ /** Products with variants expand to one row per variant. */
14
+ export function flattenProductHits(results) {
15
+ return results.flatMap((p) => (p.variants?.length ?? 0) > 0 ? p.variants.map((v) => ({ product: p, variant: v })) : [{ product: p }]);
16
+ }
17
+ export const productHitKey = (h) => `${h.product.id}:${h.variant?.id ?? ''}`;
18
+ export const productHitLabel = (h) => h.variant ? `${h.product.name} · ${h.variant.label}` : h.product.name;
19
+ export const productHitMeta = (h) => [
20
+ h.variant?.sku ?? h.product.sku,
21
+ h.product.tire?.normalized,
22
+ h.product.supplier_sku && `Prov. ${h.product.supplier_sku}`,
23
+ ]
24
+ .filter(Boolean)
25
+ .join(' · ');
26
+ /**
27
+ * Classified product search: a barcode is exact (8+ digits, no debounce), the
28
+ * rest waits for 2 characters and 250ms. `search` needn't be memoized.
29
+ */
30
+ export function useProductSearch(text, search, enabled = true) {
31
+ const stable = useLatestSearch(search);
32
+ const parsed = parseProductQuery(text);
33
+ const run = useMemo(() => (q, signal) => {
34
+ const p = parseProductQuery(q);
35
+ return p.kind === 'empty' ? Promise.resolve([]) : stable(p, signal);
36
+ }, [stable]);
37
+ const minChars = parsed.kind === 'barcode' ? 8 : 2;
38
+ const delay = parsed.kind === 'barcode' ? 0 : 250;
39
+ const { results, loading, error, pending } = useRecordSearch(text, run, { minChars, delay, enabled });
40
+ const hits = useMemo(() => flattenProductHits(results), [results]);
41
+ return { parsed, results, hits, loading: loading || pending, error, minChars };
42
+ }
43
+ /** Stock + price at the right of a product row. `compact` → bare stock count (table cell). */
44
+ export function ProductHitTrailing({ hit, warehouseId, currency, compact = false, }) {
45
+ const { t } = useTranslation();
46
+ const fmt = useFormatter({ currency });
47
+ const price = hit.variant?.price ?? hit.product.price;
48
+ const stock = availableStock(hit.variant ?? hit.product, warehouseId);
49
+ return (_jsxs(_Fragment, { children: [stock != null &&
50
+ (compact ? (_jsx(Badge, { variant: "secondary", className: "font-normal tabular-nums", children: stock })) : (_jsx(Badge, { variant: stock > 0 ? 'success' : 'danger', children: stock > 0
51
+ ? t('productPicker.inStock', { defaultValue: '{{n}} disp.', n: stock })
52
+ : t('productPicker.outOfStock', { defaultValue: 'Sin existencia' }) }))), price != null && _jsx("span", { className: "tabular-nums", children: fmt.money(price) })] }));
53
+ }
54
+ /** Option body: bold name (· variant), muted meta line, stock/price right. */
55
+ export function ProductHitRow({ hit, warehouseId, currency, compact }) {
56
+ const meta = productHitMeta(hit);
57
+ return (_jsxs("span", { className: "flex w-full min-w-0 items-center justify-between gap-3", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate font-medium", children: productHitLabel(hit) }), meta && _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: meta })] }), _jsx("span", { className: "flex shrink-0 items-center gap-2", children: _jsx(ProductHitTrailing, { hit: hit, warehouseId: warehouseId, currency: currency, compact: compact }) })] }));
58
+ }
@@ -2,7 +2,7 @@ import { type ProductQuery, type ProductResult, type ProductVariant } from './pr
2
2
  export interface ProductPickerProps {
3
3
  /**
4
4
  * Búsqueda: recibe la consulta ya clasificada (`barcode` | `tire_size` |
5
- * `text`) para que el backend elija el índice. Debe ser estable (useCallback).
5
+ * `text`) para que el backend elija el índice.
6
6
  */
7
7
  search: (query: Exclude<ProductQuery, {
8
8
  kind: 'empty';
@@ -20,5 +20,9 @@ export interface ProductPickerProps {
20
20
  currency?: string;
21
21
  disabled?: boolean;
22
22
  }
23
+ /**
24
+ * @deprecated Configuración fina de {@link RecordPicker}; para pantallas nuevas
25
+ * usa RecordPicker con `useProductSearch` / `ProductHitRow`.
26
+ */
23
27
  export declare function ProductPicker({ search, onSelect, warehouseId, autoSelectBarcode, allowOutOfStock, placeholder, autoFocus, currency, disabled, }: ProductPickerProps): import("react").JSX.Element;
24
28
  //# sourceMappingURL=product-picker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"product-picker.d.ts","sourceRoot":"","sources":["../../src/business/product-picker.tsx"],"names":[],"mappings":"AAQA,OAAO,EAGH,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACtB,MAAM,kBAAkB,CAAA;AAGzB,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;IAC1G,8DAA8D;IAC9D,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IACpE,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mGAAmG;IACnG,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,QAAQ,EACR,WAAW,EACX,iBAAwB,EACxB,eAAsB,EACtB,WAAW,EACX,SAAS,EACT,QAAQ,EACR,QAAQ,GACX,EAAE,kBAAkB,+BA2HpB"}
1
+ {"version":3,"file":"product-picker.d.ts","sourceRoot":"","sources":["../../src/business/product-picker.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAkB,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE7G,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;IAC1G,8DAA8D;IAC9D,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IACpE,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mGAAmG;IACnG,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,QAAQ,EACR,WAAW,EACX,iBAAwB,EACxB,eAAsB,EACtB,WAAW,EACX,SAAS,EACT,QAAQ,EACR,QAAQ,GACX,EAAE,kBAAkB,+BA+DpB"}
@@ -1,48 +1,39 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // ProductPicker — una sola búsqueda para productos, llantas y variantes
3
3
  // (medida «205/55R16», SKU, código de barras, clave de proveedor). Benchmark §7.
4
- import { useCallback, useState } from 'react';
4
+ // Configuración del <RecordPicker> compartido (mismas filas que la celda de
5
+ // producto del DocumentEditor, ver product-options.tsx).
6
+ import { useState } from 'react';
5
7
  import { useTranslation } from 'react-i18next';
6
- import { Search } from 'lucide-react';
7
- import { Badge, Input } from '@asteby/metacore-ui';
8
- import { useFormatter } from './format';
9
- import { useAsyncSearch } from './use-async-search';
10
- import { availableStock, parseProductQuery, } from './product-search';
11
- import { EmptyState } from './feedback';
8
+ import { RecordPicker } from '../record-picker';
9
+ import { ProductHitRow, productHitKey, productHitLabel, useProductSearch } from './product-options';
10
+ import { availableStock } from './product-search';
11
+ /**
12
+ * @deprecated Configuración fina de {@link RecordPicker}; para pantallas nuevas
13
+ * usa RecordPicker con `useProductSearch` / `ProductHitRow`.
14
+ */
12
15
  export function ProductPicker({ search, onSelect, warehouseId, autoSelectBarcode = true, allowOutOfStock = true, placeholder, autoFocus, currency, disabled, }) {
13
16
  const { t } = useTranslation();
14
- const fmt = useFormatter({ currency });
15
17
  const [text, setText] = useState('');
16
- const parsed = parseProductQuery(text);
17
- // Un código de barras es exacto: no se espera a 2 caracteres ni se debouncea mucho.
18
- const run = useCallback((q, signal) => {
19
- const p = parseProductQuery(q);
20
- return p.kind === 'empty' ? Promise.resolve([]) : search(p, signal);
21
- }, [search]);
22
- const { results, loading, error } = useAsyncSearch(text, run, {
23
- minChars: parsed.kind === 'barcode' ? 8 : 2,
24
- delay: parsed.kind === 'barcode' ? 0 : 250,
25
- });
18
+ const [open, setOpen] = useState(false);
19
+ const { parsed, results, hits, loading, error, minChars } = useProductSearch(text, search);
20
+ const outOfStock = (h) => {
21
+ const stock = availableStock(h.variant ?? h.product, warehouseId);
22
+ return !allowOutOfStock && stock != null && stock <= 0;
23
+ };
26
24
  const pick = (p, v) => {
27
- const stock = availableStock(v ?? p, warehouseId);
28
- if (!allowOutOfStock && stock != null && stock <= 0)
25
+ if (outOfStock({ product: p, variant: v }))
29
26
  return;
30
27
  onSelect(p, v);
31
28
  setText('');
32
29
  };
33
- return (_jsxs("div", { "data-slot": "product-picker", className: "space-y-2", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { autoFocus: autoFocus, disabled: disabled, className: "pl-8", value: text, placeholder: placeholder ?? t('productPicker.placeholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
34
- if (e.key === 'Enter' && autoSelectBarcode && parsed.kind === 'barcode' && results.length === 1) {
35
- e.preventDefault();
36
- const only = results[0];
37
- const exact = only.variants?.find((v) => v.barcode === parsed.barcode);
38
- pick(only, exact);
39
- }
40
- } })] }), parsed.kind === 'tire_size' && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [t('productPicker.tireSize', { defaultValue: 'Medida de llanta' }), ": ", parsed.tire.normalized] })), loading && _jsx("p", { className: "text-sm text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) }), error && !loading && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: t('productPicker.error', { defaultValue: 'No se pudo buscar. Revisa tu conexión e inténtalo de nuevo.' }) })), !loading && !error && parsed.kind !== 'empty' && text.trim().length >= 2 && results.length === 0 && (_jsx(EmptyState, { title: t('productPicker.empty', { defaultValue: 'No encontramos ese producto' }), description: t('productPicker.emptyHint', { defaultValue: 'Prueba con otra medida, SKU o código de barras.' }) })), results.length > 0 && (_jsx("ul", { role: "listbox", className: "max-h-72 divide-y overflow-auto rounded-md border", children: results.map((p) => {
41
- const stock = availableStock(p, warehouseId);
42
- const hasVariants = (p.variants?.length ?? 0) > 0;
43
- return (_jsxs("li", { role: "option", "aria-selected": false, className: "px-2 py-1.5", children: [_jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-2 text-left disabled:opacity-50", disabled: hasVariants || (!allowOutOfStock && stock != null && stock <= 0), onClick: () => pick(p), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: p.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: [p.sku, p.tire?.normalized, p.supplier_sku && `Prov. ${p.supplier_sku}`].filter(Boolean).join(' · ') })] }), _jsxs("span", { className: "flex items-center gap-2", children: [stock != null && (_jsx(Badge, { variant: stock > 0 ? 'success' : 'danger', children: stock > 0 ? t('productPicker.inStock', { defaultValue: '{{n}} disp.', n: stock }) : t('productPicker.outOfStock', { defaultValue: 'Sin existencia' }) })), p.price != null && !hasVariants && _jsx("span", { className: "text-sm tabular-nums", children: fmt.money(p.price) })] })] }), hasVariants && (_jsx("ul", { className: "mt-1 space-y-0.5 pl-3", children: p.variants.map((v) => {
44
- const vs = availableStock(v, warehouseId);
45
- return (_jsx("li", { children: _jsxs("button", { type: "button", className: "flex w-full items-center justify-between gap-2 rounded px-1 py-0.5 text-left text-sm hover:bg-accent disabled:opacity-50", disabled: !allowOutOfStock && vs != null && vs <= 0, onClick: () => pick(p, v), children: [_jsx("span", { children: v.label }), _jsxs("span", { className: "flex items-center gap-2", children: [vs != null && _jsx(Badge, { variant: vs > 0 ? 'success' : 'danger', children: vs }), (v.price ?? p.price) != null && _jsx("span", { className: "tabular-nums", children: fmt.money(v.price ?? p.price) })] })] }) }, v.id));
46
- }) }))] }, p.id));
47
- }) }))] }));
30
+ return (_jsx(RecordPicker, { trigger: "input", slot: "product-picker", items: hits, loading: loading, error: error, minChars: minChars, query: text, onQueryChange: setText, open: open, onOpenChange: setOpen, getKey: productHitKey, getLabel: productHitLabel, renderItem: (hit) => _jsx(ProductHitRow, { hit: hit, warehouseId: warehouseId, currency: currency }), isItemDisabled: outOfStock, onSelect: (hit) => pick(hit.product, hit.variant), onInputKeyDown: (e) => {
31
+ // Código de barras exacto con un único resultado: Enter lo elige.
32
+ if (e.key === 'Enter' && autoSelectBarcode && parsed.kind === 'barcode' && results.length === 1) {
33
+ e.preventDefault();
34
+ const only = results[0];
35
+ pick(only, only.variants?.find((v) => v.barcode === parsed.barcode));
36
+ setOpen(false);
37
+ }
38
+ }, disabled: disabled, triggerProps: { autoFocus }, minListWidth: "22rem", placeholder: placeholder ?? t('productPicker.placeholder', { defaultValue: 'Producto, medida (205/55R16), SKU o código de barras' }), loadingText: t('common.searching', { defaultValue: 'Buscando…' }), errorText: t('productPicker.error', { defaultValue: 'No se pudo buscar. Revisa tu conexión e inténtalo de nuevo.' }), emptyText: _jsxs("span", { className: "block space-y-0.5", children: [_jsx("span", { className: "block font-medium text-foreground", children: t('productPicker.empty', { defaultValue: 'No encontramos ese producto' }) }), _jsx("span", { className: "block text-xs", children: t('productPicker.emptyHint', { defaultValue: 'Prueba con otra medida, SKU o código de barras.' }) })] }), below: parsed.kind === 'tire_size' ? (_jsxs("p", { className: "text-xs text-muted-foreground", children: [t('productPicker.tireSize', { defaultValue: 'Medida de llanta' }), ": ", parsed.tire.normalized] })) : null }));
48
39
  }
@@ -26,7 +26,7 @@ export interface RelateDocumentsProps {
26
26
  onChange: (value: RelatedDocument[]) => void;
27
27
  /** Catálogo de tipos de relación (lo aporta el addon). */
28
28
  relationTypes: RelationTypeOption[];
29
- /** Búsqueda de documentos origen. Estable (useCallback). */
29
+ /** Búsqueda de documentos origen. */
30
30
  search: (q: string, signal: AbortSignal) => Promise<RelatableDocument[]>;
31
31
  /** Tipo preseleccionado al agregar. Default: el primero del catálogo. */
32
32
  defaultRelationType?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"relate-documents.d.ts","sourceRoot":"","sources":["../../src/business/relate-documents.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,aAAa,EAAE,kBAAkB,EAAE,CAAA;IACnC,4DAA4D;IAC5D,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAA;IACxE,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD,wBAAgB,eAAe,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,aAAa,EACb,MAAM,EACN,mBAAmB,EACnB,GAAG,EACH,QAAQ,EACR,WAAW,GACd,EAAE,oBAAoB,+BA+GtB;AAED,mFAAmF;AACnF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,qBAAqB,GAAG,eAAe,CAAC,CAAC,CAEzI"}
1
+ {"version":3,"file":"relate-documents.d.ts","sourceRoot":"","sources":["../../src/business/relate-documents.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,aAAa,EAAE,kBAAkB,EAAE,CAAA;IACnC,qCAAqC;IACrC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAA;IACxE,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD,wBAAgB,eAAe,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,aAAa,EACb,MAAM,EACN,mBAAmB,EACnB,GAAG,EACH,QAAQ,EACR,WAAW,GACd,EAAE,oBAAoB,+BAsGtB;AAED,mFAAmF;AACnF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,qBAAqB,GAAG,eAAe,CAAC,CAAC,CAEzI"}