@asteby/metacore-runtime-react 23.2.0 → 23.4.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 +33 -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 +155 -34
- 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 +13 -72
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +5 -3
- package/dist/dynamic-table.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/rich-url.d.ts +119 -0
- package/dist/rich-url.d.ts.map +1 -0
- package/dist/rich-url.js +224 -0
- package/package.json +1 -1
- package/src/__tests__/readonly-fields.test.tsx +97 -0
- package/src/__tests__/record-detail-display.test.tsx +113 -0
- package/src/__tests__/rich-url.test.tsx +196 -0
- package/src/action-modal-dispatcher.tsx +4 -4
- package/src/dialogs/dynamic-record.tsx +244 -48
- package/src/display-value.tsx +134 -0
- package/src/dynamic-columns.tsx +24 -128
- package/src/dynamic-row-actions.tsx +5 -3
- package/src/dynamic-table.tsx +2 -2
- package/src/index.ts +17 -0
- package/src/rich-url.tsx +359 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 23.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- edfa5a9: Render "pro" de URLs, imágenes y archivos: una URL nunca se muestra cruda. Nuevas primitivas compartidas en `rich-url.tsx` (`MediaValue`, `UrlChip`, `FileChip`, `ImageThumbnail`, `RichText`, `linkifyText`, `classifyUrl`, …) que consumen por igual la CELDA de la tabla/kanban (`dynamic-columns.tsx`) y el DETAIL DIALOG (`dynamic-record.tsx`) — cero copy-paste.
|
|
8
|
+
- URL de página (ej. `github_url` con la issue completa) → chip compacto con icono `ExternalLink` + label corto (hostname, ej. "github.com", o el nombre de archivo si aplica); URL completa en el tooltip; abre en pestaña nueva. Ya no se ve la URL de 120 caracteres.
|
|
9
|
+
- URL de imagen (jpg/png/gif/webp/avif/svg y rutas `…/storage/media/…` sin extensión) → THUMBNAIL inline redondeado y clickeable que abre la imagen completa; `onError` degrada a link chip (nunca un icono de imagen rota). En la celda el thumbnail es chico (~h-8) para no romper la altura de fila; en el dialog es más grande.
|
|
10
|
+
- URL de archivo (pdf/zip/docx/mp4/…) → chip con icono `FileText` + nombre del archivo.
|
|
11
|
+
- URLs embebidas en texto largo (body, textarea, long-text) → se linkifican con el mismo estilo (bare o markdown `[label](url)`), recortando puntuación final y respetando paréntesis balanceados; imágenes y archivos embebidos también se renderizan inline.
|
|
12
|
+
|
|
13
|
+
Sin llamadas de red para renderizar (nada de servicio de favicons — CSP/privacidad): el icono es un glyph lucide local.
|
|
14
|
+
|
|
15
|
+
## 23.3.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
- 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).
|
|
21
|
+
|
|
22
|
+
El dialog ahora resuelve cada valor por el display type declarado (`cellStyle ?? type`), igual que la tabla:
|
|
23
|
+
- opciones/select → Badge con color resuelto y label localizado
|
|
24
|
+
- `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
|
|
25
|
+
- `cellStyle:'url'`/`'link'` en columna de texto (ej. `github_url`) → enlace clickeable en pestaña nueva, truncado
|
|
26
|
+
- `cellStyle:'datetime'` en columna numérica/epoch (ej. `synced_at`) → fecha formateada con timezone de la org, no dígitos crudos
|
|
27
|
+
- arrays de labels/tags → fila de badges (con color cuando el label trae `color`)
|
|
28
|
+
- creator/avatar → nombre + avatar
|
|
29
|
+
|
|
30
|
+
## 23.2.1
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 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.
|
|
35
|
+
|
|
3
36
|
## 23.2.0
|
|
4
37
|
|
|
5
38
|
### 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,9 @@ 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 { MediaValue, RichText } from '../rich-url';
|
|
32
|
+
import { generateBadgeStyles } from '@asteby/metacore-ui/lib';
|
|
30
33
|
import { CollectionCell } from '../collection-cell';
|
|
31
34
|
import { ImageUrlContext, identityImageUrl } from '../image-url-context';
|
|
32
35
|
import { TimeZoneContext, CurrencyContext } from '../org-runtime-context';
|
|
@@ -228,6 +231,19 @@ export function isMoneyField(field, value) {
|
|
|
228
231
|
return false;
|
|
229
232
|
return MONEY_KEY_HEURISTIC.some(m => key === m || key.endsWith(`_${m}`) || key.startsWith(`${m}_`));
|
|
230
233
|
}
|
|
234
|
+
// filterVisibleFields decides which declared fields render in the form for a
|
|
235
|
+
// given mode. `hidden` fields never render. A `readonly` (server/system-
|
|
236
|
+
// generated) field is EXCLUDED on create — the user can't set a value the
|
|
237
|
+
// server will overwrite — but stays visible on edit/view (rendered disabled).
|
|
238
|
+
export function filterVisibleFields(fields, mode) {
|
|
239
|
+
return (fields ?? []).filter(f => {
|
|
240
|
+
if (f.hidden)
|
|
241
|
+
return false;
|
|
242
|
+
if (mode === 'create' && f.readonly)
|
|
243
|
+
return false;
|
|
244
|
+
return true;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
231
247
|
export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
|
|
232
248
|
const api = useApi();
|
|
233
249
|
const { t } = useTranslation();
|
|
@@ -319,7 +335,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
319
335
|
catch (err) {
|
|
320
336
|
console.error('[DynamicRecordDialog] load error:', err);
|
|
321
337
|
if (!seed)
|
|
322
|
-
toast.error('
|
|
338
|
+
toast.error(t('dynamic.load_error', { defaultValue: 'No se pudieron cargar los datos' }));
|
|
323
339
|
}
|
|
324
340
|
finally {
|
|
325
341
|
if (!cancelled)
|
|
@@ -421,14 +437,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
421
437
|
try {
|
|
422
438
|
if (isCreate && onCreate) {
|
|
423
439
|
const created = await onCreate(formValues);
|
|
424
|
-
toast.success(modalMeta?.messages?.created || 'Registro creado correctamente');
|
|
440
|
+
toast.success(modalMeta?.messages?.created || t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' }));
|
|
425
441
|
onSaved?.(created ?? undefined);
|
|
426
442
|
onOpenChange(false);
|
|
427
443
|
return;
|
|
428
444
|
}
|
|
429
445
|
if (!isCreate && recordId && onUpdate) {
|
|
430
446
|
const updated = await onUpdate(String(recordId), formValues);
|
|
431
|
-
toast.success(modalMeta?.messages?.updated || 'Guardado correctamente');
|
|
447
|
+
toast.success(modalMeta?.messages?.updated || t('dynamic.update_success', { defaultValue: 'Guardado correctamente' }));
|
|
432
448
|
onSaved?.(updated ?? undefined);
|
|
433
449
|
onOpenChange(false);
|
|
434
450
|
return;
|
|
@@ -449,17 +465,19 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
449
465
|
// localized fallback. NOT res.data.message — the dynamic CRUD
|
|
450
466
|
// endpoint returns a raw English string that would leak into the toast.
|
|
451
467
|
toast.success(modalMeta?.messages?.[isCreate ? 'created' : 'updated']
|
|
452
|
-
|| (isCreate
|
|
468
|
+
|| (isCreate
|
|
469
|
+
? t('dynamic.create_success', { defaultValue: 'Registro creado correctamente' })
|
|
470
|
+
: t('dynamic.update_success', { defaultValue: 'Guardado correctamente' })));
|
|
453
471
|
// Hand the persisted record back so callers can auto-select it.
|
|
454
472
|
onSaved?.(res.data?.data ?? res.data ?? undefined);
|
|
455
473
|
onOpenChange(false);
|
|
456
474
|
}
|
|
457
475
|
else {
|
|
458
|
-
toast.error(res.data?.message || '
|
|
476
|
+
toast.error(res.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
|
|
459
477
|
}
|
|
460
478
|
}
|
|
461
479
|
catch (err) {
|
|
462
|
-
toast.error(err?.response?.data?.message || '
|
|
480
|
+
toast.error(err?.response?.data?.message || t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }));
|
|
463
481
|
}
|
|
464
482
|
finally {
|
|
465
483
|
setSaving(false);
|
|
@@ -475,20 +493,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
475
493
|
}
|
|
476
494
|
catch (err) {
|
|
477
495
|
console.error('[DynamicRecordDialog] delete error:', err);
|
|
478
|
-
toast.error(err?.response?.data?.message || err?.message || '
|
|
496
|
+
toast.error(err?.response?.data?.message || err?.message || t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }));
|
|
479
497
|
}
|
|
480
498
|
finally {
|
|
481
499
|
setDeleting(false);
|
|
482
500
|
}
|
|
483
501
|
};
|
|
484
502
|
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
|
-
}) ?? [];
|
|
503
|
+
const visibleFields = filterVisibleFields(modalMeta?.fields, mode);
|
|
492
504
|
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
505
|
const isFullWidth = field.type === 'textarea';
|
|
494
506
|
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 +510,26 @@ function LoadingSkeleton() {
|
|
|
498
510
|
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
511
|
}
|
|
500
512
|
function FieldRow({ field, record, value, mode, onChange }) {
|
|
501
|
-
|
|
502
|
-
|
|
513
|
+
// A `readonly` field is server/system-generated (e.g. the GitHub addon's
|
|
514
|
+
// `number`/`github_url`, filled by the API after the outbound create). On
|
|
515
|
+
// CREATE it is excluded from the form entirely (see `visibleFields`); on EDIT
|
|
516
|
+
// it stays visible but is NOT editable — rendered as a disabled, muted input
|
|
517
|
+
// so the user sees its value without being able to change it. View mode keeps
|
|
518
|
+
// the rich read-only renderer.
|
|
519
|
+
const isEditReadonly = mode === 'edit' && !!field.readonly;
|
|
520
|
+
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 }))] }));
|
|
521
|
+
}
|
|
522
|
+
// ReadonlyEditField — the edit-mode rendering of a `readonly` (system-generated)
|
|
523
|
+
// field: a disabled, muted input that shows the current value without allowing
|
|
524
|
+
// edits. Booleans render as a disabled switch to match their editable
|
|
525
|
+
// counterpart; everything else renders the formatted display value in a disabled
|
|
526
|
+
// text input.
|
|
527
|
+
export function ReadonlyEditField({ field, value }) {
|
|
528
|
+
if (field.type === 'boolean' || typeof value === 'boolean') {
|
|
529
|
+
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' })] }));
|
|
530
|
+
}
|
|
531
|
+
const display = formatDisplayValue(value, field);
|
|
532
|
+
return _jsx(Input, { value: display === '—' ? '' : display, disabled: true, readOnly: true, className: "text-muted-foreground" });
|
|
503
533
|
}
|
|
504
534
|
// RelationViewValue — read-only FK lead. Resolves the relation's label + image
|
|
505
535
|
// from (1) the sibling object the table served, then (2) the canonical options
|
|
@@ -549,8 +579,19 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
549
579
|
const getImageUrl = getImageUrlProp ?? ctxImageUrl;
|
|
550
580
|
const timeZone = timeZoneProp ?? ctxTimeZone;
|
|
551
581
|
const currency = currencyProp ?? ctxCurrency;
|
|
582
|
+
// Declarative display hint the backend stamps (mirrors the table column's
|
|
583
|
+
// `cellStyle`). The table renders each cell off `cellStyle ?? type`; the
|
|
584
|
+
// detail view keys off the SAME resolved renderer so both stay in lock-step
|
|
585
|
+
// (a `datetime` display on a numeric column, a `url` display on a text
|
|
586
|
+
// column, a `status`/`badge` pill, …).
|
|
587
|
+
const renderAs = field.cellStyle ?? field.type;
|
|
552
588
|
// created_by / avatar resolver sibling → name (+ avatar) instead of "—".
|
|
553
|
-
if (field.type === 'avatar' ||
|
|
589
|
+
if (field.type === 'avatar' ||
|
|
590
|
+
renderAs === 'avatar' ||
|
|
591
|
+
renderAs === 'creator' ||
|
|
592
|
+
renderAs === 'user' ||
|
|
593
|
+
field.key === 'created_by' ||
|
|
594
|
+
field.key === 'created_by_id') {
|
|
554
595
|
const user = createdBySibling(rawValue, record);
|
|
555
596
|
if (user) {
|
|
556
597
|
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 +634,15 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
593
634
|
(field.key === 'icon' || field.key.endsWith('_icon'))) {
|
|
594
635
|
return _jsx(IconNameViewValue, { name: value });
|
|
595
636
|
}
|
|
596
|
-
|
|
597
|
-
|
|
637
|
+
// URL/link display (matches the table's `url`/`link` cell). Triggers on the
|
|
638
|
+
// stamped display type — not just the storage `type` — so a text column
|
|
639
|
+
// carrying `cellStyle:'url'` (e.g. `github_url`) renders as a clickable
|
|
640
|
+
// external link, opening in a new tab, truncated.
|
|
641
|
+
if ((renderAs === 'url' || renderAs === 'link') && value) {
|
|
642
|
+
// Shared media renderer (same as the table cell): an image URL shows a
|
|
643
|
+
// larger inline thumbnail, a file a chip, else a compact link chip with
|
|
644
|
+
// a smart label — never the raw 120-char URL.
|
|
645
|
+
return (_jsx("div", { className: "py-1", children: _jsx(MediaValue, { url: String(value), getImageUrl: getImageUrl, label: field.styleConfig?.label, icon: field.styleConfig?.icon, thumbHeight: 200, maxLabelWidth: 360 }) }));
|
|
598
646
|
}
|
|
599
647
|
// Money → org-currency string. Detected by the backend `cellStyle:'currency'`
|
|
600
648
|
// stamp or a numeric value whose key matches the money heuristic (fallback
|
|
@@ -615,24 +663,54 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
615
663
|
}
|
|
616
664
|
// Date/datetime/timestamp → tz-aware format. `date` pins to UTC (calendar
|
|
617
665
|
// day); instants render in the org timezone with a full-precision tooltip.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
666
|
+
// Keys off the display type (`cellStyle ?? type`) so a numeric/epoch column
|
|
667
|
+
// stamped `datetime` (e.g. `synced_at`) formats as a date, never raw digits.
|
|
668
|
+
if (renderAs === 'date' ||
|
|
669
|
+
renderAs === 'datetime' ||
|
|
670
|
+
renderAs === 'timestamp' ||
|
|
671
|
+
renderAs === 'timestamptz') {
|
|
672
|
+
const dateRenderAs = renderAs === 'date' ? 'date' : renderAs;
|
|
673
|
+
const formatted = formatDateCell(value, dateRenderAs, es, timeZone);
|
|
621
674
|
if (formatted) {
|
|
622
675
|
return (_jsx("p", { className: "text-sm py-1", title: formatted.title, children: formatted.display }));
|
|
623
676
|
}
|
|
624
677
|
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
625
678
|
}
|
|
626
|
-
// Enum/option field with served options → colored/iconed
|
|
627
|
-
//
|
|
679
|
+
// Enum/option field with served options → the SAME colored/iconed pill the
|
|
680
|
+
// table renders (shared `OptionBadge`): resolved color, thumbnail/icon and
|
|
681
|
+
// the localized option label (e.g. "Almacenable" instead of "storable").
|
|
628
682
|
const opt = servedOption(field, value);
|
|
629
683
|
if (opt) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
684
|
+
return (_jsx("div", { className: "py-1", children: _jsx(OptionBadge, { option: {
|
|
685
|
+
value: String(opt.value ?? value ?? ''),
|
|
686
|
+
label: opt.label,
|
|
687
|
+
color: opt.color ?? undefined,
|
|
688
|
+
icon: opt.icon ?? undefined,
|
|
689
|
+
image: opt.image ?? undefined,
|
|
690
|
+
}, getImageUrl: getImageUrl }) }));
|
|
691
|
+
}
|
|
692
|
+
// Array of scalars / label objects (e.g. github `labels`, tags, a
|
|
693
|
+
// group-badge list) → a row of pills, mirroring the table's `tags` /
|
|
694
|
+
// `relation-badge-list` cells. Checked before the structured-object branch
|
|
695
|
+
// so a flat label array never renders as a mini-table.
|
|
696
|
+
if (Array.isArray(value) &&
|
|
697
|
+
(renderAs === 'tags' ||
|
|
698
|
+
renderAs === 'relation-badge-list' ||
|
|
699
|
+
value.every((v) => v === null || typeof v !== 'object' || 'label' in v || 'name' in v))) {
|
|
700
|
+
return _jsx(BadgeListViewValue, { items: value, getImageUrl: getImageUrl });
|
|
701
|
+
}
|
|
702
|
+
// Status / badge / select display with no served option list — a bare enum
|
|
703
|
+
// token (e.g. a kanban `stage` like "backlog"). Render a colored pill with a
|
|
704
|
+
// semantic/value-derived color and a localized-or-humanized label, matching
|
|
705
|
+
// the table's `status`/`badge` cells.
|
|
706
|
+
if ((renderAs === 'status' ||
|
|
707
|
+
renderAs === 'badge' ||
|
|
708
|
+
renderAs === 'select' ||
|
|
709
|
+
renderAs === 'option') &&
|
|
710
|
+
value !== null &&
|
|
711
|
+
value !== undefined &&
|
|
712
|
+
typeof value !== 'object') {
|
|
713
|
+
return _jsx(StatusBadgeViewValue, { field: field, value: value, t: t });
|
|
636
714
|
}
|
|
637
715
|
// Structured value (jsonb column, e.g. fiscal_data) with no label/name/title
|
|
638
716
|
// to surface — render readable key/value pairs instead of falling through to
|
|
@@ -641,10 +719,14 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
641
719
|
return (_jsx(StructuredViewValue, { value: value, field: field, locale: i18n.language, t: t }));
|
|
642
720
|
}
|
|
643
721
|
const display = formatDisplayValue(value, field);
|
|
644
|
-
|
|
645
|
-
|
|
722
|
+
// Free text may embed URLs (a github body, notes, a long-text field). Turn
|
|
723
|
+
// them into rich chips / inline thumbnails with the shared linkifier instead
|
|
724
|
+
// of showing raw URLs — the rest of the text is preserved verbatim.
|
|
725
|
+
const hasUrl = display !== '—' && /(https?:\/\/|www\.)\S/i.test(display);
|
|
726
|
+
if (field.type === 'textarea' || renderAs === 'textarea' || renderAs === 'long-text') {
|
|
727
|
+
return (_jsx("p", { className: "text-sm whitespace-pre-wrap rounded-md bg-muted/40 p-3 min-h-[60px] [&_img]:my-1", children: hasUrl ? (_jsx(RichText, { text: display, getImageUrl: getImageUrl, imageHeight: 200 })) : (display) }));
|
|
646
728
|
}
|
|
647
|
-
return _jsx("p", { className: "text-sm py-1", children: display });
|
|
729
|
+
return (_jsx("p", { className: "text-sm py-1", children: hasUrl ? (_jsx(RichText, { text: display, getImageUrl: getImageUrl, imageHeight: 160 })) : (display) }));
|
|
648
730
|
}
|
|
649
731
|
// IconNameViewValue — read view for a column whose value is a lucide icon name
|
|
650
732
|
// (an addon's `icon` column): the glyph plus the name, so the value stays
|
|
@@ -652,6 +734,44 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
652
734
|
function IconNameViewValue({ name }) {
|
|
653
735
|
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
736
|
}
|
|
737
|
+
// StatusBadgeViewValue — a bare enum/status token (no served option list) as a
|
|
738
|
+
// colored pill: a semantic/value-derived color (same `statusColorFor` the table
|
|
739
|
+
// uses) plus a localized-or-humanized label. Mirrors the table's `status`/
|
|
740
|
+
// `badge` cell so a kanban `stage` ("backlog") reads as a colored, translated
|
|
741
|
+
// badge instead of the raw token.
|
|
742
|
+
function StatusBadgeViewValue({ field, value, t, }) {
|
|
743
|
+
const isDark = useIsDarkTheme();
|
|
744
|
+
const token = String(value);
|
|
745
|
+
// Prefer an explicit per-option color served on the field (metadata.stages /
|
|
746
|
+
// options), else derive a semantic color from the token.
|
|
747
|
+
const declared = field.options?.find((o) => String(o.value) === token);
|
|
748
|
+
const color = declared?.color || statusColorFor(token);
|
|
749
|
+
// Localized label: the option's already-localized label wins, then a manifest
|
|
750
|
+
// i18n key matching the raw token, then a humanized fallback.
|
|
751
|
+
const label = declared?.label ?? t(token, { defaultValue: humanizeToken(token) });
|
|
752
|
+
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 }) }));
|
|
753
|
+
}
|
|
754
|
+
// BadgeListViewValue — an array of scalars / label objects as a row of pills
|
|
755
|
+
// (github `labels`, tags, a group-badge list). Objects carrying a `color` render
|
|
756
|
+
// as colored `OptionBadge`s; plain strings render as neutral secondary pills.
|
|
757
|
+
function BadgeListViewValue({ items, getImageUrl, }) {
|
|
758
|
+
if (!items || items.length === 0) {
|
|
759
|
+
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
760
|
+
}
|
|
761
|
+
return (_jsx("div", { className: "flex flex-wrap gap-1 py-1", children: items.map((item, i) => {
|
|
762
|
+
if (item !== null && typeof item === 'object') {
|
|
763
|
+
const opt = {
|
|
764
|
+
value: String(item.value ?? item.id ?? item.name ?? item.label ?? i),
|
|
765
|
+
label: String(item.label ?? item.name ?? item.value ?? ''),
|
|
766
|
+
color: item.color ?? undefined,
|
|
767
|
+
icon: item.icon ?? undefined,
|
|
768
|
+
image: item.image ?? item.avatar ?? undefined,
|
|
769
|
+
};
|
|
770
|
+
return _jsx(OptionBadge, { option: opt, getImageUrl: getImageUrl }, i);
|
|
771
|
+
}
|
|
772
|
+
return (_jsx(Badge, { variant: "secondary", className: "w-fit", children: String(item) }, i));
|
|
773
|
+
}) }));
|
|
774
|
+
}
|
|
655
775
|
// StructuredViewValue renders a jsonb object/array that has no resolvable label.
|
|
656
776
|
// It delegates to the shared `CollectionCell` in `'inline'` mode so the detail
|
|
657
777
|
// view gets the SAME pro rendering as the table: a declared `item_fields` schema
|
|
@@ -738,6 +858,7 @@ export function EditField({ field, value, onChange, record }) {
|
|
|
738
858
|
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
859
|
}
|
|
740
860
|
function ImageUploadField({ field: _field, value, onChange }) {
|
|
861
|
+
const { t } = useTranslation();
|
|
741
862
|
const api = useApi();
|
|
742
863
|
const model = useContext(ModelContext);
|
|
743
864
|
const getImageUrl = useContext(ImageUrlContext);
|
|
@@ -758,7 +879,7 @@ function ImageUploadField({ field: _field, value, onChange }) {
|
|
|
758
879
|
onChange(url);
|
|
759
880
|
}
|
|
760
881
|
catch {
|
|
761
|
-
toast.error('
|
|
882
|
+
toast.error(t('dynamic.image_upload_error', { defaultValue: 'No se pudo subir la imagen' }));
|
|
762
883
|
}
|
|
763
884
|
finally {
|
|
764
885
|
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;AAuC9C,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,CAqmBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|