@asteby/metacore-runtime-react 23.1.0 → 23.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/dist/action-modal-dispatcher.js +4 -4
- package/dist/dialogs/dynamic-record.d.ts +5 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +146 -31
- package/dist/display-value.d.ts +56 -0
- package/dist/display-value.d.ts.map +1 -0
- package/dist/display-value.js +74 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +2 -53
- package/dist/dynamic-kanban.d.ts +19 -3
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +114 -8
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +5 -3
- package/dist/dynamic-table.d.ts +9 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +132 -32
- package/dist/use-infinite-scroll.d.ts +27 -0
- package/dist/use-infinite-scroll.d.ts.map +1 -0
- package/dist/use-infinite-scroll.js +60 -0
- package/package.json +3 -3
- package/src/__tests__/dynamic-kanban-infinite-scroll.test.tsx +210 -0
- package/src/__tests__/dynamic-table-infinite-scroll.test.tsx +196 -0
- package/src/__tests__/readonly-fields.test.tsx +97 -0
- package/src/__tests__/record-detail-display.test.tsx +113 -0
- package/src/__tests__/use-infinite-scroll.test.tsx +147 -0
- package/src/action-modal-dispatcher.tsx +4 -4
- package/src/dialogs/dynamic-record.tsx +217 -40
- package/src/display-value.tsx +134 -0
- package/src/dynamic-columns.tsx +6 -102
- package/src/dynamic-kanban.tsx +168 -8
- package/src/dynamic-row-actions.tsx +5 -3
- package/src/dynamic-table.tsx +149 -10
- package/src/use-infinite-scroll.ts +94 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 40a1e8e: Soporte de campos `readonly` (kernel v0.64.0) en el diálogo de registro dinámico. Un campo generado por el servidor/sistema (p. ej. `number`/`github_url` que el addon de GitHub rellena tras el create outbound) ahora se OCULTA en el formulario de creación y se muestra DESHABILITADO (input muted, valor visible) en edición. Las vistas de lectura (tabla/kanban/detalle) no cambian.
|
|
8
|
+
- d0eb423: El modal de detalle de registro (`ViewRecordDialog` / `DynamicRecordDialog`) ahora renderiza los valores con los MISMOS display types "pro" que la tabla, en vez de texto plano genérico. Las primitivas de render de la tabla (`OptionBadge`, `RelationThumbnail`, `statusColorFor`, `useIsDarkTheme`) se extrajeron a un módulo compartido `display-value.tsx` que tabla y dialog consumen (cero copy-paste).
|
|
9
|
+
|
|
10
|
+
El dialog ahora resuelve cada valor por el display type declarado (`cellStyle ?? type`), igual que la tabla:
|
|
11
|
+
- opciones/select → Badge con color resuelto y label localizado
|
|
12
|
+
- `cellStyle:'status'`/`'badge'` (ej. stage de kanban) → pill con color semántico y label traducido vía i18n del manifest, en vez de "backlog" crudo
|
|
13
|
+
- `cellStyle:'url'`/`'link'` en columna de texto (ej. `github_url`) → enlace clickeable en pestaña nueva, truncado
|
|
14
|
+
- `cellStyle:'datetime'` en columna numérica/epoch (ej. `synced_at`) → fecha formateada con timezone de la org, no dígitos crudos
|
|
15
|
+
- arrays de labels/tags → fila de badges (con color cuando el label trae `color`)
|
|
16
|
+
- creator/avatar → nombre + avatar
|
|
17
|
+
|
|
18
|
+
## 23.2.1
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- d578fe0: Localiza los toasts de CRUD estándar (eliminar, crear, actualizar, borrado masivo, subida de imagen) al español vía i18n con `defaultValue`, en lugar de mostrar el `message` en inglés que devuelve el backend. Los mensajes de acciones declarativas de addons ahora pasan por `t()` por si son claves i18n.
|
|
23
|
+
|
|
24
|
+
## 23.2.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- 68a6844: Scroll infinito con carga incremental en DynamicKanban y DynamicTable,
|
|
29
|
+
respetando la búsqueda y los filtros activos. Cero cambios de backend: se apoya
|
|
30
|
+
en el `page`/`per_page` que ya expone `/data/:model`.
|
|
31
|
+
- **Primitivos compartidos (`use-infinite-scroll`):** `dedupeById` (append puro
|
|
32
|
+
que descarta ids ya presentes, estable en identidad cuando no hay altas) y
|
|
33
|
+
`useInfiniteScrollSentinel` (IntersectionObserver sobre un sentinel; lee
|
|
34
|
+
`onLoadMore`/`disabled` por ref para no recrear el observer en cada render;
|
|
35
|
+
degrada a no-op donde no hay IntersectionObserver). Los usan ambas vistas.
|
|
36
|
+
- **Kanban incremental por lane:** una página global inicial (`pageSize`, 50 por
|
|
37
|
+
defecto) pinta el tablero agrupado —y captura naturalmente la lane
|
|
38
|
+
"sin asignar"— y luego cada lane rellena SU propia etapa al acercarse el
|
|
39
|
+
scroll al fondo (`f_<group_by>=<stage>&page=n&per_page=lanePageSize`, 25 por
|
|
40
|
+
defecto, sobre los filtros activos), con dedup por id y un skeleton chico al
|
|
41
|
+
fondo. El contador del header muestra el total real de la etapa cuando la
|
|
42
|
+
respuesta trae `meta.total` (`count/total`), si no el cargado. Cambiar
|
|
43
|
+
filtros/búsqueda resetea la paginación de todas las lanes. El drag&drop
|
|
44
|
+
optimista ajusta los totales de origen y destino (`applyLaneTotalsOnMove`)
|
|
45
|
+
para que las lanes parciales sigan mostrando un `count/total` veraz, y los
|
|
46
|
+
revierte si el PUT falla.
|
|
47
|
+
- **Tabla opt-in (`infiniteScroll?: boolean`, default `false`):** la paginación
|
|
48
|
+
clásica queda intacta salvo que se active. Con el flag, un sentinel al fondo
|
|
49
|
+
del contenedor de scroll pide la siguiente página y APPENDEA filas (dedup por
|
|
50
|
+
id); el pager clásico se reemplaza por un indicador "N de total". Cambiar
|
|
51
|
+
filtros/orden/búsqueda resetea a la página 1 y limpia el acumulado. El footer
|
|
52
|
+
de totales (`/aggregate`) no cambia.
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- Updated dependencies [68a6844]
|
|
57
|
+
- @asteby/metacore-ui@2.9.1
|
|
58
|
+
|
|
3
59
|
## 23.1.0
|
|
4
60
|
|
|
5
61
|
### Minor Changes
|
|
@@ -116,12 +116,12 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
116
116
|
const url = buildActionUrl(endpoint, model, record.id, action.key);
|
|
117
117
|
const res = await api.post(url, {});
|
|
118
118
|
if (res.data.success) {
|
|
119
|
-
toast.success(res.data.message
|
|
119
|
+
toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'));
|
|
120
120
|
onOpenChange(false);
|
|
121
121
|
onSuccess();
|
|
122
122
|
}
|
|
123
123
|
else {
|
|
124
|
-
toast.error(res.data.message
|
|
124
|
+
toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'));
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
catch (err) {
|
|
@@ -209,12 +209,12 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
209
209
|
const url = buildActionUrl(endpoint, model, record.id, action.key);
|
|
210
210
|
const res = await api.post(url, formData);
|
|
211
211
|
if (res.data.success) {
|
|
212
|
-
toast.success(res.data.message
|
|
212
|
+
toast.success(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.success'));
|
|
213
213
|
onOpenChange(false);
|
|
214
214
|
onSuccess();
|
|
215
215
|
}
|
|
216
216
|
else {
|
|
217
|
-
toast.error(res.data.message
|
|
217
|
+
toast.error(res.data.message ? t(res.data.message, { defaultValue: res.data.message }) : t('common.error'));
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
catch (err) {
|
|
@@ -162,7 +162,12 @@ export interface DynamicRecordDialogProps {
|
|
|
162
162
|
export declare function isLineItemsField(field: FieldDef, value: any): boolean;
|
|
163
163
|
export declare function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null;
|
|
164
164
|
export declare function isMoneyField(field: FieldDef, value: any): boolean;
|
|
165
|
+
export declare function filterVisibleFields(fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create'): FieldDef[];
|
|
165
166
|
export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, onChange, }: DynamicRecordDialogProps): import("react").JSX.Element;
|
|
167
|
+
export declare function ReadonlyEditField({ field, value }: {
|
|
168
|
+
field: FieldDef;
|
|
169
|
+
value: any;
|
|
170
|
+
}): import("react").JSX.Element;
|
|
166
171
|
export declare function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }: {
|
|
167
172
|
field: FieldDef;
|
|
168
173
|
value: any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAuC1C,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
|
|
@@ -27,6 +27,8 @@ import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
|
|
|
27
27
|
import { DynamicIcon, isLucideIconName } from '../dynamic-icon';
|
|
28
28
|
import { humanizeToken } from '../dynamic-columns-helpers';
|
|
29
29
|
import { formatDateCell } from '../dynamic-columns';
|
|
30
|
+
import { OptionBadge, statusColorFor, useIsDarkTheme, } from '../display-value';
|
|
31
|
+
import { generateBadgeStyles } from '@asteby/metacore-ui/lib';
|
|
30
32
|
import { CollectionCell } from '../collection-cell';
|
|
31
33
|
import { ImageUrlContext, identityImageUrl } from '../image-url-context';
|
|
32
34
|
import { TimeZoneContext, CurrencyContext } from '../org-runtime-context';
|
|
@@ -228,6 +230,19 @@ export function isMoneyField(field, value) {
|
|
|
228
230
|
return false;
|
|
229
231
|
return MONEY_KEY_HEURISTIC.some(m => key === m || key.endsWith(`_${m}`) || key.startsWith(`${m}_`));
|
|
230
232
|
}
|
|
233
|
+
// filterVisibleFields decides which declared fields render in the form for a
|
|
234
|
+
// given mode. `hidden` fields never render. A `readonly` (server/system-
|
|
235
|
+
// generated) field is EXCLUDED on create — the user can't set a value the
|
|
236
|
+
// server will overwrite — but stays visible on edit/view (rendered disabled).
|
|
237
|
+
export function filterVisibleFields(fields, mode) {
|
|
238
|
+
return (fields ?? []).filter(f => {
|
|
239
|
+
if (f.hidden)
|
|
240
|
+
return false;
|
|
241
|
+
if (mode === 'create' && f.readonly)
|
|
242
|
+
return false;
|
|
243
|
+
return true;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
231
246
|
export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
|
|
232
247
|
const api = useApi();
|
|
233
248
|
const { t } = useTranslation();
|
|
@@ -319,7 +334,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
319
334
|
catch (err) {
|
|
320
335
|
console.error('[DynamicRecordDialog] load error:', err);
|
|
321
336
|
if (!seed)
|
|
322
|
-
toast.error('
|
|
337
|
+
toast.error(t('dynamic.load_error', { defaultValue: 'No se pudieron cargar los datos' }));
|
|
323
338
|
}
|
|
324
339
|
finally {
|
|
325
340
|
if (!cancelled)
|
|
@@ -421,14 +436,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
421
436
|
try {
|
|
422
437
|
if (isCreate && onCreate) {
|
|
423
438
|
const created = await onCreate(formValues);
|
|
424
|
-
toast.success(modalMeta?.messages?.created || 'Registro creado correctamente');
|
|
439
|
+
toast.success(modalMeta?.messages?.created || t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' }));
|
|
425
440
|
onSaved?.(created ?? undefined);
|
|
426
441
|
onOpenChange(false);
|
|
427
442
|
return;
|
|
428
443
|
}
|
|
429
444
|
if (!isCreate && recordId && onUpdate) {
|
|
430
445
|
const updated = await onUpdate(String(recordId), formValues);
|
|
431
|
-
toast.success(modalMeta?.messages?.updated || 'Guardado correctamente');
|
|
446
|
+
toast.success(modalMeta?.messages?.updated || t('dynamic.update_success', { defaultValue: 'Guardado correctamente' }));
|
|
432
447
|
onSaved?.(updated ?? undefined);
|
|
433
448
|
onOpenChange(false);
|
|
434
449
|
return;
|
|
@@ -449,17 +464,19 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
449
464
|
// localized fallback. NOT res.data.message — the dynamic CRUD
|
|
450
465
|
// endpoint returns a raw English string that would leak into the toast.
|
|
451
466
|
toast.success(modalMeta?.messages?.[isCreate ? 'created' : 'updated']
|
|
452
|
-
|| (isCreate
|
|
467
|
+
|| (isCreate
|
|
468
|
+
? t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' })
|
|
469
|
+
: t('dynamic.update_success', { defaultValue: 'Guardado correctamente' })));
|
|
453
470
|
// Hand the persisted record back so callers can auto-select it.
|
|
454
471
|
onSaved?.(res.data?.data ?? res.data ?? undefined);
|
|
455
472
|
onOpenChange(false);
|
|
456
473
|
}
|
|
457
474
|
else {
|
|
458
|
-
toast.error(res.data?.message || '
|
|
475
|
+
toast.error(res.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
|
|
459
476
|
}
|
|
460
477
|
}
|
|
461
478
|
catch (err) {
|
|
462
|
-
toast.error(err?.response?.data?.message || '
|
|
479
|
+
toast.error(err?.response?.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
|
|
463
480
|
}
|
|
464
481
|
finally {
|
|
465
482
|
setSaving(false);
|
|
@@ -475,20 +492,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
475
492
|
}
|
|
476
493
|
catch (err) {
|
|
477
494
|
console.error('[DynamicRecordDialog] delete error:', err);
|
|
478
|
-
toast.error(err?.response?.data?.message || err?.message || '
|
|
495
|
+
toast.error(err?.response?.data?.message || err?.message || t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }));
|
|
479
496
|
}
|
|
480
497
|
finally {
|
|
481
498
|
setDeleting(false);
|
|
482
499
|
}
|
|
483
500
|
};
|
|
484
501
|
const title = modalMeta ? config.getTitle(modalMeta, t) : '';
|
|
485
|
-
const visibleFields = modalMeta?.fields
|
|
486
|
-
if (f.hidden)
|
|
487
|
-
return false;
|
|
488
|
-
if (isCreate && f.readonly)
|
|
489
|
-
return false;
|
|
490
|
-
return true;
|
|
491
|
-
}) ?? [];
|
|
502
|
+
const visibleFields = filterVisibleFields(modalMeta?.fields, mode);
|
|
492
503
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
|
|
493
504
|
const isFullWidth = field.type === 'textarea';
|
|
494
505
|
return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
|
|
@@ -498,8 +509,26 @@ function LoadingSkeleton() {
|
|
|
498
509
|
return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
|
|
499
510
|
}
|
|
500
511
|
function FieldRow({ field, record, value, mode, onChange }) {
|
|
501
|
-
|
|
502
|
-
|
|
512
|
+
// A `readonly` field is server/system-generated (e.g. the GitHub addon's
|
|
513
|
+
// `number`/`github_url`, filled by the API after the outbound create). On
|
|
514
|
+
// CREATE it is excluded from the form entirely (see `visibleFields`); on EDIT
|
|
515
|
+
// it stays visible but is NOT editable — rendered as a disabled, muted input
|
|
516
|
+
// so the user sees its value without being able to change it. View mode keeps
|
|
517
|
+
// the rich read-only renderer.
|
|
518
|
+
const isEditReadonly = mode === 'edit' && !!field.readonly;
|
|
519
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [field.label, field.required && mode !== 'view' && !isEditReadonly && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), mode === 'view' ? (_jsx(ViewValue, { field: field, value: value, record: record })) : isEditReadonly ? (_jsx(ReadonlyEditField, { field: field, value: value })) : (_jsx(EditField, { field: field, value: value, onChange: onChange, record: record }))] }));
|
|
520
|
+
}
|
|
521
|
+
// ReadonlyEditField — the edit-mode rendering of a `readonly` (system-generated)
|
|
522
|
+
// field: a disabled, muted input that shows the current value without allowing
|
|
523
|
+
// edits. Booleans render as a disabled switch to match their editable
|
|
524
|
+
// counterpart; everything else renders the formatted display value in a disabled
|
|
525
|
+
// text input.
|
|
526
|
+
export function ReadonlyEditField({ field, value }) {
|
|
527
|
+
if (field.type === 'boolean' || typeof value === 'boolean') {
|
|
528
|
+
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, disabled: true }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
|
|
529
|
+
}
|
|
530
|
+
const display = formatDisplayValue(value, field);
|
|
531
|
+
return _jsx(Input, { value: display === '—' ? '' : display, disabled: true, readOnly: true, className: "text-muted-foreground" });
|
|
503
532
|
}
|
|
504
533
|
// RelationViewValue — read-only FK lead. Resolves the relation's label + image
|
|
505
534
|
// from (1) the sibling object the table served, then (2) the canonical options
|
|
@@ -549,8 +578,19 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
549
578
|
const getImageUrl = getImageUrlProp ?? ctxImageUrl;
|
|
550
579
|
const timeZone = timeZoneProp ?? ctxTimeZone;
|
|
551
580
|
const currency = currencyProp ?? ctxCurrency;
|
|
581
|
+
// Declarative display hint the backend stamps (mirrors the table column's
|
|
582
|
+
// `cellStyle`). The table renders each cell off `cellStyle ?? type`; the
|
|
583
|
+
// detail view keys off the SAME resolved renderer so both stay in lock-step
|
|
584
|
+
// (a `datetime` display on a numeric column, a `url` display on a text
|
|
585
|
+
// column, a `status`/`badge` pill, …).
|
|
586
|
+
const renderAs = field.cellStyle ?? field.type;
|
|
552
587
|
// created_by / avatar resolver sibling → name (+ avatar) instead of "—".
|
|
553
|
-
if (field.type === 'avatar' ||
|
|
588
|
+
if (field.type === 'avatar' ||
|
|
589
|
+
renderAs === 'avatar' ||
|
|
590
|
+
renderAs === 'creator' ||
|
|
591
|
+
renderAs === 'user' ||
|
|
592
|
+
field.key === 'created_by' ||
|
|
593
|
+
field.key === 'created_by_id') {
|
|
554
594
|
const user = createdBySibling(rawValue, record);
|
|
555
595
|
if (user) {
|
|
556
596
|
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [user.avatar ? (_jsx("img", { src: getImageUrl(String(user.avatar)), alt: user.name ?? '', className: "h-6 w-6 rounded-full object-cover" })) : null, _jsx("span", { className: "text-sm", children: user.name ?? user.email ?? '—' })] }));
|
|
@@ -593,8 +633,14 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
593
633
|
(field.key === 'icon' || field.key.endsWith('_icon'))) {
|
|
594
634
|
return _jsx(IconNameViewValue, { name: value });
|
|
595
635
|
}
|
|
596
|
-
|
|
597
|
-
|
|
636
|
+
// URL/link display (matches the table's `url`/`link` cell). Triggers on the
|
|
637
|
+
// stamped display type — not just the storage `type` — so a text column
|
|
638
|
+
// carrying `cellStyle:'url'` (e.g. `github_url`) renders as a clickable
|
|
639
|
+
// external link, opening in a new tab, truncated.
|
|
640
|
+
if ((renderAs === 'url' || renderAs === 'link') && value) {
|
|
641
|
+
const urlStr = String(value);
|
|
642
|
+
const href = /^https?:\/\//i.test(urlStr) ? urlStr : `https://${urlStr}`;
|
|
643
|
+
return (_jsxs("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "truncate max-w-[360px]", children: urlStr })] }));
|
|
598
644
|
}
|
|
599
645
|
// Money → org-currency string. Detected by the backend `cellStyle:'currency'`
|
|
600
646
|
// stamp or a numeric value whose key matches the money heuristic (fallback
|
|
@@ -615,24 +661,54 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
615
661
|
}
|
|
616
662
|
// Date/datetime/timestamp → tz-aware format. `date` pins to UTC (calendar
|
|
617
663
|
// day); instants render in the org timezone with a full-precision tooltip.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
664
|
+
// Keys off the display type (`cellStyle ?? type`) so a numeric/epoch column
|
|
665
|
+
// stamped `datetime` (e.g. `synced_at`) formats as a date, never raw digits.
|
|
666
|
+
if (renderAs === 'date' ||
|
|
667
|
+
renderAs === 'datetime' ||
|
|
668
|
+
renderAs === 'timestamp' ||
|
|
669
|
+
renderAs === 'timestamptz') {
|
|
670
|
+
const dateRenderAs = renderAs === 'date' ? 'date' : renderAs;
|
|
671
|
+
const formatted = formatDateCell(value, dateRenderAs, es, timeZone);
|
|
621
672
|
if (formatted) {
|
|
622
673
|
return (_jsx("p", { className: "text-sm py-1", title: formatted.title, children: formatted.display }));
|
|
623
674
|
}
|
|
624
675
|
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
625
676
|
}
|
|
626
|
-
// Enum/option field with served options → colored/iconed
|
|
627
|
-
//
|
|
677
|
+
// Enum/option field with served options → the SAME colored/iconed pill the
|
|
678
|
+
// table renders (shared `OptionBadge`): resolved color, thumbnail/icon and
|
|
679
|
+
// the localized option label (e.g. "Almacenable" instead of "storable").
|
|
628
680
|
const opt = servedOption(field, value);
|
|
629
681
|
if (opt) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
682
|
+
return (_jsx("div", { className: "py-1", children: _jsx(OptionBadge, { option: {
|
|
683
|
+
value: String(opt.value ?? value ?? ''),
|
|
684
|
+
label: opt.label,
|
|
685
|
+
color: opt.color ?? undefined,
|
|
686
|
+
icon: opt.icon ?? undefined,
|
|
687
|
+
image: opt.image ?? undefined,
|
|
688
|
+
}, getImageUrl: getImageUrl }) }));
|
|
689
|
+
}
|
|
690
|
+
// Array of scalars / label objects (e.g. github `labels`, tags, a
|
|
691
|
+
// group-badge list) → a row of pills, mirroring the table's `tags` /
|
|
692
|
+
// `relation-badge-list` cells. Checked before the structured-object branch
|
|
693
|
+
// so a flat label array never renders as a mini-table.
|
|
694
|
+
if (Array.isArray(value) &&
|
|
695
|
+
(renderAs === 'tags' ||
|
|
696
|
+
renderAs === 'relation-badge-list' ||
|
|
697
|
+
value.every((v) => v === null || typeof v !== 'object' || 'label' in v || 'name' in v))) {
|
|
698
|
+
return _jsx(BadgeListViewValue, { items: value, getImageUrl: getImageUrl });
|
|
699
|
+
}
|
|
700
|
+
// Status / badge / select display with no served option list — a bare enum
|
|
701
|
+
// token (e.g. a kanban `stage` like "backlog"). Render a colored pill with a
|
|
702
|
+
// semantic/value-derived color and a localized-or-humanized label, matching
|
|
703
|
+
// the table's `status`/`badge` cells.
|
|
704
|
+
if ((renderAs === 'status' ||
|
|
705
|
+
renderAs === 'badge' ||
|
|
706
|
+
renderAs === 'select' ||
|
|
707
|
+
renderAs === 'option') &&
|
|
708
|
+
value !== null &&
|
|
709
|
+
value !== undefined &&
|
|
710
|
+
typeof value !== 'object') {
|
|
711
|
+
return _jsx(StatusBadgeViewValue, { field: field, value: value, t: t });
|
|
636
712
|
}
|
|
637
713
|
// Structured value (jsonb column, e.g. fiscal_data) with no label/name/title
|
|
638
714
|
// to surface — render readable key/value pairs instead of falling through to
|
|
@@ -652,6 +728,44 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
652
728
|
function IconNameViewValue({ name }) {
|
|
653
729
|
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx("div", { className: "h-8 w-8 flex items-center justify-center rounded bg-muted", children: _jsx(DynamicIcon, { name: name, className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm text-muted-foreground", children: name })] }));
|
|
654
730
|
}
|
|
731
|
+
// StatusBadgeViewValue — a bare enum/status token (no served option list) as a
|
|
732
|
+
// colored pill: a semantic/value-derived color (same `statusColorFor` the table
|
|
733
|
+
// uses) plus a localized-or-humanized label. Mirrors the table's `status`/
|
|
734
|
+
// `badge` cell so a kanban `stage` ("backlog") reads as a colored, translated
|
|
735
|
+
// badge instead of the raw token.
|
|
736
|
+
function StatusBadgeViewValue({ field, value, t, }) {
|
|
737
|
+
const isDark = useIsDarkTheme();
|
|
738
|
+
const token = String(value);
|
|
739
|
+
// Prefer an explicit per-option color served on the field (metadata.stages /
|
|
740
|
+
// options), else derive a semantic color from the token.
|
|
741
|
+
const declared = field.options?.find((o) => String(o.value) === token);
|
|
742
|
+
const color = declared?.color || statusColorFor(token);
|
|
743
|
+
// Localized label: the option's already-localized label wins, then a manifest
|
|
744
|
+
// i18n key matching the raw token, then a humanized fallback.
|
|
745
|
+
const label = declared?.label ?? t(token, { defaultValue: humanizeToken(token) });
|
|
746
|
+
return (_jsx("div", { className: "py-1", children: _jsx(Badge, { variant: "outline", className: "border-0 flex w-fit items-center gap-1", style: generateBadgeStyles(color, { isDark }), children: label }) }));
|
|
747
|
+
}
|
|
748
|
+
// BadgeListViewValue — an array of scalars / label objects as a row of pills
|
|
749
|
+
// (github `labels`, tags, a group-badge list). Objects carrying a `color` render
|
|
750
|
+
// as colored `OptionBadge`s; plain strings render as neutral secondary pills.
|
|
751
|
+
function BadgeListViewValue({ items, getImageUrl, }) {
|
|
752
|
+
if (!items || items.length === 0) {
|
|
753
|
+
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
754
|
+
}
|
|
755
|
+
return (_jsx("div", { className: "flex flex-wrap gap-1 py-1", children: items.map((item, i) => {
|
|
756
|
+
if (item !== null && typeof item === 'object') {
|
|
757
|
+
const opt = {
|
|
758
|
+
value: String(item.value ?? item.id ?? item.name ?? item.label ?? i),
|
|
759
|
+
label: String(item.label ?? item.name ?? item.value ?? ''),
|
|
760
|
+
color: item.color ?? undefined,
|
|
761
|
+
icon: item.icon ?? undefined,
|
|
762
|
+
image: item.image ?? item.avatar ?? undefined,
|
|
763
|
+
};
|
|
764
|
+
return _jsx(OptionBadge, { option: opt, getImageUrl: getImageUrl }, i);
|
|
765
|
+
}
|
|
766
|
+
return (_jsx(Badge, { variant: "secondary", className: "w-fit", children: String(item) }, i));
|
|
767
|
+
}) }));
|
|
768
|
+
}
|
|
655
769
|
// StructuredViewValue renders a jsonb object/array that has no resolvable label.
|
|
656
770
|
// It delegates to the shared `CollectionCell` in `'inline'` mode so the detail
|
|
657
771
|
// view gets the SAME pro rendering as the table: a declared `item_fields` schema
|
|
@@ -738,6 +852,7 @@ export function EditField({ field, value, onChange, record }) {
|
|
|
738
852
|
return (_jsx(Input, { type: inputType, value: value ?? '', onChange: (e) => onChange(field.type === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value), placeholder: field.placeholder }));
|
|
739
853
|
}
|
|
740
854
|
function ImageUploadField({ field: _field, value, onChange }) {
|
|
855
|
+
const { t } = useTranslation();
|
|
741
856
|
const api = useApi();
|
|
742
857
|
const model = useContext(ModelContext);
|
|
743
858
|
const getImageUrl = useContext(ImageUrlContext);
|
|
@@ -758,7 +873,7 @@ function ImageUploadField({ field: _field, value, onChange }) {
|
|
|
758
873
|
onChange(url);
|
|
759
874
|
}
|
|
760
875
|
catch {
|
|
761
|
-
toast.error('
|
|
876
|
+
toast.error(t('dynamic.image_upload_error', { defaultValue: 'No se pudo subir la imagen' }));
|
|
762
877
|
}
|
|
763
878
|
finally {
|
|
764
879
|
setUploading(false);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared display-value primitives.
|
|
3
|
+
*
|
|
4
|
+
* These are the building blocks the dynamic TABLE (`dynamic-columns.tsx`) uses
|
|
5
|
+
* to render "pro" cells — colored option badges, relation thumbnails, semantic
|
|
6
|
+
* status colors, dark-mode detection. They live here (instead of inline in the
|
|
7
|
+
* table) so the read-only DETAIL DIALOG (`dialogs/dynamic-record.tsx`) can reuse
|
|
8
|
+
* the EXACT same rendering. Ecosystem rule: shared primitives, zero copy-paste —
|
|
9
|
+
* table and dialog must not drift.
|
|
10
|
+
*/
|
|
11
|
+
import React from 'react';
|
|
12
|
+
/**
|
|
13
|
+
* `true` when the host document is in dark mode. Observes the `<html>` class so
|
|
14
|
+
* badge colors re-derive on theme toggle. SSR/no-DOM safe (starts light).
|
|
15
|
+
*/
|
|
16
|
+
export declare function useIsDarkTheme(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Semantic status → badge color. Used by the `status` cell/value when no
|
|
19
|
+
* explicit `options` color is declared. Generic, value-driven mapping.
|
|
20
|
+
*/
|
|
21
|
+
export declare function statusColorFor(value: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Tiny square thumbnail for a resolved relation/option that carries an `image`
|
|
24
|
+
* (brand logo, product photo, customer/user avatar). Uses the same Avatar
|
|
25
|
+
* primitive as the `avatar`/`creator` cells so a broken/loading image
|
|
26
|
+
* gracefully falls back to the record's initials. Sized small (inline style so
|
|
27
|
+
* an addon-arbitrary Tailwind class never gets dropped by a consuming app's
|
|
28
|
+
* class scan). Rendered inline alongside a label — never alone.
|
|
29
|
+
*/
|
|
30
|
+
export declare const RelationThumbnail: React.FC<{
|
|
31
|
+
src: string;
|
|
32
|
+
alt: string;
|
|
33
|
+
getImageUrl?: (path: string) => string;
|
|
34
|
+
size?: number;
|
|
35
|
+
}>;
|
|
36
|
+
export interface DisplayOption {
|
|
37
|
+
value: string;
|
|
38
|
+
label: string;
|
|
39
|
+
icon?: string;
|
|
40
|
+
color?: string;
|
|
41
|
+
image?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Colored option badge — the canonical "pro" pill for a select/status/badge
|
|
45
|
+
* value. Explicit backend `color` wins; otherwise a stable, cohesive color is
|
|
46
|
+
* derived from the option value so "dead" gray badges come alive. Inline style
|
|
47
|
+
* (hex-derived) so it works regardless of the host's tailwind safelist —
|
|
48
|
+
* addon-arbitrary classes aren't in the host scan.
|
|
49
|
+
*/
|
|
50
|
+
export declare const OptionBadge: React.FC<{
|
|
51
|
+
option: DisplayOption;
|
|
52
|
+
getImageUrl?: (path: string) => string;
|
|
53
|
+
/** Accepted for call-site compatibility with the table; unused (option.label wins). */
|
|
54
|
+
fallback?: string;
|
|
55
|
+
}>;
|
|
56
|
+
//# sourceMappingURL=display-value.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"display-value.d.ts","sourceRoot":"","sources":["../src/display-value.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAepD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,CAcA,CAAA;AAED,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;IAC/B,MAAM,EAAE,aAAa,CAAA;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,CAmBA,CAAA"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Shared display-value primitives.
|
|
4
|
+
*
|
|
5
|
+
* These are the building blocks the dynamic TABLE (`dynamic-columns.tsx`) uses
|
|
6
|
+
* to render "pro" cells — colored option badges, relation thumbnails, semantic
|
|
7
|
+
* status colors, dark-mode detection. They live here (instead of inline in the
|
|
8
|
+
* table) so the read-only DETAIL DIALOG (`dialogs/dynamic-record.tsx`) can reuse
|
|
9
|
+
* the EXACT same rendering. Ecosystem rule: shared primitives, zero copy-paste —
|
|
10
|
+
* table and dialog must not drift.
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { Avatar, AvatarImage, AvatarFallback, Badge } from '@asteby/metacore-ui';
|
|
14
|
+
import { generateBadgeStyles, getInitials, optionColor, } from '@asteby/metacore-ui/lib';
|
|
15
|
+
import { DynamicIcon } from './dynamic-icon';
|
|
16
|
+
/**
|
|
17
|
+
* `true` when the host document is in dark mode. Observes the `<html>` class so
|
|
18
|
+
* badge colors re-derive on theme toggle. SSR/no-DOM safe (starts light).
|
|
19
|
+
*/
|
|
20
|
+
export function useIsDarkTheme() {
|
|
21
|
+
const [isDark, setIsDark] = React.useState(() => typeof document !== 'undefined' &&
|
|
22
|
+
document.documentElement.classList.contains('dark'));
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
if (typeof document === 'undefined')
|
|
25
|
+
return;
|
|
26
|
+
const sync = () => setIsDark(document.documentElement.classList.contains('dark'));
|
|
27
|
+
sync();
|
|
28
|
+
const observer = new MutationObserver(sync);
|
|
29
|
+
observer.observe(document.documentElement, {
|
|
30
|
+
attributes: true,
|
|
31
|
+
attributeFilter: ['class'],
|
|
32
|
+
});
|
|
33
|
+
return () => observer.disconnect();
|
|
34
|
+
}, []);
|
|
35
|
+
return isDark;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Semantic status → badge color. Used by the `status` cell/value when no
|
|
39
|
+
* explicit `options` color is declared. Generic, value-driven mapping.
|
|
40
|
+
*/
|
|
41
|
+
export function statusColorFor(value) {
|
|
42
|
+
const v = value.toLowerCase();
|
|
43
|
+
if (['active', 'enabled', 'paid', 'completed', 'done', 'success', 'approved', 'open']
|
|
44
|
+
.includes(v))
|
|
45
|
+
return '#22c55e';
|
|
46
|
+
if (['pending', 'draft', 'processing', 'in_progress', 'review', 'waiting'].includes(v))
|
|
47
|
+
return '#eab308';
|
|
48
|
+
if (['inactive', 'disabled', 'cancelled', 'canceled', 'failed', 'rejected', 'error', 'closed']
|
|
49
|
+
.includes(v))
|
|
50
|
+
return '#ef4444';
|
|
51
|
+
return '#6b7280';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Tiny square thumbnail for a resolved relation/option that carries an `image`
|
|
55
|
+
* (brand logo, product photo, customer/user avatar). Uses the same Avatar
|
|
56
|
+
* primitive as the `avatar`/`creator` cells so a broken/loading image
|
|
57
|
+
* gracefully falls back to the record's initials. Sized small (inline style so
|
|
58
|
+
* an addon-arbitrary Tailwind class never gets dropped by a consuming app's
|
|
59
|
+
* class scan). Rendered inline alongside a label — never alone.
|
|
60
|
+
*/
|
|
61
|
+
export const RelationThumbnail = ({ src, alt, getImageUrl, size = 18 }) => (_jsxs(Avatar, { className: "shrink-0 rounded-sm ring-1 ring-border/40", style: { width: size, height: size }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(src) : src, alt: alt, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-sm bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(alt) })] }));
|
|
62
|
+
/**
|
|
63
|
+
* Colored option badge — the canonical "pro" pill for a select/status/badge
|
|
64
|
+
* value. Explicit backend `color` wins; otherwise a stable, cohesive color is
|
|
65
|
+
* derived from the option value so "dead" gray badges come alive. Inline style
|
|
66
|
+
* (hex-derived) so it works regardless of the host's tailwind safelist —
|
|
67
|
+
* addon-arbitrary classes aren't in the host scan.
|
|
68
|
+
*/
|
|
69
|
+
export const OptionBadge = ({ option, getImageUrl }) => {
|
|
70
|
+
const isDark = useIsDarkTheme();
|
|
71
|
+
const colorSource = option.color || optionColor(option.value || option.label);
|
|
72
|
+
const colorStyles = generateBadgeStyles(colorSource, { isDark });
|
|
73
|
+
return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.image ? (_jsx(RelationThumbnail, { src: option.image, alt: option.label, getImageUrl: getImageUrl, size: 16 })) : (option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" })), _jsx("span", { children: option.label })] }));
|
|
74
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAsC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAqFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA8mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|