@tinya-code/gas-admin-ui 0.0.0-stage → 0.1.0-alpha.1

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 (136) hide show
  1. package/README.md +63 -2
  2. package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs +281 -0
  3. package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs.map +1 -0
  4. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +84 -0
  5. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +1 -0
  6. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +140 -0
  7. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +1 -0
  8. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +113 -0
  9. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +1 -0
  10. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +163 -0
  11. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +1 -0
  12. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs +1022 -0
  13. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs.map +1 -0
  14. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +278 -0
  15. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +1 -0
  16. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs +101 -0
  17. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs.map +1 -0
  18. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +141 -0
  19. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +1 -0
  20. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs +89 -0
  21. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs.map +1 -0
  22. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +107 -0
  23. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +1 -0
  24. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +140 -0
  25. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +1 -0
  26. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +119 -0
  27. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +1 -0
  28. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +125 -0
  29. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +1 -0
  30. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +152 -0
  31. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +1 -0
  32. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +211 -0
  33. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +1 -0
  34. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +116 -0
  35. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +1 -0
  36. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +111 -0
  37. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +1 -0
  38. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +107 -0
  39. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +1 -0
  40. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +109 -0
  41. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +1 -0
  42. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs +45 -0
  43. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs.map +1 -0
  44. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +50 -0
  45. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +1 -0
  46. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +24 -0
  47. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +1 -0
  48. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +27 -0
  49. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +1 -0
  50. package/fesm2022/tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs +408 -0
  51. package/fesm2022/tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs.map +1 -0
  52. package/fesm2022/tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs +4514 -0
  53. package/fesm2022/tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map +1 -0
  54. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +82 -0
  55. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +1 -0
  56. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +105 -0
  57. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +1 -0
  58. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +65 -0
  59. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +1 -0
  60. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +65 -0
  61. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +1 -0
  62. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +65 -0
  63. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +1 -0
  64. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +59 -0
  65. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +1 -0
  66. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +94 -0
  67. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +1 -0
  68. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +72 -0
  69. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +1 -0
  70. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +238 -0
  71. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +1 -0
  72. package/fesm2022/tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs +416 -0
  73. package/fesm2022/tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs.map +1 -0
  74. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs +439 -0
  75. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs.map +1 -0
  76. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +513 -0
  77. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +1 -0
  78. package/fesm2022/tinya-code-gas-admin-ui-list-CMzl9MkX.mjs +637 -0
  79. package/fesm2022/tinya-code-gas-admin-ui-list-CMzl9MkX.mjs.map +1 -0
  80. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +127 -0
  81. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +1 -0
  82. package/fesm2022/tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs +251 -0
  83. package/fesm2022/tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs.map +1 -0
  84. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +66 -0
  85. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +1 -0
  86. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +114 -0
  87. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +1 -0
  88. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +116 -0
  89. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +1 -0
  90. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +235 -0
  91. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +1 -0
  92. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +8 -0
  93. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +1 -0
  94. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +124 -0
  95. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +1 -0
  96. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +249 -0
  97. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +1 -0
  98. package/fesm2022/tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs +309 -0
  99. package/fesm2022/tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs.map +1 -0
  100. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +39 -0
  101. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +1 -0
  102. package/fesm2022/tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs +546 -0
  103. package/fesm2022/tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs.map +1 -0
  104. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +11 -0
  105. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs +131 -0
  106. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs.map +1 -0
  107. package/fesm2022/tinya-code-gas-admin-ui-seed-CNbQtREA.mjs +80 -0
  108. package/fesm2022/tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map +1 -0
  109. package/fesm2022/tinya-code-gas-admin-ui-select-DzaRfHXO.mjs +292 -0
  110. package/fesm2022/tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map +1 -0
  111. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +119 -0
  112. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +1 -0
  113. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +158 -0
  114. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +1 -0
  115. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +26 -0
  116. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +1 -0
  117. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +205 -0
  118. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +1 -0
  119. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +432 -0
  120. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +1 -0
  121. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +36 -0
  122. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +1 -0
  123. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +107 -0
  124. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +1 -0
  125. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +223 -0
  126. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +1 -0
  127. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +107 -0
  128. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +1 -0
  129. package/fesm2022/tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs +60 -0
  130. package/fesm2022/tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs.map +1 -0
  131. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +141 -0
  132. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +1 -0
  133. package/fesm2022/tinya-code-gas-admin-ui.mjs +4400 -0
  134. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -0
  135. package/package.json +31 -3
  136. package/types/tinya-code-gas-admin-ui.d.ts +3091 -0
@@ -0,0 +1,4400 @@
1
+ import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
2
+ import { a as SESSION_USER_KEY, c as renewedTokenOf, i as SESSION_TOKEN_KEY, n as AuthService, o as API_URL, r as LOGIN_PATH, s as ApiError, t as ApiService } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
3
+ import { a as provideGasAdmin, c as apiErrorInterceptor, i as getSchema, n as allSchemas, o as setSchemaCatalog, r as getAuth, s as ConfigService, t as GAS_SCHEMAS } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
4
+ import { a as register, c as unregister, d as SUPPORTED_FIELD_TYPE_LIST, f as fieldDescriptors, i as provideRenderDescriptors, l as viewDescriptors, m as columnFormatDescriptors, n as get, o as renderKinds, p as COLUMN_FORMAT_TYPES, r as isSupported, u as layoutDescriptors } from "./tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs";
5
+ import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs";
6
+ import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
7
+ import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
8
+ import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
9
+ import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs";
10
+ import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs";
11
+ import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs";
12
+ import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
13
+ import { a as stringItemTree, i as listTree, n as groupTree, r as itemTree, t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
14
+ import { _ as resolveShare, a as DrawerForm, c as buildFormSchema, d as actionTypes, f as actionsFor, g as isTriggerAction, h as isShareAction, i as CopyShare, l as FieldHost, m as registerAction, o as buildPayload, p as getAction, r as Modal, s as seedModel, t as FormView, u as actionType, v as RenderHost } from "./tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs";
15
+ import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
16
+ import { t as getValue } from "./tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs";
17
+ import * as i0 from "@angular/core";
18
+ import { ChangeDetectionStrategy, Component, EnvironmentInjector, Injector, Service, ViewContainerRef, computed, effect, inject, input, model, output, resource, signal, viewChild } from "@angular/core";
19
+ import { catchError, combineLatest, distinctUntilChanged, firstValueFrom, map, of, switchMap } from "rxjs";
20
+ import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet, convertToParamMap } from "@angular/router";
21
+ import { LucideArrowLeft, LucideChevronDown, LucideChevronRight, LucideChevronUp, LucideCircleCheck, LucideCircleX, LucideDynamicIcon, LucideExternalLink, LucideEye, LucideGripVertical, LucideInfo, LucideMonitor, LucideMoon, LucidePencil, LucidePlus, LucideRotateCw, LucideSun, LucideTrash2, LucideX, LucideZap } from "@lucide/angular";
22
+ import { takeUntilDestroyed, toObservable, toSignal } from "@angular/core/rxjs-interop";
23
+ import { CdkDrag, CdkDragHandle, CdkDropList } from "@angular/cdk/drag-drop";
24
+ const authGuard = () => {
25
+ if (inject(AuthService).isAuthenticated()) return true;
26
+ return inject(Router).createUrlTree([LOGIN_PATH]);
27
+ };
28
+ const roleGuard = (route) => {
29
+ const auth = inject(AuthService);
30
+ const router = inject(Router);
31
+ if (!auth.isAuthenticated()) return router.createUrlTree([LOGIN_PATH]);
32
+ const id = route.data?.["resourceId"] ?? route.paramMap.get("id");
33
+ if (id === null) return true;
34
+ const schema = getSchema(String(id));
35
+ if (schema === void 0) return true;
36
+ return auth.canAccess(schema.access);
37
+ };
38
+ const MAX_DEPTH = 3;
39
+ var SchemaCompositionError = class extends Error {
40
+ constructor(message) {
41
+ super(message);
42
+ this.name = "SchemaCompositionError";
43
+ }
44
+ };
45
+ function configDe(vista) {
46
+ return "config" in vista ? vista.config : void 0;
47
+ }
48
+ function hijosDe(vista) {
49
+ const hijos = configDe(vista)?.children;
50
+ return Array.isArray(hijos) ? hijos : void 0;
51
+ }
52
+ function validateComposition(schema) {
53
+ const estado = {
54
+ esquema: schema,
55
+ nodos: 0,
56
+ contados: /* @__PURE__ */ new Set(),
57
+ camino: /* @__PURE__ */ new Set()
58
+ };
59
+ for (const [nombre, vista] of Object.entries(schema.uiViews ?? {})) {
60
+ estado.camino.add(nombre);
61
+ validarContenido(nombre, vista, 1, estado);
62
+ estado.camino.delete(nombre);
63
+ }
64
+ }
65
+ function validarContenido(nombre, vista, profundidadHijos, estado) {
66
+ const donde = `${estado.esquema.id}.${nombre}`;
67
+ validarSelect(donde, vista.select);
68
+ validarSource(donde, configDe(vista)?.source);
69
+ const hijos = hijosDe(vista);
70
+ if (!hijos) return;
71
+ validarHijos(nombre, hijos, profundidadHijos, estado);
72
+ }
73
+ function validarHijos(contenedor, hijos, profundidad, estado) {
74
+ const ids = /* @__PURE__ */ new Set();
75
+ for (const hijo of hijos) {
76
+ const id = hijo.id;
77
+ if (typeof id !== "string" || id === "") throw new SchemaCompositionError(`${estado.esquema.id}.${contenedor}: hijo sin id`);
78
+ if (ids.has(id)) throw new SchemaCompositionError(`${estado.esquema.id}.${contenedor}: id duplicado '${id}'`);
79
+ ids.add(id);
80
+ validarHijo(hijo, contenedor, id, profundidad, estado);
81
+ }
82
+ }
83
+ function validarHijo(hijo, contenedor, id, profundidad, estado) {
84
+ const donde = `${estado.esquema.id}.${contenedor} → child '${id}'`;
85
+ if (!estado.contados.has(hijo)) {
86
+ estado.contados.add(hijo);
87
+ estado.nodos += 1;
88
+ if (estado.nodos > 50) throw new SchemaCompositionError(`${estado.esquema.id}: la composición supera el límite de 50 nodos (observados ${estado.nodos})`);
89
+ }
90
+ if (profundidad > 3) throw new SchemaCompositionError(`${donde}: profundidad ${profundidad} supera el máximo 3`);
91
+ if ("ref" in hijo) {
92
+ validarRef(donde, hijo.ref, profundidad, estado);
93
+ return;
94
+ }
95
+ const tipo = hijo.type;
96
+ if (!get("view", tipo)) throw new SchemaCompositionError(`${donde}: tipo desconocido '${tipo}'`);
97
+ validarSelect(donde, hijo.select);
98
+ validarSource(donde, configDe(hijo)?.source);
99
+ const internos = hijosDe(hijo);
100
+ if (internos !== void 0) validarHijos(id, internos, profundidad + 1, estado);
101
+ }
102
+ function validarRef(donde, target, profundidad, estado) {
103
+ if (estado.camino.has(target)) throw new SchemaCompositionError(`${donde}: ciclo detectado — '${target}' ya está en el camino`);
104
+ const vista = estado.esquema.uiViews?.[target];
105
+ if (!vista) throw new SchemaCompositionError(`${donde}: ref '${target}' no existe en ${estado.esquema.id}.uiViews`);
106
+ estado.camino.add(target);
107
+ validarContenido(target, vista, profundidad + 1, estado);
108
+ estado.camino.delete(target);
109
+ }
110
+ function validarSelect(donde, select) {
111
+ if (select === void 0 || select.mode !== "route") return;
112
+ const target = select.target;
113
+ if (target === void 0 || target === null) throw new SchemaCompositionError(`${donde}: select mode 'route' sin target`);
114
+ if (typeof target === "string") throw new SchemaCompositionError(`${donde}: destino con URL cruda '${target}' — declará resource + view`);
115
+ validarTarget(donde, target);
116
+ }
117
+ function validarTarget(donde, target) {
118
+ const { resource, view, params } = target;
119
+ if (typeof resource !== "string" || resource === "" || typeof view !== "string" || view === "") throw new SchemaCompositionError(`${donde}: destino sin resource + view`);
120
+ const destino = getSchema(resource);
121
+ if (!destino) throw new SchemaCompositionError(`${donde}: destino '${resource}' no existe`);
122
+ const vista = destino.uiViews?.[view];
123
+ if (!vista) throw new SchemaCompositionError(`${donde}: destino ${resource}.${view} no tiene uiView declarada`);
124
+ const paramsRuta = (vista.path ?? "").split("/").filter((segmento) => segmento.startsWith(":")).map((segmento) => segmento.slice(1));
125
+ const declarados = params ?? [];
126
+ for (const param of declarados) if (!paramsRuta.includes(param)) throw new SchemaCompositionError(`${donde}: param '${param}' no existe en la ruta ${resource}.${view}`);
127
+ for (const param of paramsRuta) if (!declarados.includes(param)) throw new SchemaCompositionError(`${donde}: falta declarar el param '${param}' de la ruta ${resource}.${view}`);
128
+ }
129
+ function validarSource(donde, source) {
130
+ if (source === void 0) return;
131
+ if (typeof source.resource !== "string" || source.resource === "") throw new SchemaCompositionError(`${donde}: source sin resource`);
132
+ const origen = getSchema(source.resource);
133
+ if (!origen) throw new SchemaCompositionError(`${donde}: source '${source.resource}' no existe`);
134
+ if (source.view !== void 0 && origen.views?.[source.view] === void 0) throw new SchemaCompositionError(`${donde}: la vista '${source.view}' no existe en ${source.resource}.views`);
135
+ }
136
+ const unsavedChangesGuard = (component) => {
137
+ return component?.canDeactivate?.() ?? true;
138
+ };
139
+ function instanceIdFor(parentId, childId) {
140
+ return `${parentId}/${childId}`;
141
+ }
142
+ function virtualSchemaFor(schema, instanceId, view) {
143
+ return {
144
+ ...schema,
145
+ uiViews: {
146
+ ...schema.uiViews,
147
+ [instanceId]: view
148
+ }
149
+ };
150
+ }
151
+ function syntheticRoute(data) {
152
+ return {
153
+ data: of(data),
154
+ paramMap: of(convertToParamMap({}))
155
+ };
156
+ }
157
+ function createViewContext(base, emit) {
158
+ return emit === void 0 ? { ...base } : {
159
+ ...base,
160
+ emit
161
+ };
162
+ }
163
+ const STORAGE_KEY = "admin-theme";
164
+ const QUERY = "(prefers-color-scheme: dark)";
165
+ const MODES = [
166
+ "system",
167
+ "light",
168
+ "dark"
169
+ ];
170
+ function storedMode() {
171
+ try {
172
+ const raw = localStorage.getItem(STORAGE_KEY);
173
+ return raw !== null && MODES.includes(raw) ? raw : "system";
174
+ } catch {
175
+ return "system";
176
+ }
177
+ }
178
+ function prefersDark() {
179
+ return typeof window.matchMedia === "function" && window.matchMedia(QUERY).matches;
180
+ }
181
+ function resolve(mode) {
182
+ return mode === "system" ? prefersDark() ? "dark" : "light" : mode;
183
+ }
184
+ var ThemeService = class ThemeService {
185
+ mode = signal(storedMode(), ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
186
+ resolved = signal(resolve(this.mode()), ...ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []);
187
+ constructor() {
188
+ this.apply();
189
+ (typeof window.matchMedia === "function" ? window.matchMedia(QUERY) : null)?.addEventListener("change", () => {
190
+ if (this.mode() === "system") this.apply();
191
+ });
192
+ }
193
+ set(mode) {
194
+ this.mode.set(mode);
195
+ try {
196
+ localStorage.setItem(STORAGE_KEY, mode);
197
+ } catch {}
198
+ this.apply();
199
+ }
200
+ apply() {
201
+ const resolved = resolve(this.mode());
202
+ this.resolved.set(resolved);
203
+ document.documentElement.dataset["theme"] = resolved;
204
+ document.documentElement.dataset["themeMode"] = this.mode();
205
+ }
206
+ static ɵfac = i0.ɵɵngDeclareFactory({
207
+ minVersion: "12.0.0",
208
+ version: "22.2.3",
209
+ ngImport: i0,
210
+ type: ThemeService,
211
+ deps: [],
212
+ target: i0.ɵɵFactoryTarget.Service
213
+ });
214
+ static ɵprov = i0.ɵɵngDeclareService({
215
+ minVersion: "22.0.0",
216
+ version: "22.2.3",
217
+ ngImport: i0,
218
+ type: ThemeService
219
+ });
220
+ };
221
+ i0.ɵɵngDeclareClassMetadata({
222
+ minVersion: "12.0.0",
223
+ version: "22.2.3",
224
+ ngImport: i0,
225
+ type: ThemeService,
226
+ decorators: [{ type: Service }],
227
+ ctorParameters: () => []
228
+ });
229
+ const BASE = "rounded-md p-1.5 transition-colors min-h-11 min-w-11 lg:min-h-0 lg:min-w-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
230
+ const ACTIVE = "bg-primary/10 text-primary";
231
+ const IDLE = "text-neutral hover:bg-neutral/10";
232
+ var ThemeToggle = class ThemeToggle {
233
+ theme = inject(ThemeService);
234
+ classes(mode) {
235
+ return `${BASE} ${this.theme.mode() === mode ? ACTIVE : IDLE}`;
236
+ }
237
+ static ɵfac = i0.ɵɵngDeclareFactory({
238
+ minVersion: "12.0.0",
239
+ version: "22.2.3",
240
+ ngImport: i0,
241
+ type: ThemeToggle,
242
+ deps: [],
243
+ target: i0.ɵɵFactoryTarget.Component
244
+ });
245
+ static ɵcmp = i0.ɵɵngDeclareComponent({
246
+ minVersion: "14.0.0",
247
+ version: "22.2.3",
248
+ type: ThemeToggle,
249
+ isStandalone: true,
250
+ selector: "app-theme-toggle",
251
+ ngImport: i0,
252
+ template: `
253
+ <div
254
+ role="group"
255
+ aria-label="Tema del panel"
256
+ class="inline-flex items-center gap-0.5 rounded-lg border border-neutral/20 bg-surface p-0.5"
257
+ >
258
+ <button
259
+ type="button"
260
+ [class]="classes('system')"
261
+ [attr.aria-pressed]="theme.mode() === 'system'"
262
+ aria-label="Tema: seguir al sistema"
263
+ title="Seguir al sistema"
264
+ (click)="theme.set('system')"
265
+ >
266
+ <svg lucideMonitor size="16" />
267
+ </button>
268
+ <button
269
+ type="button"
270
+ [class]="classes('light')"
271
+ [attr.aria-pressed]="theme.mode() === 'light'"
272
+ aria-label="Tema: claro"
273
+ title="Tema claro"
274
+ (click)="theme.set('light')"
275
+ >
276
+ <svg lucideSun size="16" />
277
+ </button>
278
+ <button
279
+ type="button"
280
+ [class]="classes('dark')"
281
+ [attr.aria-pressed]="theme.mode() === 'dark'"
282
+ aria-label="Tema: oscuro"
283
+ title="Tema oscuro"
284
+ (click)="theme.set('dark')"
285
+ >
286
+ <svg lucideMoon size="16" />
287
+ </button>
288
+ </div>
289
+ `,
290
+ isInline: true,
291
+ dependencies: [
292
+ {
293
+ kind: "component",
294
+ type: LucideMonitor,
295
+ selector: "svg[lucideMonitor]"
296
+ },
297
+ {
298
+ kind: "component",
299
+ type: LucideSun,
300
+ selector: "svg[lucideSun]"
301
+ },
302
+ {
303
+ kind: "component",
304
+ type: LucideMoon,
305
+ selector: "svg[lucideMoon]"
306
+ }
307
+ ]
308
+ });
309
+ };
310
+ i0.ɵɵngDeclareClassMetadata({
311
+ minVersion: "12.0.0",
312
+ version: "22.2.3",
313
+ ngImport: i0,
314
+ type: ThemeToggle,
315
+ decorators: [{
316
+ type: Component,
317
+ args: [{
318
+ selector: "app-theme-toggle",
319
+ imports: [
320
+ LucideMonitor,
321
+ LucideSun,
322
+ LucideMoon
323
+ ],
324
+ template: `
325
+ <div
326
+ role="group"
327
+ aria-label="Tema del panel"
328
+ class="inline-flex items-center gap-0.5 rounded-lg border border-neutral/20 bg-surface p-0.5"
329
+ >
330
+ <button
331
+ type="button"
332
+ [class]="classes('system')"
333
+ [attr.aria-pressed]="theme.mode() === 'system'"
334
+ aria-label="Tema: seguir al sistema"
335
+ title="Seguir al sistema"
336
+ (click)="theme.set('system')"
337
+ >
338
+ <svg lucideMonitor size="16" />
339
+ </button>
340
+ <button
341
+ type="button"
342
+ [class]="classes('light')"
343
+ [attr.aria-pressed]="theme.mode() === 'light'"
344
+ aria-label="Tema: claro"
345
+ title="Tema claro"
346
+ (click)="theme.set('light')"
347
+ >
348
+ <svg lucideSun size="16" />
349
+ </button>
350
+ <button
351
+ type="button"
352
+ [class]="classes('dark')"
353
+ [attr.aria-pressed]="theme.mode() === 'dark'"
354
+ aria-label="Tema: oscuro"
355
+ title="Tema oscuro"
356
+ (click)="theme.set('dark')"
357
+ >
358
+ <svg lucideMoon size="16" />
359
+ </button>
360
+ </div>
361
+ `
362
+ }]
363
+ }]
364
+ });
365
+ var Sidebar = class Sidebar {
366
+ open = model(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
367
+ auth = inject(AuthService);
368
+ notifications = inject(NotificationService);
369
+ isAuthenticated = this.auth.isAuthenticated;
370
+ closingAllSessions = signal(false, ...ngDevMode ? [{ debugName: "closingAllSessions" }] : /* istanbul ignore next */ []);
371
+ menuItems = computed(() => allSchemas().filter((schema) => isVisible(schema) && this.auth.canAccess(schema.access)).map((schema) => ({
372
+ id: schema.id,
373
+ label: schema.labelPlural
374
+ })), ...ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []);
375
+ async closeAllSessions() {
376
+ if (!await this.notifications.confirm({
377
+ title: "Cerrar todas las sesiones",
378
+ message: "Se cerrarán las sesiones abiertas en todos tus dispositivos. ¿Continuar?",
379
+ confirmLabel: "Cerrar sesiones"
380
+ })) return;
381
+ this.closingAllSessions.set(true);
382
+ this.auth.logoutAll().subscribe({
383
+ next: ({ effectiveWithinSeconds }) => {
384
+ this.notifications.success(`Sesiones revocadas. El cambio se aplica en un máximo de ${effectiveWithinSeconds} segundos.`);
385
+ },
386
+ error: () => {
387
+ this.closingAllSessions.set(false);
388
+ this.notifications.error("No se pudieron cerrar todas las sesiones. Inténtalo de nuevo.");
389
+ },
390
+ complete: () => this.closingAllSessions.set(false)
391
+ });
392
+ }
393
+ static ɵfac = i0.ɵɵngDeclareFactory({
394
+ minVersion: "12.0.0",
395
+ version: "22.2.3",
396
+ ngImport: i0,
397
+ type: Sidebar,
398
+ deps: [],
399
+ target: i0.ɵɵFactoryTarget.Component
400
+ });
401
+ static ɵcmp = i0.ɵɵngDeclareComponent({
402
+ minVersion: "17.0.0",
403
+ version: "22.2.3",
404
+ type: Sidebar,
405
+ isStandalone: true,
406
+ selector: "app-sidebar",
407
+ inputs: { open: {
408
+ classPropertyName: "open",
409
+ publicName: "open",
410
+ isSignal: true,
411
+ isRequired: false,
412
+ transformFunction: null
413
+ } },
414
+ outputs: { open: "openChange" },
415
+ ngImport: i0,
416
+ template: `
417
+ <!-- Offcanvas en mobile (transform); las clases lg: la dejan estática
418
+ y siempre visible en pantallas grandes. -->
419
+ <aside
420
+ class="fixed inset-y-0 left-0 z-40 w-60 shrink-0 h-[100svh] min-h-[80svh] lg:sticky border-r border-neutral/20 bg-surface-raised transition-transform duration-200 lg:static lg:z-auto lg:translate-x-0"
421
+ [class.-translate-x-full]="!open()"
422
+ >
423
+ <div class="flex items-center justify-between gap-2 border-b border-neutral/20 px-4 py-4">
424
+ <!-- min-w-0+truncate: si el toggle exige su ancho, el rótulo se
425
+ trunca en vez de desbordar la sidebar de 240 px. -->
426
+ <span class="min-w-0 truncate font-display text-base font-semibold">Admin</span>
427
+ <app-theme-toggle />
428
+ </div>
429
+ <nav aria-label="Principal" class="p-3">
430
+ <ul class="space-y-2 lg:space-y-1">
431
+ @for (item of menuItems(); track item.id) {
432
+ <li>
433
+ <a
434
+ [routerLink]="['/', item.id]"
435
+ routerLinkActive="bg-primary/10 text-primary"
436
+ class="flex min-h-11 items-center rounded-lg px-3 py-2 text-sm font-medium text-neutral hover:bg-neutral/10"
437
+ (click)="open.set(false)"
438
+ >
439
+ {{ item.label }}
440
+ </a>
441
+ </li>
442
+ }
443
+ </ul>
444
+ </nav>
445
+ @if (isAuthenticated()) {
446
+ <div class="border-t border-neutral/20 p-3">
447
+ <button
448
+ type="button"
449
+ class="w-full rounded-lg px-3 py-2 text-left text-sm text-neutral hover:bg-neutral/10 disabled:opacity-50"
450
+ [disabled]="closingAllSessions()"
451
+ (click)="closeAllSessions()"
452
+ >
453
+ {{ closingAllSessions() ? 'Cerrando sesiones…' : 'Cerrar todas las sesiones' }}
454
+ </button>
455
+ </div>
456
+ }
457
+ </aside>
458
+ `,
459
+ isInline: true,
460
+ dependencies: [
461
+ {
462
+ kind: "directive",
463
+ type: RouterLink,
464
+ selector: "[routerLink]",
465
+ inputs: [
466
+ "target",
467
+ "queryParams",
468
+ "fragment",
469
+ "queryParamsHandling",
470
+ "state",
471
+ "info",
472
+ "relativeTo",
473
+ "preserveFragment",
474
+ "skipLocationChange",
475
+ "replaceUrl",
476
+ "browserUrl",
477
+ "routerLink"
478
+ ]
479
+ },
480
+ {
481
+ kind: "directive",
482
+ type: RouterLinkActive,
483
+ selector: "[routerLinkActive]",
484
+ inputs: [
485
+ "routerLinkActiveOptions",
486
+ "ariaCurrentWhenActive",
487
+ "routerLinkActive"
488
+ ],
489
+ outputs: ["isActiveChange"],
490
+ exportAs: ["routerLinkActive"]
491
+ },
492
+ {
493
+ kind: "component",
494
+ type: ThemeToggle,
495
+ selector: "app-theme-toggle"
496
+ }
497
+ ]
498
+ });
499
+ };
500
+ i0.ɵɵngDeclareClassMetadata({
501
+ minVersion: "12.0.0",
502
+ version: "22.2.3",
503
+ ngImport: i0,
504
+ type: Sidebar,
505
+ decorators: [{
506
+ type: Component,
507
+ args: [{
508
+ selector: "app-sidebar",
509
+ imports: [
510
+ RouterLink,
511
+ RouterLinkActive,
512
+ ThemeToggle
513
+ ],
514
+ template: `
515
+ <!-- Offcanvas en mobile (transform); las clases lg: la dejan estática
516
+ y siempre visible en pantallas grandes. -->
517
+ <aside
518
+ class="fixed inset-y-0 left-0 z-40 w-60 shrink-0 h-[100svh] min-h-[80svh] lg:sticky border-r border-neutral/20 bg-surface-raised transition-transform duration-200 lg:static lg:z-auto lg:translate-x-0"
519
+ [class.-translate-x-full]="!open()"
520
+ >
521
+ <div class="flex items-center justify-between gap-2 border-b border-neutral/20 px-4 py-4">
522
+ <!-- min-w-0+truncate: si el toggle exige su ancho, el rótulo se
523
+ trunca en vez de desbordar la sidebar de 240 px. -->
524
+ <span class="min-w-0 truncate font-display text-base font-semibold">Admin</span>
525
+ <app-theme-toggle />
526
+ </div>
527
+ <nav aria-label="Principal" class="p-3">
528
+ <ul class="space-y-2 lg:space-y-1">
529
+ @for (item of menuItems(); track item.id) {
530
+ <li>
531
+ <a
532
+ [routerLink]="['/', item.id]"
533
+ routerLinkActive="bg-primary/10 text-primary"
534
+ class="flex min-h-11 items-center rounded-lg px-3 py-2 text-sm font-medium text-neutral hover:bg-neutral/10"
535
+ (click)="open.set(false)"
536
+ >
537
+ {{ item.label }}
538
+ </a>
539
+ </li>
540
+ }
541
+ </ul>
542
+ </nav>
543
+ @if (isAuthenticated()) {
544
+ <div class="border-t border-neutral/20 p-3">
545
+ <button
546
+ type="button"
547
+ class="w-full rounded-lg px-3 py-2 text-left text-sm text-neutral hover:bg-neutral/10 disabled:opacity-50"
548
+ [disabled]="closingAllSessions()"
549
+ (click)="closeAllSessions()"
550
+ >
551
+ {{ closingAllSessions() ? 'Cerrando sesiones…' : 'Cerrar todas las sesiones' }}
552
+ </button>
553
+ </div>
554
+ }
555
+ </aside>
556
+ `
557
+ }]
558
+ }],
559
+ propDecorators: { open: [{
560
+ type: i0.Input,
561
+ args: [{
562
+ isSignal: true,
563
+ alias: "open",
564
+ required: false
565
+ }]
566
+ }, {
567
+ type: i0.Output,
568
+ args: ["openChange"]
569
+ }] }
570
+ });
571
+ function isVisible(schema) {
572
+ if (schema.policies?.access) return true;
573
+ const permissions = schema.permissions;
574
+ if (!permissions) return true;
575
+ return permissions.create !== false || permissions.update !== false || permissions.remove !== false;
576
+ }
577
+ var Layout = class Layout {
578
+ sidebarOpen = signal(false, ...ngDevMode ? [{ debugName: "sidebarOpen" }] : /* istanbul ignore next */ []);
579
+ static ɵfac = i0.ɵɵngDeclareFactory({
580
+ minVersion: "12.0.0",
581
+ version: "22.2.3",
582
+ ngImport: i0,
583
+ type: Layout,
584
+ deps: [],
585
+ target: i0.ɵɵFactoryTarget.Component
586
+ });
587
+ static ɵcmp = i0.ɵɵngDeclareComponent({
588
+ minVersion: "17.0.0",
589
+ version: "22.2.3",
590
+ type: Layout,
591
+ isStandalone: true,
592
+ selector: "app-layout",
593
+ ngImport: i0,
594
+ template: `
595
+ <div class="flex min-h-screen bg-neutral/5">
596
+ <button
597
+ app-button
598
+ type="button"
599
+ variant="outline"
600
+ size="icon"
601
+ class="fixed top-[calc(env(safe-area-inset-top)+0.75rem)] left-3 z-40 shadow-md lg:hidden"
602
+ [attr.aria-expanded]="sidebarOpen()"
603
+ aria-controls="main-sidebar"
604
+ aria-label="Abrir menú"
605
+ (click)="sidebarOpen.set(true)"
606
+ >
607
+ <svg
608
+ aria-hidden="true"
609
+ class="size-5"
610
+ fill="none"
611
+ stroke="currentColor"
612
+ stroke-width="2"
613
+ viewBox="0 0 24 24"
614
+ >
615
+ <path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16" />
616
+ </svg>
617
+ </button>
618
+ @if (sidebarOpen()) {
619
+ <div
620
+ class="fixed inset-0 z-30 bg-black/30 lg:hidden"
621
+ aria-hidden="true"
622
+ (click)="sidebarOpen.set(false)"
623
+ ></div>
624
+ }
625
+ <app-sidebar id="main-sidebar" [(open)]="sidebarOpen" />
626
+ <!-- pt reafirmado en sm: sin esto la variante sm:p-6 (va después en la
627
+ cascada) pisa el pt-16 base y el contenido choca con la hamburguesa.
628
+ base/sm dejan pasar el safe-area del notch; en lg no hay hamburguesa. -->
629
+ <main
630
+ class="min-w-0 flex-1 p-4 pt-[calc(4rem+env(safe-area-inset-top))] sm:p-6 sm:pt-[calc(4rem+env(safe-area-inset-top))] lg:p-8"
631
+ >
632
+ <router-outlet />
633
+ </main>
634
+ </div>
635
+ `,
636
+ isInline: true,
637
+ dependencies: [
638
+ {
639
+ kind: "directive",
640
+ type: RouterOutlet,
641
+ selector: "router-outlet",
642
+ inputs: ["name", "routerOutletData"],
643
+ outputs: [
644
+ "activate",
645
+ "deactivate",
646
+ "attach",
647
+ "detach"
648
+ ],
649
+ exportAs: ["outlet"]
650
+ },
651
+ {
652
+ kind: "component",
653
+ type: Sidebar,
654
+ selector: "app-sidebar",
655
+ inputs: ["open"],
656
+ outputs: ["openChange"]
657
+ },
658
+ {
659
+ kind: "component",
660
+ type: Button,
661
+ selector: "button[app-button]",
662
+ inputs: [
663
+ "variant",
664
+ "size",
665
+ "type",
666
+ "disabled",
667
+ "loading"
668
+ ]
669
+ }
670
+ ]
671
+ });
672
+ };
673
+ i0.ɵɵngDeclareClassMetadata({
674
+ minVersion: "12.0.0",
675
+ version: "22.2.3",
676
+ ngImport: i0,
677
+ type: Layout,
678
+ decorators: [{
679
+ type: Component,
680
+ args: [{
681
+ selector: "app-layout",
682
+ imports: [
683
+ RouterOutlet,
684
+ Sidebar,
685
+ Button
686
+ ],
687
+ template: `
688
+ <div class="flex min-h-screen bg-neutral/5">
689
+ <button
690
+ app-button
691
+ type="button"
692
+ variant="outline"
693
+ size="icon"
694
+ class="fixed top-[calc(env(safe-area-inset-top)+0.75rem)] left-3 z-40 shadow-md lg:hidden"
695
+ [attr.aria-expanded]="sidebarOpen()"
696
+ aria-controls="main-sidebar"
697
+ aria-label="Abrir menú"
698
+ (click)="sidebarOpen.set(true)"
699
+ >
700
+ <svg
701
+ aria-hidden="true"
702
+ class="size-5"
703
+ fill="none"
704
+ stroke="currentColor"
705
+ stroke-width="2"
706
+ viewBox="0 0 24 24"
707
+ >
708
+ <path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16" />
709
+ </svg>
710
+ </button>
711
+ @if (sidebarOpen()) {
712
+ <div
713
+ class="fixed inset-0 z-30 bg-black/30 lg:hidden"
714
+ aria-hidden="true"
715
+ (click)="sidebarOpen.set(false)"
716
+ ></div>
717
+ }
718
+ <app-sidebar id="main-sidebar" [(open)]="sidebarOpen" />
719
+ <!-- pt reafirmado en sm: sin esto la variante sm:p-6 (va después en la
720
+ cascada) pisa el pt-16 base y el contenido choca con la hamburguesa.
721
+ base/sm dejan pasar el safe-area del notch; en lg no hay hamburguesa. -->
722
+ <main
723
+ class="min-w-0 flex-1 p-4 pt-[calc(4rem+env(safe-area-inset-top))] sm:p-6 sm:pt-[calc(4rem+env(safe-area-inset-top))] lg:p-8"
724
+ >
725
+ <router-outlet />
726
+ </main>
727
+ </div>
728
+ `
729
+ }]
730
+ }]
731
+ });
732
+ function pathRank(path) {
733
+ const segments = path.split("/");
734
+ return [segments.length, segments.filter((segment) => !segment.startsWith(":")).length];
735
+ }
736
+ function compareRoutes(a, b) {
737
+ const aRank = pathRank(a.path ?? "");
738
+ const bRank = pathRank(b.path ?? "");
739
+ return bRank[0] - aRank[0] || bRank[1] - aRank[1];
740
+ }
741
+ function resourceRoutes(catalog = allSchemas()) {
742
+ return catalog.flatMap((schema) => {
743
+ if (!schema.uiViews || Object.keys(schema.uiViews).length === 0) throw new Error(`El recurso '${schema.id}' no declara uiViews`);
744
+ validateComposition(schema);
745
+ return Object.entries(schema.uiViews).map(([name, view]) => {
746
+ const descriptor = get("view", view.type);
747
+ if (!descriptor) throw new Error(`No existe descriptor kind:view type '${view.type}' para ${schema.id}.${name}`);
748
+ const relativePath = view.path ?? "";
749
+ if (relativePath.startsWith("/") || relativePath.endsWith("/") || relativePath !== "" && relativePath.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error(`Ruta uiViews inválida en ${schema.id}.${name}: '${relativePath}'`);
750
+ const route = {
751
+ path: [schema.id, relativePath].filter(Boolean).join("/"),
752
+ loadComponent: descriptor.load,
753
+ canActivate: [roleGuard],
754
+ data: {
755
+ resourceId: schema.id,
756
+ uiView: name,
757
+ viewType: view.type
758
+ },
759
+ resolve: { viewContext: () => {
760
+ const base = {
761
+ resourceId: schema.id,
762
+ uiView: name,
763
+ viewType: view.type,
764
+ ...view.select !== void 0 ? { select: view.select } : {}
765
+ };
766
+ const select = view.select;
767
+ if (select?.mode !== "route") return createViewContext(base);
768
+ const nav = inject(ViewNavigationService);
769
+ return createViewContext(base, (event) => {
770
+ nav.dispatch(select, event);
771
+ });
772
+ } }
773
+ };
774
+ if (view.type === "form") route.canDeactivate = [unsavedChangesGuard];
775
+ return route;
776
+ });
777
+ }).sort(compareRoutes);
778
+ }
779
+ const shellRoutes = [{
780
+ path: "",
781
+ component: Layout,
782
+ children: resourceRoutes()
783
+ }];
784
+ const LABELS = {
785
+ email: "Correo electrónico",
786
+ password: "Contraseña"
787
+ };
788
+ const FIELD_BASE = "mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
789
+ var Login = class Login {
790
+ auth = inject(AuthService);
791
+ router = inject(Router);
792
+ schema = getAuth();
793
+ error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
794
+ loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
795
+ values = signal({}, ...ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []);
796
+ label(key) {
797
+ return LABELS[key] ?? key;
798
+ }
799
+ value(key) {
800
+ return this.values()[key] ?? "";
801
+ }
802
+ setValue(key, next) {
803
+ this.values.update((current) => ({
804
+ ...current,
805
+ [key]: next
806
+ }));
807
+ }
808
+ submit(event) {
809
+ event.preventDefault();
810
+ if (this.loading()) return;
811
+ const email = this.value("email").trim();
812
+ const password = this.value("password");
813
+ if (email === "" || password === "") {
814
+ this.error.set("Completa el correo y la contraseña.");
815
+ return;
816
+ }
817
+ this.loading.set(true);
818
+ this.error.set(null);
819
+ this.auth.login(email, password).subscribe({
820
+ next: () => {
821
+ this.loading.set(false);
822
+ this.goAfterLogin();
823
+ },
824
+ error: (err) => {
825
+ this.loading.set(false);
826
+ this.error.set(err instanceof ApiError && err.message !== "" ? err.message : "No se pudo iniciar sesión.");
827
+ }
828
+ });
829
+ }
830
+ goAfterLogin() {
831
+ const previous = this.auth.consumeReturnUrl();
832
+ const target = previous !== null && previous !== "" && !previous.startsWith("/login") ? previous : this.schema.redirectAfterLogin;
833
+ this.router.navigateByUrl(target).catch(() => void 0);
834
+ }
835
+ static ɵfac = i0.ɵɵngDeclareFactory({
836
+ minVersion: "12.0.0",
837
+ version: "22.2.3",
838
+ ngImport: i0,
839
+ type: Login,
840
+ deps: [],
841
+ target: i0.ɵɵFactoryTarget.Component
842
+ });
843
+ static ɵcmp = i0.ɵɵngDeclareComponent({
844
+ minVersion: "17.0.0",
845
+ version: "22.2.3",
846
+ type: Login,
847
+ isStandalone: true,
848
+ selector: "app-login",
849
+ ngImport: i0,
850
+ template: "\n <main class=\"flex min-h-[100svh] items-center justify-center bg-surface px-4\">\n <div\n class=\"w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm\"\n >\n <h1 class=\"font-display text-lg font-semibold\">Admin</h1>\n <p class=\"mt-1 text-sm text-neutral\">Inicia sesión para continuar</p>\n\n <form class=\"mt-6 space-y-4\" (submit)=\"submit($event)\" novalidate>\n @for (field of schema.loginFields; track field.key) {\n <div>\n <label [attr.for]=\"field.key\" class=\"block text-sm font-medium text-neutral\">\n {{ label(field.key) }}\n </label>\n\n @switch (field.type) {\n @case ('email') {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"email\"\n autocomplete=\"email\"\n inputmode=\"email\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n @default {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"password\"\n autocomplete=\"current-password\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n }\n </div>\n }\n\n @if (error(); as message) {\n <p role=\"alert\" class=\"rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger\">\n {{ message }}\n </p>\n }\n\n <button app-button type=\"submit\" [loading]=\"loading()\" class=\"w-full\">\n Iniciar sesión\n </button>\n </form>\n </div>\n </main>\n ",
851
+ isInline: true,
852
+ dependencies: [{
853
+ kind: "component",
854
+ type: Button,
855
+ selector: "button[app-button]",
856
+ inputs: [
857
+ "variant",
858
+ "size",
859
+ "type",
860
+ "disabled",
861
+ "loading"
862
+ ]
863
+ }]
864
+ });
865
+ };
866
+ i0.ɵɵngDeclareClassMetadata({
867
+ minVersion: "12.0.0",
868
+ version: "22.2.3",
869
+ ngImport: i0,
870
+ type: Login,
871
+ decorators: [{
872
+ type: Component,
873
+ args: [{
874
+ selector: "app-login",
875
+ imports: [Button],
876
+ template: `
877
+ <main class="flex min-h-[100svh] items-center justify-center bg-surface px-4">
878
+ <div
879
+ class="w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm"
880
+ >
881
+ <h1 class="font-display text-lg font-semibold">Admin</h1>
882
+ <p class="mt-1 text-sm text-neutral">Inicia sesión para continuar</p>
883
+
884
+ <form class="mt-6 space-y-4" (submit)="submit($event)" novalidate>
885
+ @for (field of schema.loginFields; track field.key) {
886
+ <div>
887
+ <label [attr.for]="field.key" class="block text-sm font-medium text-neutral">
888
+ {{ label(field.key) }}
889
+ </label>
890
+
891
+ @switch (field.type) {
892
+ @case ('email') {
893
+ <input
894
+ [attr.id]="field.key"
895
+ [attr.name]="field.key"
896
+ type="email"
897
+ autocomplete="email"
898
+ inputmode="email"
899
+ [attr.required]="field.required ? '' : null"
900
+ [value]="value(field.key)"
901
+ (input)="setValue(field.key, $any($event.target).value)"
902
+ class="${FIELD_BASE}"
903
+ />
904
+ }
905
+ @default {
906
+ <input
907
+ [attr.id]="field.key"
908
+ [attr.name]="field.key"
909
+ type="password"
910
+ autocomplete="current-password"
911
+ [attr.required]="field.required ? '' : null"
912
+ [value]="value(field.key)"
913
+ (input)="setValue(field.key, $any($event.target).value)"
914
+ class="${FIELD_BASE}"
915
+ />
916
+ }
917
+ }
918
+ </div>
919
+ }
920
+
921
+ @if (error(); as message) {
922
+ <p role="alert" class="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">
923
+ {{ message }}
924
+ </p>
925
+ }
926
+
927
+ <button app-button type="submit" [loading]="loading()" class="w-full">
928
+ Iniciar sesión
929
+ </button>
930
+ </form>
931
+ </div>
932
+ </main>
933
+ `
934
+ }]
935
+ }]
936
+ });
937
+ var WidgetHost = class WidgetHost {
938
+ widget = input.required(...ngDevMode ? [{ debugName: "widget" }] : /* istanbul ignore next */ []);
939
+ data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
940
+ loading = input(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
941
+ gridSpanClass = computed(() => {
942
+ switch (this.widget().width) {
943
+ case 12: return "col-span-12 block";
944
+ case 8: return "col-span-12 lg:col-span-8 block";
945
+ case 6: return "col-span-12 sm:col-span-6 block";
946
+ case 4: return "col-span-12 sm:col-span-6 lg:col-span-4 block";
947
+ case 3: return "col-span-12 sm:col-span-6 lg:col-span-3 block";
948
+ default: return "col-span-12 sm:col-span-6 lg:col-span-4 block";
949
+ }
950
+ }, ...ngDevMode ? [{ debugName: "gridSpanClass" }] : /* istanbul ignore next */ []);
951
+ static ɵfac = i0.ɵɵngDeclareFactory({
952
+ minVersion: "12.0.0",
953
+ version: "22.2.3",
954
+ ngImport: i0,
955
+ type: WidgetHost,
956
+ deps: [],
957
+ target: i0.ɵɵFactoryTarget.Component
958
+ });
959
+ static ɵcmp = i0.ɵɵngDeclareComponent({
960
+ minVersion: "17.0.0",
961
+ version: "22.2.3",
962
+ type: WidgetHost,
963
+ isStandalone: true,
964
+ selector: "app-widget-host",
965
+ inputs: {
966
+ widget: {
967
+ classPropertyName: "widget",
968
+ publicName: "widget",
969
+ isSignal: true,
970
+ isRequired: true,
971
+ transformFunction: null
972
+ },
973
+ data: {
974
+ classPropertyName: "data",
975
+ publicName: "data",
976
+ isSignal: true,
977
+ isRequired: false,
978
+ transformFunction: null
979
+ },
980
+ loading: {
981
+ classPropertyName: "loading",
982
+ publicName: "loading",
983
+ isSignal: true,
984
+ isRequired: false,
985
+ transformFunction: null
986
+ }
987
+ },
988
+ host: { properties: { "class": "gridSpanClass()" } },
989
+ ngImport: i0,
990
+ template: `
991
+ @if (loading()) {
992
+ <!-- Reserva el espacio del widget mientras su dato viaja (la red
993
+ Apps Script tarda ~1s): sin esto el panel se veía "completo"
994
+ con valores vacíos que después aparecían solos (cargar ≠ vacío,
995
+ doc/ux-ui-mobile.md §estados de UI). La grilla mantiene su
996
+ span porque la clase la pone el host. -->
997
+ <div class="rounded-lg border border-neutral/20 bg-surface p-4">
998
+ <app-skeleton [lines]="1" [height]="14" width="40%" />
999
+ <div class="mt-3">
1000
+ <app-skeleton [lines]="2" [height]="32" />
1001
+ </div>
1002
+ </div>
1003
+ } @else {
1004
+ <app-render-host kind="widget" [type]="widget().type" [config]="widget()" [data]="data()" />
1005
+ }
1006
+ `,
1007
+ isInline: true,
1008
+ dependencies: [{
1009
+ kind: "component",
1010
+ type: RenderHost,
1011
+ selector: "app-render-host",
1012
+ inputs: [
1013
+ "kind",
1014
+ "type",
1015
+ "config",
1016
+ "ctx",
1017
+ "data",
1018
+ "mode",
1019
+ "value",
1020
+ "chip"
1021
+ ]
1022
+ }, {
1023
+ kind: "component",
1024
+ type: Skeleton,
1025
+ selector: "app-skeleton",
1026
+ inputs: [
1027
+ "lines",
1028
+ "height",
1029
+ "width"
1030
+ ]
1031
+ }]
1032
+ });
1033
+ };
1034
+ i0.ɵɵngDeclareClassMetadata({
1035
+ minVersion: "12.0.0",
1036
+ version: "22.2.3",
1037
+ ngImport: i0,
1038
+ type: WidgetHost,
1039
+ decorators: [{
1040
+ type: Component,
1041
+ args: [{
1042
+ selector: "app-widget-host",
1043
+ imports: [RenderHost, Skeleton],
1044
+ host: { "[class]": "gridSpanClass()" },
1045
+ template: `
1046
+ @if (loading()) {
1047
+ <!-- Reserva el espacio del widget mientras su dato viaja (la red
1048
+ Apps Script tarda ~1s): sin esto el panel se veía "completo"
1049
+ con valores vacíos que después aparecían solos (cargar ≠ vacío,
1050
+ doc/ux-ui-mobile.md §estados de UI). La grilla mantiene su
1051
+ span porque la clase la pone el host. -->
1052
+ <div class="rounded-lg border border-neutral/20 bg-surface p-4">
1053
+ <app-skeleton [lines]="1" [height]="14" width="40%" />
1054
+ <div class="mt-3">
1055
+ <app-skeleton [lines]="2" [height]="32" />
1056
+ </div>
1057
+ </div>
1058
+ } @else {
1059
+ <app-render-host kind="widget" [type]="widget().type" [config]="widget()" [data]="data()" />
1060
+ }
1061
+ `
1062
+ }]
1063
+ }],
1064
+ propDecorators: {
1065
+ widget: [{
1066
+ type: i0.Input,
1067
+ args: [{
1068
+ isSignal: true,
1069
+ alias: "widget",
1070
+ required: true
1071
+ }]
1072
+ }],
1073
+ data: [{
1074
+ type: i0.Input,
1075
+ args: [{
1076
+ isSignal: true,
1077
+ alias: "data",
1078
+ required: false
1079
+ }]
1080
+ }],
1081
+ loading: [{
1082
+ type: i0.Input,
1083
+ args: [{
1084
+ isSignal: true,
1085
+ alias: "loading",
1086
+ required: false
1087
+ }]
1088
+ }]
1089
+ }
1090
+ });
1091
+ var Dashboard = class Dashboard {
1092
+ api = inject(ApiService, { optional: true });
1093
+ ds = inject(DataSourceService);
1094
+ schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
1095
+ data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
1096
+ fetchedData = toSignal(toObservable(this.schema).pipe(switchMap((schema) => {
1097
+ const getEndpoint = schema.endpoint?.get;
1098
+ if (!getEndpoint || !this.api) return of({});
1099
+ return this.api.request("GET", getEndpoint).pipe(catchError(() => of({})));
1100
+ })), { initialValue: null });
1101
+ effectiveData = computed(() => {
1102
+ const override = this.data();
1103
+ if (override !== null && override !== void 0) return override;
1104
+ return this.fetchedData() ?? {};
1105
+ }, ...ngDevMode ? [{ debugName: "effectiveData" }] : /* istanbul ignore next */ []);
1106
+ widgets = computed(() => this.schema().widgets ?? [], ...ngDevMode ? [{ debugName: "widgets" }] : /* istanbul ignore next */ []);
1107
+ clavesFuente = computed(() => this.widgets().map((widget) => esFuente(widget.source) ? widget.key : "").filter((key) => key !== "").join("|"), ...ngDevMode ? [{ debugName: "clavesFuente" }] : /* istanbul ignore next */ []);
1108
+ fuentes = resource({
1109
+ ...ngDevMode ? { debugName: "fuentes" } : /* istanbul ignore next */ {},
1110
+ params: () => this.clavesFuente() === "" ? void 0 : this.clavesFuente(),
1111
+ loader: async () => {
1112
+ const filas = {};
1113
+ for (const widget of this.widgets()) {
1114
+ if (!esFuente(widget.source)) continue;
1115
+ filas[widget.key] = await this.ds.rows(widget.source);
1116
+ }
1117
+ return filas;
1118
+ }
1119
+ });
1120
+ falloFuentes = computed(() => this.fuentes.status() === "error", ...ngDevMode ? [{ debugName: "falloFuentes" }] : /* istanbul ignore next */ []);
1121
+ cargando = computed(() => {
1122
+ const payloadViaja = this.data() === null && this.fetchedData() === null;
1123
+ const fuentesViajan = this.fuentes.status() === "loading";
1124
+ return payloadViaja || fuentesViajan;
1125
+ }, ...ngDevMode ? [{ debugName: "cargando" }] : /* istanbul ignore next */ []);
1126
+ datum(widget) {
1127
+ if (esFuente(widget.source)) return this.fuentes.status() === "error" ? null : this.fuentes.value()?.[widget.key] ?? null;
1128
+ const allData = this.effectiveData();
1129
+ return widget.source?.kind === "payload" ? allData : allData[widget.key] ?? null;
1130
+ }
1131
+ static ɵfac = i0.ɵɵngDeclareFactory({
1132
+ minVersion: "12.0.0",
1133
+ version: "22.2.3",
1134
+ ngImport: i0,
1135
+ type: Dashboard,
1136
+ deps: [],
1137
+ target: i0.ɵɵFactoryTarget.Component
1138
+ });
1139
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1140
+ minVersion: "17.0.0",
1141
+ version: "22.2.3",
1142
+ type: Dashboard,
1143
+ isStandalone: true,
1144
+ selector: "app-dashboard",
1145
+ inputs: {
1146
+ schema: {
1147
+ classPropertyName: "schema",
1148
+ publicName: "schema",
1149
+ isSignal: true,
1150
+ isRequired: true,
1151
+ transformFunction: null
1152
+ },
1153
+ data: {
1154
+ classPropertyName: "data",
1155
+ publicName: "data",
1156
+ isSignal: true,
1157
+ isRequired: false,
1158
+ transformFunction: null
1159
+ }
1160
+ },
1161
+ ngImport: i0,
1162
+ template: `
1163
+ <header class="mb-6">
1164
+ <h1 class="font-display text-2xl font-semibold">{{ schema().labelPlural }}</h1>
1165
+ <p class="text-sm text-neutral">Dashboard · {{ widgets().length }} widgets</p>
1166
+ </header>
1167
+
1168
+ @if (falloFuentes()) {
1169
+ <!-- Degradación visible (F9): sin este aviso el fallo de una fuente
1170
+ se confundiría con «el panel no tiene datos». -->
1171
+ <p class="mb-4 text-sm text-danger" role="alert">
1172
+ No se pudieron cargar los datos de algunos widgets.
1173
+ </p>
1174
+ }
1175
+
1176
+ @if (widgets().length === 0) {
1177
+ <app-empty-state
1178
+ title="Sin widgets declarados"
1179
+ message="Los widgets se declaran en la propiedad widgets del schema del dashboard."
1180
+ />
1181
+ } @else {
1182
+ <div class="grid grid-cols-12 gap-4">
1183
+ @for (widget of widgets(); track widget.key) {
1184
+ <app-widget-host [widget]="widget" [data]="datum(widget)" [loading]="cargando()" />
1185
+ }
1186
+ </div>
1187
+ }
1188
+ `,
1189
+ isInline: true,
1190
+ dependencies: [{
1191
+ kind: "component",
1192
+ type: EmptyState,
1193
+ selector: "app-empty-state",
1194
+ inputs: ["title", "message"]
1195
+ }, {
1196
+ kind: "component",
1197
+ type: WidgetHost,
1198
+ selector: "app-widget-host",
1199
+ inputs: [
1200
+ "widget",
1201
+ "data",
1202
+ "loading"
1203
+ ]
1204
+ }]
1205
+ });
1206
+ };
1207
+ i0.ɵɵngDeclareClassMetadata({
1208
+ minVersion: "12.0.0",
1209
+ version: "22.2.3",
1210
+ ngImport: i0,
1211
+ type: Dashboard,
1212
+ decorators: [{
1213
+ type: Component,
1214
+ args: [{
1215
+ selector: "app-dashboard",
1216
+ imports: [EmptyState, WidgetHost],
1217
+ template: `
1218
+ <header class="mb-6">
1219
+ <h1 class="font-display text-2xl font-semibold">{{ schema().labelPlural }}</h1>
1220
+ <p class="text-sm text-neutral">Dashboard · {{ widgets().length }} widgets</p>
1221
+ </header>
1222
+
1223
+ @if (falloFuentes()) {
1224
+ <!-- Degradación visible (F9): sin este aviso el fallo de una fuente
1225
+ se confundiría con «el panel no tiene datos». -->
1226
+ <p class="mb-4 text-sm text-danger" role="alert">
1227
+ No se pudieron cargar los datos de algunos widgets.
1228
+ </p>
1229
+ }
1230
+
1231
+ @if (widgets().length === 0) {
1232
+ <app-empty-state
1233
+ title="Sin widgets declarados"
1234
+ message="Los widgets se declaran en la propiedad widgets del schema del dashboard."
1235
+ />
1236
+ } @else {
1237
+ <div class="grid grid-cols-12 gap-4">
1238
+ @for (widget of widgets(); track widget.key) {
1239
+ <app-widget-host [widget]="widget" [data]="datum(widget)" [loading]="cargando()" />
1240
+ }
1241
+ </div>
1242
+ }
1243
+ `
1244
+ }]
1245
+ }],
1246
+ propDecorators: {
1247
+ schema: [{
1248
+ type: i0.Input,
1249
+ args: [{
1250
+ isSignal: true,
1251
+ alias: "schema",
1252
+ required: true
1253
+ }]
1254
+ }],
1255
+ data: [{
1256
+ type: i0.Input,
1257
+ args: [{
1258
+ isSignal: true,
1259
+ alias: "data",
1260
+ required: false
1261
+ }]
1262
+ }]
1263
+ }
1264
+ });
1265
+ function esFuente(source) {
1266
+ return source !== void 0 && source !== null && "resource" in source;
1267
+ }
1268
+ var ActionService = class ActionService {
1269
+ config = inject(ConfigService);
1270
+ errores = inject(ErrorMapperService);
1271
+ async run(action, request) {
1272
+ const type = actionType(action);
1273
+ const descriptor = getAction(type);
1274
+ if (!descriptor) throw new Error(`[actions] «${action.label}» no tiene descriptor registrado (type "${type}").`);
1275
+ if (descriptor.confirm) {
1276
+ if (!await request.toast.confirm({
1277
+ title: action.label,
1278
+ message: confirmMessage(action),
1279
+ confirmLabel: "Confirmar",
1280
+ cancelLabel: "Cancelar"
1281
+ })) return { ok: false };
1282
+ }
1283
+ if (descriptor.type === "copy-share") await this.config.ensureLoaded();
1284
+ const ctx = {
1285
+ ...request,
1286
+ vars: this.vars(request.record)
1287
+ };
1288
+ try {
1289
+ return await descriptor.run(ctx, action);
1290
+ } catch (err) {
1291
+ request.toast.error(this.errores.messageOf(err));
1292
+ return { ok: false };
1293
+ }
1294
+ }
1295
+ vars(record) {
1296
+ return record ? this.config.buildInterpolationContext(record) : {};
1297
+ }
1298
+ static ɵfac = i0.ɵɵngDeclareFactory({
1299
+ minVersion: "12.0.0",
1300
+ version: "22.2.3",
1301
+ ngImport: i0,
1302
+ type: ActionService,
1303
+ deps: [],
1304
+ target: i0.ɵɵFactoryTarget.Service
1305
+ });
1306
+ static ɵprov = i0.ɵɵngDeclareService({
1307
+ minVersion: "22.0.0",
1308
+ version: "22.2.3",
1309
+ ngImport: i0,
1310
+ type: ActionService
1311
+ });
1312
+ };
1313
+ i0.ɵɵngDeclareClassMetadata({
1314
+ minVersion: "12.0.0",
1315
+ version: "22.2.3",
1316
+ ngImport: i0,
1317
+ type: ActionService,
1318
+ decorators: [{ type: Service }]
1319
+ });
1320
+ function confirmMessage(action) {
1321
+ return "confirmMessage" in action && action.confirmMessage ? action.confirmMessage : action.label;
1322
+ }
1323
+ const FORMATO_POR_DEFECTO = {
1324
+ text: "text",
1325
+ textarea: "text",
1326
+ slug: "text",
1327
+ url: "text",
1328
+ email: "text",
1329
+ phone: "text",
1330
+ "readonly-text": "text",
1331
+ relation: "text",
1332
+ group: "text",
1333
+ "key-value": "text",
1334
+ "string-list": "text",
1335
+ multiselect: "text",
1336
+ time: "text",
1337
+ list: "text",
1338
+ date: "date",
1339
+ number: "number",
1340
+ currency: "currency",
1341
+ boolean: "boolean",
1342
+ select: "badge",
1343
+ image: "thumbnail",
1344
+ file: "text"
1345
+ };
1346
+ function formatoPorTipo(type) {
1347
+ return Object.prototype.hasOwnProperty.call(FORMATO_POR_DEFECTO, type) ? FORMATO_POR_DEFECTO[type] : void 0;
1348
+ }
1349
+ function formatoDeColumna(column, fields) {
1350
+ if (column.format) return column.format;
1351
+ const field = fields.find((candidate) => candidate.key === column.key);
1352
+ return (field ? formatoPorTipo(field.type) : void 0) ?? "text";
1353
+ }
1354
+ const BOOLEAN_FILTER_OPTIONS = [
1355
+ {
1356
+ value: "",
1357
+ label: "Todos"
1358
+ },
1359
+ {
1360
+ value: "yes",
1361
+ label: "Sí"
1362
+ },
1363
+ {
1364
+ value: "no",
1365
+ label: "No"
1366
+ }
1367
+ ];
1368
+ function errorMessage(err, fallback) {
1369
+ return err instanceof ApiError ? err.message : fallback;
1370
+ }
1371
+ var ListView = class ListView {
1372
+ schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
1373
+ viewRecord = output();
1374
+ api = inject(ApiService);
1375
+ auth = inject(AuthService);
1376
+ router = inject(Router);
1377
+ notifications = inject(NotificationService);
1378
+ actionService = inject(ActionService);
1379
+ status = signal("loading", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
1380
+ rows = signal([], ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
1381
+ query = signal("", ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
1382
+ filterValues = signal({}, ...ngDevMode ? [{ debugName: "filterValues" }] : /* istanbul ignore next */ []);
1383
+ savingKey = signal(null, ...ngDevMode ? [{ debugName: "savingKey" }] : /* istanbul ignore next */ []);
1384
+ quickEditRow = signal(null, ...ngDevMode ? [{ debugName: "quickEditRow" }] : /* istanbul ignore next */ []);
1385
+ loadEpoch = 0;
1386
+ constructor() {
1387
+ effect(() => {
1388
+ const schema = this.schema();
1389
+ this.query.set("");
1390
+ this.filterValues.set({});
1391
+ this.load(schema);
1392
+ });
1393
+ }
1394
+ visibleRows = computed(() => {
1395
+ const schema = this.schema();
1396
+ let rows = this.rows();
1397
+ const query = this.query().trim().toLowerCase();
1398
+ if (query !== "" && schema.search?.length) rows = rows.filter((row) => schema.search.some((key) => String(getValue(row, key) ?? "").toLowerCase().includes(query)));
1399
+ for (const filter of schema.filters ?? []) {
1400
+ const value = this.filterValues()[filter.key] ?? "";
1401
+ if (value === "") continue;
1402
+ rows = rows.filter((row) => {
1403
+ const cell = getValue(row, filter.key);
1404
+ if (filter.type === "boolean") return (cell === true || cell === "true") === (value === "yes");
1405
+ if (filter.type === "select") return String(cell ?? "") === value;
1406
+ return String(cell ?? "").toLowerCase().includes(value.toLowerCase());
1407
+ });
1408
+ }
1409
+ const positionField = schema.positionField;
1410
+ if (positionField) {
1411
+ const positionOf = (row) => {
1412
+ const position = Number(getValue(row, positionField));
1413
+ return Number.isFinite(position) ? position : Number.POSITIVE_INFINITY;
1414
+ };
1415
+ rows = [...rows].sort((a, b) => positionOf(a) - positionOf(b));
1416
+ }
1417
+ return rows;
1418
+ }, ...ngDevMode ? [{ debugName: "visibleRows" }] : /* istanbul ignore next */ []);
1419
+ currencyCode = computed(() => {
1420
+ const field = getSchema("site")?.fields.find((f) => f.key === "currency");
1421
+ return field?.type === "text" && field.default ? field.default : "USD";
1422
+ }, ...ngDevMode ? [{ debugName: "currencyCode" }] : /* istanbul ignore next */ []);
1423
+ permissions = computed(() => this.schema().permissions, ...ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []);
1424
+ canCreate = computed(() => this.auth.canDoOperation(this.schema(), "create"), ...ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []);
1425
+ canUpdate = computed(() => this.auth.canDoOperation(this.schema(), "update"), ...ngDevMode ? [{ debugName: "canUpdate" }] : /* istanbul ignore next */ []);
1426
+ canRemove = computed(() => this.auth.canDoOperation(this.schema(), "remove"), ...ngDevMode ? [{ debugName: "canRemove" }] : /* istanbul ignore next */ []);
1427
+ canQuickEdit = computed(() => this.canUpdate() && (this.schema().quickEdit?.length ?? 0) > 0, ...ngDevMode ? [{ debugName: "canQuickEdit" }] : /* istanbul ignore next */ []);
1428
+ quickEditFields = computed(() => {
1429
+ const keys = this.schema().quickEdit ?? [];
1430
+ const byKey = new Map(this.schema().fields.map((field) => [field.key, field]));
1431
+ return keys.map((key) => byKey.get(key)).filter((field) => field !== void 0);
1432
+ }, ...ngDevMode ? [{ debugName: "quickEditFields" }] : /* istanbul ignore next */ []);
1433
+ hasRowActions = computed(() => this.canUpdate() || this.canRemove() || (this.schema().actions?.length ?? 0) > 0, ...ngDevMode ? [{ debugName: "hasRowActions" }] : /* istanbul ignore next */ []);
1434
+ reordering = signal(false, ...ngDevMode ? [{ debugName: "reordering" }] : /* istanbul ignore next */ []);
1435
+ hasActiveFilters = computed(() => {
1436
+ if (this.query().trim() !== "") return true;
1437
+ return Object.values(this.filterValues()).some((value) => value !== "");
1438
+ }, ...ngDevMode ? [{ debugName: "hasActiveFilters" }] : /* istanbul ignore next */ []);
1439
+ canReorder = computed(() => {
1440
+ const schema = this.schema();
1441
+ return schema.sortable === true && schema.positionField !== void 0 && schema.permissions?.reorder !== false && !this.hasActiveFilters();
1442
+ }, ...ngDevMode ? [{ debugName: "canReorder" }] : /* istanbul ignore next */ []);
1443
+ dropDisabled = computed(() => !this.canReorder() || this.reordering(), ...ngDevMode ? [{ debugName: "dropDisabled" }] : /* istanbul ignore next */ []);
1444
+ setQuery(value) {
1445
+ this.query.set(value);
1446
+ }
1447
+ setFilter(key, value) {
1448
+ this.filterValues.update((values) => ({
1449
+ ...values,
1450
+ [key]: value
1451
+ }));
1452
+ }
1453
+ clearFilters() {
1454
+ this.query.set("");
1455
+ this.filterValues.set({});
1456
+ }
1457
+ retry() {
1458
+ this.load(this.schema());
1459
+ }
1460
+ isRowClickable() {
1461
+ return Boolean(this.schema().detail?.enabled || this.canUpdate());
1462
+ }
1463
+ goView(row) {
1464
+ this.viewRecord.emit(row);
1465
+ }
1466
+ onRowClick(row) {
1467
+ if (this.schema().detail?.enabled) this.goView(row);
1468
+ else if (this.canUpdate()) this.goEdit(row);
1469
+ }
1470
+ goCreate() {
1471
+ this.router.navigate([
1472
+ "/",
1473
+ this.schema().id,
1474
+ "new"
1475
+ ]);
1476
+ }
1477
+ goEdit(row) {
1478
+ if (this.schema().detail?.enabled === true) {
1479
+ this.viewRecord.emit(row);
1480
+ return;
1481
+ }
1482
+ this.router.navigate([
1483
+ "/",
1484
+ this.schema().id,
1485
+ this.keyOf(row),
1486
+ "edit"
1487
+ ]);
1488
+ }
1489
+ quickEditSaved() {
1490
+ this.quickEditRow.set(null);
1491
+ this.load(this.schema());
1492
+ }
1493
+ async toggleActive(row, checked) {
1494
+ const schema = this.schema();
1495
+ const key = this.keyOf(row);
1496
+ this.savingKey.set(key);
1497
+ try {
1498
+ await firstValueFrom(this.api.update(schema.endpoint, key, {
1499
+ ...row,
1500
+ active: checked
1501
+ }));
1502
+ this.rows.update((rows) => rows.map((candidate) => this.keyOf(candidate) === key ? {
1503
+ ...candidate,
1504
+ active: checked
1505
+ } : candidate));
1506
+ } catch (err) {
1507
+ this.notifications.error(errorMessage(err, "No se pudo actualizar el registro."));
1508
+ } finally {
1509
+ this.savingKey.set(null);
1510
+ }
1511
+ }
1512
+ onDrop(event) {
1513
+ if (event.previousContainer !== event.container || event.previousIndex === event.currentIndex) return;
1514
+ const row = this.visibleRows()[event.previousIndex];
1515
+ if (row !== void 0) this.moveRow(this.keyOf(row), event.currentIndex);
1516
+ }
1517
+ moveBy(key, delta) {
1518
+ const index = this.visibleRows().findIndex((row) => this.keyOf(row) === key);
1519
+ if (index < 0) return;
1520
+ this.moveRow(key, index + delta);
1521
+ }
1522
+ async moveRow(key, targetIndex) {
1523
+ const schema = this.schema();
1524
+ const path = schema.endpoint.list;
1525
+ if (!path || !schema.positionField || this.reordering()) return;
1526
+ const rows = this.visibleRows();
1527
+ const from = rows.findIndex((row) => this.keyOf(row) === key);
1528
+ if (from < 0) return;
1529
+ const without = rows.filter((row) => this.keyOf(row) !== key);
1530
+ const target = Math.min(Math.max(targetIndex, 0), without.length);
1531
+ if (target === from) return;
1532
+ const reorder = target === 0 ? {
1533
+ key,
1534
+ toStart: true
1535
+ } : target >= without.length ? {
1536
+ key,
1537
+ toEnd: true
1538
+ } : {
1539
+ key,
1540
+ after: this.keyOf(without[target - 1])
1541
+ };
1542
+ this.reordering.set(true);
1543
+ try {
1544
+ const fresh = await firstValueFrom(this.api.request("PUT", path, { reorder }));
1545
+ if (Array.isArray(fresh)) this.rows.set(fresh);
1546
+ this.notifications.success("Orden actualizado.");
1547
+ } catch (err) {
1548
+ this.notifications.error(errorMessage(err, "No se pudo guardar el orden."));
1549
+ } finally {
1550
+ this.reordering.set(false);
1551
+ }
1552
+ }
1553
+ async removeRow(row) {
1554
+ const schema = this.schema();
1555
+ const key = this.keyOf(row);
1556
+ if (!await this.notifications.confirm({
1557
+ title: `Eliminar «${this.titleOf(row)}»`,
1558
+ message: "Esta acción no se puede deshacer.",
1559
+ confirmLabel: "Eliminar",
1560
+ cancelLabel: "Cancelar"
1561
+ })) return;
1562
+ try {
1563
+ await firstValueFrom(this.api.remove(schema.endpoint, key));
1564
+ this.notifications.success("Registro eliminado.");
1565
+ this.rows.update((rows) => rows.filter((candidate) => this.keyOf(candidate) !== key));
1566
+ } catch (err) {
1567
+ this.notifications.error(errorMessage(err, "No se pudo eliminar el registro."));
1568
+ }
1569
+ }
1570
+ async runAction(action, row) {
1571
+ await this.actionService.run(action, {
1572
+ record: row,
1573
+ resource: this.schema().id,
1574
+ api: this.api,
1575
+ router: this.router,
1576
+ toast: this.notifications,
1577
+ user: this.auth.user(),
1578
+ keyField: this.schema().keyField
1579
+ });
1580
+ }
1581
+ iconOf(action) {
1582
+ return getAction(actionType(action))?.icon;
1583
+ }
1584
+ keyOf(row) {
1585
+ return String(row[this.schema().keyField] ?? "");
1586
+ }
1587
+ titleOf(row) {
1588
+ return String(row[this.schema().titleField] ?? "");
1589
+ }
1590
+ formatOf(column) {
1591
+ return formatoDeColumna(column, this.schema().fields);
1592
+ }
1593
+ cellOf(row, column) {
1594
+ return {
1595
+ row,
1596
+ column,
1597
+ fields: this.schema().fields,
1598
+ title: this.titleOf(row),
1599
+ canUpdate: this.canUpdate(),
1600
+ saving: this.savingKey() === this.keyOf(row),
1601
+ currency: this.currencyCode(),
1602
+ toggle: (checked) => void this.toggleActive(row, checked)
1603
+ };
1604
+ }
1605
+ filterOptions(filter) {
1606
+ if (filter.type === "boolean") return BOOLEAN_FILTER_OPTIONS;
1607
+ if (filter.type === "select") return [{
1608
+ value: "",
1609
+ label: "Todos"
1610
+ }, ...filter.options ?? []];
1611
+ return [];
1612
+ }
1613
+ inputValue(event) {
1614
+ return event.target.value;
1615
+ }
1616
+ selectValue(event) {
1617
+ return event.target.value;
1618
+ }
1619
+ async load(schema) {
1620
+ const epoch = ++this.loadEpoch;
1621
+ this.status.set("loading");
1622
+ try {
1623
+ const rows = await firstValueFrom(this.api.list(schema.endpoint));
1624
+ if (epoch !== this.loadEpoch) return;
1625
+ this.rows.set(Array.isArray(rows) ? rows : []);
1626
+ this.status.set("ready");
1627
+ } catch {
1628
+ if (epoch !== this.loadEpoch) return;
1629
+ this.status.set("error");
1630
+ }
1631
+ }
1632
+ static ɵfac = i0.ɵɵngDeclareFactory({
1633
+ minVersion: "12.0.0",
1634
+ version: "22.2.3",
1635
+ ngImport: i0,
1636
+ type: ListView,
1637
+ deps: [],
1638
+ target: i0.ɵɵFactoryTarget.Component
1639
+ });
1640
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1641
+ minVersion: "17.0.0",
1642
+ version: "22.2.3",
1643
+ type: ListView,
1644
+ isStandalone: true,
1645
+ selector: "app-list-view",
1646
+ inputs: { schema: {
1647
+ classPropertyName: "schema",
1648
+ publicName: "schema",
1649
+ isSignal: true,
1650
+ isRequired: true,
1651
+ transformFunction: null
1652
+ } },
1653
+ outputs: { viewRecord: "viewRecord" },
1654
+ ngImport: i0,
1655
+ template: `
1656
+ <section class="space-y-4" aria-label="Listado" [attr.aria-busy]="status() === 'loading'">
1657
+ <div class="flex flex-wrap items-end justify-between gap-3">
1658
+ <div class="flex flex-wrap items-end gap-3">
1659
+ @if (schema().search?.length) {
1660
+ <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
1661
+ Buscar
1662
+ <input
1663
+ type="search"
1664
+ class="w-56 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
1665
+ placeholder="Buscar…"
1666
+ [value]="query()"
1667
+ (input)="setQuery(inputValue($event))"
1668
+ />
1669
+ </label>
1670
+ }
1671
+ @for (filter of schema().filters ?? []; track filter.key) {
1672
+ <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
1673
+ {{ filter.label }}
1674
+ @if (filter.type === 'text') {
1675
+ <input
1676
+ type="search"
1677
+ class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
1678
+ [value]="filterValues()[filter.key] ?? ''"
1679
+ (input)="setFilter(filter.key, inputValue($event))"
1680
+ />
1681
+ } @else {
1682
+ <select
1683
+ class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
1684
+ [value]="filterValues()[filter.key] ?? ''"
1685
+ (change)="setFilter(filter.key, selectValue($event))"
1686
+ >
1687
+ @for (option of filterOptions(filter); track option.value) {
1688
+ <option [value]="option.value">{{ option.label }}</option>
1689
+ }
1690
+ </select>
1691
+ }
1692
+ </label>
1693
+ }
1694
+ </div>
1695
+ @if (canCreate()) {
1696
+ <button app-button type="button" (click)="goCreate()">
1697
+ <svg lucidePlus size="16" />
1698
+ Crear {{ schema().label }}
1699
+ </button>
1700
+ }
1701
+ </div>
1702
+
1703
+ @switch (status()) {
1704
+ @case ('loading') {
1705
+ <app-skeleton [lines]="6" [height]="44" />
1706
+ }
1707
+ @case ('error') {
1708
+ <app-empty-state
1709
+ title="No se pudo cargar el listado"
1710
+ message="Revisa tu conexión e intenta de nuevo."
1711
+ >
1712
+ <button app-button variant="outline" type="button" (click)="retry()">
1713
+ <svg lucideRotateCw size="16" />
1714
+ Reintentar
1715
+ </button>
1716
+ </app-empty-state>
1717
+ }
1718
+ @case ('ready') {
1719
+ @if (visibleRows().length === 0) {
1720
+ <app-empty-state
1721
+ [title]="rows().length === 0 ? 'Sin registros' : 'Sin resultados'"
1722
+ [message]="
1723
+ rows().length === 0
1724
+ ? 'Todavía no hay registros en este recurso.'
1725
+ : 'Ningún registro coincide con la búsqueda o los filtros.'
1726
+ "
1727
+ >
1728
+ @if (rows().length > 0) {
1729
+ <button app-button variant="outline" type="button" (click)="clearFilters()">
1730
+ Limpiar filtros
1731
+ </button>
1732
+ } @else if (canCreate()) {
1733
+ <button app-button type="button" (click)="goCreate()">
1734
+ <svg lucidePlus size="16" />
1735
+ Crear el primero
1736
+ </button>
1737
+ }
1738
+ </app-empty-state>
1739
+ } @else {
1740
+ <!-- La tabla es la vista de escritorio: se oculta bajo lg para
1741
+ que la lista de tarjetas de abajo sea la ÚNICA vista en
1742
+ móvil (con las dos visibles, el listado se duplicaba). -->
1743
+ <div
1744
+ class="hidden overflow-x-auto rounded-xl border border-neutral/20 bg-surface lg:block"
1745
+ >
1746
+ <table class="w-full text-sm">
1747
+ <thead>
1748
+ <tr class="border-b border-neutral/20 text-left text-xs uppercase text-neutral">
1749
+ @if (canReorder()) {
1750
+ <th scope="col" class="w-16 px-2 py-3 font-medium">
1751
+ <span class="sr-only">Orden</span>
1752
+ </th>
1753
+ }
1754
+ @for (column of schema().listColumns; track column.key) {
1755
+ <th scope="col" class="px-4 py-3 font-medium">
1756
+ {{ column.label }}
1757
+ </th>
1758
+ }
1759
+ @if (hasRowActions()) {
1760
+ <th scope="col" class="px-4 py-3 text-right font-medium">Acciones</th>
1761
+ }
1762
+ </tr>
1763
+ </thead>
1764
+ <tbody
1765
+ cdkDropList
1766
+ [cdkDropListData]="visibleRows()"
1767
+ [cdkDropListDisabled]="dropDisabled()"
1768
+ (cdkDropListDropped)="onDrop($event)"
1769
+ >
1770
+ @for (row of visibleRows(); track keyOf(row); let i = $index) {
1771
+ <tr
1772
+ class="border-b border-neutral/10 last:border-0 hover:bg-neutral/5 transition-colors"
1773
+ [class.cursor-pointer]="isRowClickable()"
1774
+ cdkDrag
1775
+ [cdkDragDisabled]="dropDisabled()"
1776
+ (click)="onRowClick(row)"
1777
+ >
1778
+ @if (canReorder()) {
1779
+ <td class="px-2 py-2.5 align-middle" (click)="$event.stopPropagation()">
1780
+ <div class="flex items-center gap-1">
1781
+ <button
1782
+ type="button"
1783
+ class="cursor-grab text-neutral hover:text-primary"
1784
+ cdkDragHandle
1785
+ [attr.aria-label]="'Reordenar ' + titleOf(row)"
1786
+ title="Arrastrar para reordenar"
1787
+ >
1788
+ <svg lucideGripVertical size="16" />
1789
+ </button>
1790
+ <span class="flex flex-col leading-none">
1791
+ <button
1792
+ type="button"
1793
+ class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
1794
+ [disabled]="i === 0 || reordering()"
1795
+ [attr.aria-label]="'Subir ' + titleOf(row)"
1796
+ (click)="moveBy(keyOf(row), -1)"
1797
+ >
1798
+ <svg lucideChevronUp size="16" />
1799
+ </button>
1800
+ <button
1801
+ type="button"
1802
+ class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
1803
+ [disabled]="i === visibleRows().length - 1 || reordering()"
1804
+ [attr.aria-label]="'Bajar ' + titleOf(row)"
1805
+ (click)="moveBy(keyOf(row), 1)"
1806
+ >
1807
+ <svg lucideChevronDown size="16" />
1808
+ </button>
1809
+ </span>
1810
+ </div>
1811
+ </td>
1812
+ }
1813
+ @for (column of schema().listColumns; track column.key) {
1814
+ <td class="px-4 py-2.5 align-middle">
1815
+ <!-- Los 7 formatos salen del registro (R4.3):
1816
+ la vista sólo elige cuál y entrega la celda. -->
1817
+ <app-render-host
1818
+ kind="column-format"
1819
+ [type]="formatOf(column)"
1820
+ [config]="column"
1821
+ [data]="cellOf(row, column)"
1822
+ />
1823
+ </td>
1824
+ }
1825
+ @if (hasRowActions()) {
1826
+ <td class="px-4 py-2.5" (click)="$event.stopPropagation()">
1827
+ <div class="flex items-center justify-end gap-1">
1828
+ @if (schema().detail?.enabled) {
1829
+ <button
1830
+ app-button
1831
+ variant="ghost"
1832
+ type="button"
1833
+ (click)="goView(row)"
1834
+ title="Ver ficha de detalle"
1835
+ class="h-8 px-2.5 text-xs text-neutral-light hover:text-primary hover:bg-primary/10"
1836
+ >
1837
+ <svg lucideEye size="15" />
1838
+ <span>Ver</span>
1839
+ </button>
1840
+ }
1841
+ @for (action of schema().actions ?? []; track action.id) {
1842
+ <button
1843
+ app-button
1844
+ variant="ghost"
1845
+ type="button"
1846
+ [title]="action.label"
1847
+ [attr.aria-label]="action.label"
1848
+ (click)="runAction(action, row)"
1849
+ class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-primary/10"
1850
+ >
1851
+ @let actionIcon = iconOf(action);
1852
+ @if (actionIcon) {
1853
+ <svg [lucideIcon]="actionIcon" size="15" />
1854
+ } @else {
1855
+ {{ action.label }}
1856
+ }
1857
+ </button>
1858
+ }
1859
+ @if (canQuickEdit()) {
1860
+ <button
1861
+ app-button
1862
+ variant="ghost"
1863
+ type="button"
1864
+ (click)="quickEditRow.set(row)"
1865
+ title="Edición rápida"
1866
+ class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-neutral/10"
1867
+ >
1868
+ <svg lucideZap size="15" />
1869
+ <span>Edición rápida</span>
1870
+ </button>
1871
+ }
1872
+ @if (canUpdate()) {
1873
+ <button
1874
+ app-button
1875
+ variant="ghost"
1876
+ type="button"
1877
+ (click)="goEdit(row)"
1878
+ title="Editar registro"
1879
+ class="h-8 px-2.5 text-xs text-primary hover:bg-primary/10"
1880
+ >
1881
+ <svg lucidePencil size="15" />
1882
+ <span>Editar</span>
1883
+ </button>
1884
+ }
1885
+ @if (canRemove()) {
1886
+ <button
1887
+ app-button
1888
+ variant="ghost"
1889
+ type="button"
1890
+ (click)="removeRow(row)"
1891
+ title="Eliminar registro"
1892
+ class="h-8 px-2 text-xs text-danger hover:bg-danger/10"
1893
+ >
1894
+ <svg lucideTrash2 size="15" />
1895
+ <span>Eliminar</span>
1896
+ </button>
1897
+ }
1898
+ </div>
1899
+ </td>
1900
+ }
1901
+ </tr>
1902
+ }
1903
+ </tbody>
1904
+ </table>
1905
+ </div>
1906
+
1907
+ <!-- Vista de tarjetas — mobile-first (guía mobile §6).
1908
+ La tabla de arriba necesita scroll horizontal con 5–7 columnas
1909
+ (el anti-patrón #2): en pantallas chicas se convierte en una
1910
+ lista de tarjetas de dos niveles (principal + secundario),
1911
+ donde cada fila completa es un objetivo táctil. lg:hidden la
1912
+ retira cuando la tabla vuelve a ser usable. -->
1913
+ <ul class="space-y-2 lg:hidden">
1914
+ @for (row of visibleRows(); track keyOf(row)) {
1915
+ <li
1916
+ class="rounded-xl border border-neutral/20 bg-surface p-3"
1917
+ [class.cursor-pointer]="isRowClickable()"
1918
+ (click)="onRowClick(row)"
1919
+ >
1920
+ <!-- Nivel principal: el título del registro, siempre visible
1921
+ (guía §6.2: el identificador nunca se esconde). -->
1922
+ <div class="flex items-start justify-between gap-2">
1923
+ <span class="text-sm font-medium">
1924
+ {{ titleOf(row) }}
1925
+ </span>
1926
+ @if (schema().detail?.enabled) {
1927
+ <svg
1928
+ lucideChevronRight
1929
+ size="16"
1930
+ class="mt-0.5 shrink-0 text-neutral"
1931
+ aria-hidden="true"
1932
+ />
1933
+ }
1934
+ </div>
1935
+ <!-- Nivel secundario: el resto de las columnas como pares
1936
+ etiqueta/valor. Sin hover: todo lo accionable va en el
1937
+ pie de la tarjeta, al alcance del pulgar. -->
1938
+ <dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
1939
+ @for (column of schema().listColumns; track column.key) {
1940
+ <dt class="text-neutral">{{ column.label }}</dt>
1941
+ <dd>
1942
+ <app-render-host
1943
+ kind="column-format"
1944
+ [type]="formatOf(column)"
1945
+ [config]="column"
1946
+ [data]="cellOf(row, column)"
1947
+ />
1948
+ </dd>
1949
+ }
1950
+ </dl>
1951
+ @if (hasRowActions()) {
1952
+ <!-- Pie táctil: objetivos ≥44 px (guía §8.2), sin hover. -->
1953
+ <div class="mt-3 flex flex-wrap gap-2 border-t border-neutral/10 pt-2">
1954
+ @if (schema().detail?.enabled) {
1955
+ <button
1956
+ app-button
1957
+ variant="outline"
1958
+ type="button"
1959
+ size="sm"
1960
+ class="flex-1"
1961
+ (click)="$event.stopPropagation(); goView(row)"
1962
+ >
1963
+ <svg lucideEye size="15" aria-hidden="true" />
1964
+ <span>Ver</span>
1965
+ </button>
1966
+ }
1967
+ @for (action of schema().actions ?? []; track action.id) {
1968
+ <button
1969
+ app-button
1970
+ variant="outline"
1971
+ type="button"
1972
+ size="sm"
1973
+ class="flex-1"
1974
+ (click)="$event.stopPropagation(); runAction(action, row)"
1975
+ >
1976
+ @let actionIcon = iconOf(action);
1977
+ @if (actionIcon) {
1978
+ <svg [lucideIcon]="actionIcon" size="15" aria-hidden="true" />
1979
+ }
1980
+ <span>{{ action.label }}</span>
1981
+ </button>
1982
+ }
1983
+ <!-- Editar es O una vía u otra, como en la tabla: si el
1984
+ schema declara quickEdit va el atajo; si no, la
1985
+ pantalla completa. Apilados los dos sería ruido. -->
1986
+ @if (canQuickEdit()) {
1987
+ <button
1988
+ app-button
1989
+ variant="outline"
1990
+ type="button"
1991
+ size="sm"
1992
+ class="flex-1"
1993
+ (click)="$event.stopPropagation(); quickEditRow.set(row)"
1994
+ >
1995
+ <svg lucideZap size="15" aria-hidden="true" />
1996
+ <span>Editar</span>
1997
+ </button>
1998
+ } @else if (canUpdate()) {
1999
+ <button
2000
+ app-button
2001
+ variant="outline"
2002
+ type="button"
2003
+ size="sm"
2004
+ class="flex-1"
2005
+ (click)="$event.stopPropagation(); goEdit(row)"
2006
+ >
2007
+ <svg lucidePencil size="15" aria-hidden="true" />
2008
+ <span>Editar</span>
2009
+ </button>
2010
+ }
2011
+ @if (canRemove()) {
2012
+ <button
2013
+ app-button
2014
+ variant="danger"
2015
+ type="button"
2016
+ size="sm"
2017
+ class="flex-1"
2018
+ (click)="$event.stopPropagation(); removeRow(row)"
2019
+ >
2020
+ <svg lucideTrash2 size="15" aria-hidden="true" />
2021
+ <span>Eliminar</span>
2022
+ </button>
2023
+ }
2024
+ </div>
2025
+ }
2026
+ </li>
2027
+ }
2028
+ </ul>
2029
+ }
2030
+ }
2031
+ }
2032
+
2033
+ @if (quickEditRow(); as row) {
2034
+ <app-drawer-form
2035
+ [open]="true"
2036
+ [schema]="schema()"
2037
+ mode="edit"
2038
+ [recordKey]="keyOf(row)"
2039
+ [fields]="quickEditFields()"
2040
+ title="Edición rápida"
2041
+ size="sm"
2042
+ (close)="quickEditRow.set(null)"
2043
+ (saved)="quickEditSaved()"
2044
+ />
2045
+ }
2046
+ </section>
2047
+ `,
2048
+ isInline: true,
2049
+ dependencies: [
2050
+ {
2051
+ kind: "component",
2052
+ type: Button,
2053
+ selector: "button[app-button]",
2054
+ inputs: [
2055
+ "variant",
2056
+ "size",
2057
+ "type",
2058
+ "disabled",
2059
+ "loading"
2060
+ ]
2061
+ },
2062
+ {
2063
+ kind: "directive",
2064
+ type: CdkDrag,
2065
+ selector: "[cdkDrag]",
2066
+ inputs: [
2067
+ "cdkDragData",
2068
+ "cdkDragLockAxis",
2069
+ "cdkDragRootElement",
2070
+ "cdkDragBoundary",
2071
+ "cdkDragStartDelay",
2072
+ "cdkDragFreeDragPosition",
2073
+ "cdkDragDisabled",
2074
+ "cdkDragConstrainPosition",
2075
+ "cdkDragPreviewClass",
2076
+ "cdkDragPreviewContainer",
2077
+ "cdkDragScale"
2078
+ ],
2079
+ outputs: [
2080
+ "cdkDragStarted",
2081
+ "cdkDragReleased",
2082
+ "cdkDragEnded",
2083
+ "cdkDragEntered",
2084
+ "cdkDragExited",
2085
+ "cdkDragDropped",
2086
+ "cdkDragMoved"
2087
+ ],
2088
+ exportAs: ["cdkDrag"]
2089
+ },
2090
+ {
2091
+ kind: "directive",
2092
+ type: CdkDragHandle,
2093
+ selector: "[cdkDragHandle]",
2094
+ inputs: ["cdkDragHandleDisabled"]
2095
+ },
2096
+ {
2097
+ kind: "directive",
2098
+ type: CdkDropList,
2099
+ selector: "[cdkDropList], cdk-drop-list",
2100
+ inputs: [
2101
+ "cdkDropListConnectedTo",
2102
+ "cdkDropListData",
2103
+ "cdkDropListOrientation",
2104
+ "id",
2105
+ "cdkDropListLockAxis",
2106
+ "cdkDropListDisabled",
2107
+ "cdkDropListSortingDisabled",
2108
+ "cdkDropListEnterPredicate",
2109
+ "cdkDropListSortPredicate",
2110
+ "cdkDropListAutoScrollDisabled",
2111
+ "cdkDropListAutoScrollStep",
2112
+ "cdkDropListElementContainer",
2113
+ "cdkDropListHasAnchor"
2114
+ ],
2115
+ outputs: [
2116
+ "cdkDropListDropped",
2117
+ "cdkDropListEntered",
2118
+ "cdkDropListExited",
2119
+ "cdkDropListSorted"
2120
+ ],
2121
+ exportAs: ["cdkDropList"]
2122
+ },
2123
+ {
2124
+ kind: "component",
2125
+ type: DrawerForm,
2126
+ selector: "app-drawer-form",
2127
+ inputs: [
2128
+ "open",
2129
+ "schema",
2130
+ "fields",
2131
+ "mode",
2132
+ "recordKey",
2133
+ "title",
2134
+ "size"
2135
+ ],
2136
+ outputs: ["saved", "close"]
2137
+ },
2138
+ {
2139
+ kind: "component",
2140
+ type: EmptyState,
2141
+ selector: "app-empty-state",
2142
+ inputs: ["title", "message"]
2143
+ },
2144
+ {
2145
+ kind: "component",
2146
+ type: LucideChevronDown,
2147
+ selector: "svg[lucideChevronDown]"
2148
+ },
2149
+ {
2150
+ kind: "component",
2151
+ type: LucideChevronRight,
2152
+ selector: "svg[lucideChevronRight]"
2153
+ },
2154
+ {
2155
+ kind: "component",
2156
+ type: LucideChevronUp,
2157
+ selector: "svg[lucideChevronUp]"
2158
+ },
2159
+ {
2160
+ kind: "component",
2161
+ type: LucideEye,
2162
+ selector: "svg[lucideEye]"
2163
+ },
2164
+ {
2165
+ kind: "component",
2166
+ type: LucideGripVertical,
2167
+ selector: "svg[lucideGripVertical]"
2168
+ },
2169
+ {
2170
+ kind: "component",
2171
+ type: LucidePencil,
2172
+ selector: "svg[lucidePencil]"
2173
+ },
2174
+ {
2175
+ kind: "component",
2176
+ type: LucidePlus,
2177
+ selector: "svg[lucidePlus]"
2178
+ },
2179
+ {
2180
+ kind: "component",
2181
+ type: LucideRotateCw,
2182
+ selector: "svg[lucideRotateCw]"
2183
+ },
2184
+ {
2185
+ kind: "component",
2186
+ type: LucideDynamicIcon,
2187
+ selector: "svg[lucideIcon]",
2188
+ inputs: ["lucideIcon"]
2189
+ },
2190
+ {
2191
+ kind: "component",
2192
+ type: LucideTrash2,
2193
+ selector: "svg[lucideTrash], svg[lucideTrash2]"
2194
+ },
2195
+ {
2196
+ kind: "component",
2197
+ type: LucideZap,
2198
+ selector: "svg[lucideZap]"
2199
+ },
2200
+ {
2201
+ kind: "component",
2202
+ type: RenderHost,
2203
+ selector: "app-render-host",
2204
+ inputs: [
2205
+ "kind",
2206
+ "type",
2207
+ "config",
2208
+ "ctx",
2209
+ "data",
2210
+ "mode",
2211
+ "value",
2212
+ "chip"
2213
+ ]
2214
+ },
2215
+ {
2216
+ kind: "component",
2217
+ type: Skeleton,
2218
+ selector: "app-skeleton",
2219
+ inputs: [
2220
+ "lines",
2221
+ "height",
2222
+ "width"
2223
+ ]
2224
+ }
2225
+ ]
2226
+ });
2227
+ };
2228
+ i0.ɵɵngDeclareClassMetadata({
2229
+ minVersion: "12.0.0",
2230
+ version: "22.2.3",
2231
+ ngImport: i0,
2232
+ type: ListView,
2233
+ decorators: [{
2234
+ type: Component,
2235
+ args: [{
2236
+ selector: "app-list-view",
2237
+ imports: [
2238
+ Button,
2239
+ CdkDrag,
2240
+ CdkDragHandle,
2241
+ CdkDropList,
2242
+ DrawerForm,
2243
+ EmptyState,
2244
+ LucideChevronDown,
2245
+ LucideChevronRight,
2246
+ LucideChevronUp,
2247
+ LucideEye,
2248
+ LucideGripVertical,
2249
+ LucidePencil,
2250
+ LucidePlus,
2251
+ LucideRotateCw,
2252
+ LucideDynamicIcon,
2253
+ LucideTrash2,
2254
+ LucideZap,
2255
+ RenderHost,
2256
+ Skeleton
2257
+ ],
2258
+ template: `
2259
+ <section class="space-y-4" aria-label="Listado" [attr.aria-busy]="status() === 'loading'">
2260
+ <div class="flex flex-wrap items-end justify-between gap-3">
2261
+ <div class="flex flex-wrap items-end gap-3">
2262
+ @if (schema().search?.length) {
2263
+ <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
2264
+ Buscar
2265
+ <input
2266
+ type="search"
2267
+ class="w-56 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
2268
+ placeholder="Buscar…"
2269
+ [value]="query()"
2270
+ (input)="setQuery(inputValue($event))"
2271
+ />
2272
+ </label>
2273
+ }
2274
+ @for (filter of schema().filters ?? []; track filter.key) {
2275
+ <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
2276
+ {{ filter.label }}
2277
+ @if (filter.type === 'text') {
2278
+ <input
2279
+ type="search"
2280
+ class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
2281
+ [value]="filterValues()[filter.key] ?? ''"
2282
+ (input)="setFilter(filter.key, inputValue($event))"
2283
+ />
2284
+ } @else {
2285
+ <select
2286
+ class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
2287
+ [value]="filterValues()[filter.key] ?? ''"
2288
+ (change)="setFilter(filter.key, selectValue($event))"
2289
+ >
2290
+ @for (option of filterOptions(filter); track option.value) {
2291
+ <option [value]="option.value">{{ option.label }}</option>
2292
+ }
2293
+ </select>
2294
+ }
2295
+ </label>
2296
+ }
2297
+ </div>
2298
+ @if (canCreate()) {
2299
+ <button app-button type="button" (click)="goCreate()">
2300
+ <svg lucidePlus size="16" />
2301
+ Crear {{ schema().label }}
2302
+ </button>
2303
+ }
2304
+ </div>
2305
+
2306
+ @switch (status()) {
2307
+ @case ('loading') {
2308
+ <app-skeleton [lines]="6" [height]="44" />
2309
+ }
2310
+ @case ('error') {
2311
+ <app-empty-state
2312
+ title="No se pudo cargar el listado"
2313
+ message="Revisa tu conexión e intenta de nuevo."
2314
+ >
2315
+ <button app-button variant="outline" type="button" (click)="retry()">
2316
+ <svg lucideRotateCw size="16" />
2317
+ Reintentar
2318
+ </button>
2319
+ </app-empty-state>
2320
+ }
2321
+ @case ('ready') {
2322
+ @if (visibleRows().length === 0) {
2323
+ <app-empty-state
2324
+ [title]="rows().length === 0 ? 'Sin registros' : 'Sin resultados'"
2325
+ [message]="
2326
+ rows().length === 0
2327
+ ? 'Todavía no hay registros en este recurso.'
2328
+ : 'Ningún registro coincide con la búsqueda o los filtros.'
2329
+ "
2330
+ >
2331
+ @if (rows().length > 0) {
2332
+ <button app-button variant="outline" type="button" (click)="clearFilters()">
2333
+ Limpiar filtros
2334
+ </button>
2335
+ } @else if (canCreate()) {
2336
+ <button app-button type="button" (click)="goCreate()">
2337
+ <svg lucidePlus size="16" />
2338
+ Crear el primero
2339
+ </button>
2340
+ }
2341
+ </app-empty-state>
2342
+ } @else {
2343
+ <!-- La tabla es la vista de escritorio: se oculta bajo lg para
2344
+ que la lista de tarjetas de abajo sea la ÚNICA vista en
2345
+ móvil (con las dos visibles, el listado se duplicaba). -->
2346
+ <div
2347
+ class="hidden overflow-x-auto rounded-xl border border-neutral/20 bg-surface lg:block"
2348
+ >
2349
+ <table class="w-full text-sm">
2350
+ <thead>
2351
+ <tr class="border-b border-neutral/20 text-left text-xs uppercase text-neutral">
2352
+ @if (canReorder()) {
2353
+ <th scope="col" class="w-16 px-2 py-3 font-medium">
2354
+ <span class="sr-only">Orden</span>
2355
+ </th>
2356
+ }
2357
+ @for (column of schema().listColumns; track column.key) {
2358
+ <th scope="col" class="px-4 py-3 font-medium">
2359
+ {{ column.label }}
2360
+ </th>
2361
+ }
2362
+ @if (hasRowActions()) {
2363
+ <th scope="col" class="px-4 py-3 text-right font-medium">Acciones</th>
2364
+ }
2365
+ </tr>
2366
+ </thead>
2367
+ <tbody
2368
+ cdkDropList
2369
+ [cdkDropListData]="visibleRows()"
2370
+ [cdkDropListDisabled]="dropDisabled()"
2371
+ (cdkDropListDropped)="onDrop($event)"
2372
+ >
2373
+ @for (row of visibleRows(); track keyOf(row); let i = $index) {
2374
+ <tr
2375
+ class="border-b border-neutral/10 last:border-0 hover:bg-neutral/5 transition-colors"
2376
+ [class.cursor-pointer]="isRowClickable()"
2377
+ cdkDrag
2378
+ [cdkDragDisabled]="dropDisabled()"
2379
+ (click)="onRowClick(row)"
2380
+ >
2381
+ @if (canReorder()) {
2382
+ <td class="px-2 py-2.5 align-middle" (click)="$event.stopPropagation()">
2383
+ <div class="flex items-center gap-1">
2384
+ <button
2385
+ type="button"
2386
+ class="cursor-grab text-neutral hover:text-primary"
2387
+ cdkDragHandle
2388
+ [attr.aria-label]="'Reordenar ' + titleOf(row)"
2389
+ title="Arrastrar para reordenar"
2390
+ >
2391
+ <svg lucideGripVertical size="16" />
2392
+ </button>
2393
+ <span class="flex flex-col leading-none">
2394
+ <button
2395
+ type="button"
2396
+ class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
2397
+ [disabled]="i === 0 || reordering()"
2398
+ [attr.aria-label]="'Subir ' + titleOf(row)"
2399
+ (click)="moveBy(keyOf(row), -1)"
2400
+ >
2401
+ <svg lucideChevronUp size="16" />
2402
+ </button>
2403
+ <button
2404
+ type="button"
2405
+ class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
2406
+ [disabled]="i === visibleRows().length - 1 || reordering()"
2407
+ [attr.aria-label]="'Bajar ' + titleOf(row)"
2408
+ (click)="moveBy(keyOf(row), 1)"
2409
+ >
2410
+ <svg lucideChevronDown size="16" />
2411
+ </button>
2412
+ </span>
2413
+ </div>
2414
+ </td>
2415
+ }
2416
+ @for (column of schema().listColumns; track column.key) {
2417
+ <td class="px-4 py-2.5 align-middle">
2418
+ <!-- Los 7 formatos salen del registro (R4.3):
2419
+ la vista sólo elige cuál y entrega la celda. -->
2420
+ <app-render-host
2421
+ kind="column-format"
2422
+ [type]="formatOf(column)"
2423
+ [config]="column"
2424
+ [data]="cellOf(row, column)"
2425
+ />
2426
+ </td>
2427
+ }
2428
+ @if (hasRowActions()) {
2429
+ <td class="px-4 py-2.5" (click)="$event.stopPropagation()">
2430
+ <div class="flex items-center justify-end gap-1">
2431
+ @if (schema().detail?.enabled) {
2432
+ <button
2433
+ app-button
2434
+ variant="ghost"
2435
+ type="button"
2436
+ (click)="goView(row)"
2437
+ title="Ver ficha de detalle"
2438
+ class="h-8 px-2.5 text-xs text-neutral-light hover:text-primary hover:bg-primary/10"
2439
+ >
2440
+ <svg lucideEye size="15" />
2441
+ <span>Ver</span>
2442
+ </button>
2443
+ }
2444
+ @for (action of schema().actions ?? []; track action.id) {
2445
+ <button
2446
+ app-button
2447
+ variant="ghost"
2448
+ type="button"
2449
+ [title]="action.label"
2450
+ [attr.aria-label]="action.label"
2451
+ (click)="runAction(action, row)"
2452
+ class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-primary/10"
2453
+ >
2454
+ @let actionIcon = iconOf(action);
2455
+ @if (actionIcon) {
2456
+ <svg [lucideIcon]="actionIcon" size="15" />
2457
+ } @else {
2458
+ {{ action.label }}
2459
+ }
2460
+ </button>
2461
+ }
2462
+ @if (canQuickEdit()) {
2463
+ <button
2464
+ app-button
2465
+ variant="ghost"
2466
+ type="button"
2467
+ (click)="quickEditRow.set(row)"
2468
+ title="Edición rápida"
2469
+ class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-neutral/10"
2470
+ >
2471
+ <svg lucideZap size="15" />
2472
+ <span>Edición rápida</span>
2473
+ </button>
2474
+ }
2475
+ @if (canUpdate()) {
2476
+ <button
2477
+ app-button
2478
+ variant="ghost"
2479
+ type="button"
2480
+ (click)="goEdit(row)"
2481
+ title="Editar registro"
2482
+ class="h-8 px-2.5 text-xs text-primary hover:bg-primary/10"
2483
+ >
2484
+ <svg lucidePencil size="15" />
2485
+ <span>Editar</span>
2486
+ </button>
2487
+ }
2488
+ @if (canRemove()) {
2489
+ <button
2490
+ app-button
2491
+ variant="ghost"
2492
+ type="button"
2493
+ (click)="removeRow(row)"
2494
+ title="Eliminar registro"
2495
+ class="h-8 px-2 text-xs text-danger hover:bg-danger/10"
2496
+ >
2497
+ <svg lucideTrash2 size="15" />
2498
+ <span>Eliminar</span>
2499
+ </button>
2500
+ }
2501
+ </div>
2502
+ </td>
2503
+ }
2504
+ </tr>
2505
+ }
2506
+ </tbody>
2507
+ </table>
2508
+ </div>
2509
+
2510
+ <!-- Vista de tarjetas — mobile-first (guía mobile §6).
2511
+ La tabla de arriba necesita scroll horizontal con 5–7 columnas
2512
+ (el anti-patrón #2): en pantallas chicas se convierte en una
2513
+ lista de tarjetas de dos niveles (principal + secundario),
2514
+ donde cada fila completa es un objetivo táctil. lg:hidden la
2515
+ retira cuando la tabla vuelve a ser usable. -->
2516
+ <ul class="space-y-2 lg:hidden">
2517
+ @for (row of visibleRows(); track keyOf(row)) {
2518
+ <li
2519
+ class="rounded-xl border border-neutral/20 bg-surface p-3"
2520
+ [class.cursor-pointer]="isRowClickable()"
2521
+ (click)="onRowClick(row)"
2522
+ >
2523
+ <!-- Nivel principal: el título del registro, siempre visible
2524
+ (guía §6.2: el identificador nunca se esconde). -->
2525
+ <div class="flex items-start justify-between gap-2">
2526
+ <span class="text-sm font-medium">
2527
+ {{ titleOf(row) }}
2528
+ </span>
2529
+ @if (schema().detail?.enabled) {
2530
+ <svg
2531
+ lucideChevronRight
2532
+ size="16"
2533
+ class="mt-0.5 shrink-0 text-neutral"
2534
+ aria-hidden="true"
2535
+ />
2536
+ }
2537
+ </div>
2538
+ <!-- Nivel secundario: el resto de las columnas como pares
2539
+ etiqueta/valor. Sin hover: todo lo accionable va en el
2540
+ pie de la tarjeta, al alcance del pulgar. -->
2541
+ <dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
2542
+ @for (column of schema().listColumns; track column.key) {
2543
+ <dt class="text-neutral">{{ column.label }}</dt>
2544
+ <dd>
2545
+ <app-render-host
2546
+ kind="column-format"
2547
+ [type]="formatOf(column)"
2548
+ [config]="column"
2549
+ [data]="cellOf(row, column)"
2550
+ />
2551
+ </dd>
2552
+ }
2553
+ </dl>
2554
+ @if (hasRowActions()) {
2555
+ <!-- Pie táctil: objetivos ≥44 px (guía §8.2), sin hover. -->
2556
+ <div class="mt-3 flex flex-wrap gap-2 border-t border-neutral/10 pt-2">
2557
+ @if (schema().detail?.enabled) {
2558
+ <button
2559
+ app-button
2560
+ variant="outline"
2561
+ type="button"
2562
+ size="sm"
2563
+ class="flex-1"
2564
+ (click)="$event.stopPropagation(); goView(row)"
2565
+ >
2566
+ <svg lucideEye size="15" aria-hidden="true" />
2567
+ <span>Ver</span>
2568
+ </button>
2569
+ }
2570
+ @for (action of schema().actions ?? []; track action.id) {
2571
+ <button
2572
+ app-button
2573
+ variant="outline"
2574
+ type="button"
2575
+ size="sm"
2576
+ class="flex-1"
2577
+ (click)="$event.stopPropagation(); runAction(action, row)"
2578
+ >
2579
+ @let actionIcon = iconOf(action);
2580
+ @if (actionIcon) {
2581
+ <svg [lucideIcon]="actionIcon" size="15" aria-hidden="true" />
2582
+ }
2583
+ <span>{{ action.label }}</span>
2584
+ </button>
2585
+ }
2586
+ <!-- Editar es O una vía u otra, como en la tabla: si el
2587
+ schema declara quickEdit va el atajo; si no, la
2588
+ pantalla completa. Apilados los dos sería ruido. -->
2589
+ @if (canQuickEdit()) {
2590
+ <button
2591
+ app-button
2592
+ variant="outline"
2593
+ type="button"
2594
+ size="sm"
2595
+ class="flex-1"
2596
+ (click)="$event.stopPropagation(); quickEditRow.set(row)"
2597
+ >
2598
+ <svg lucideZap size="15" aria-hidden="true" />
2599
+ <span>Editar</span>
2600
+ </button>
2601
+ } @else if (canUpdate()) {
2602
+ <button
2603
+ app-button
2604
+ variant="outline"
2605
+ type="button"
2606
+ size="sm"
2607
+ class="flex-1"
2608
+ (click)="$event.stopPropagation(); goEdit(row)"
2609
+ >
2610
+ <svg lucidePencil size="15" aria-hidden="true" />
2611
+ <span>Editar</span>
2612
+ </button>
2613
+ }
2614
+ @if (canRemove()) {
2615
+ <button
2616
+ app-button
2617
+ variant="danger"
2618
+ type="button"
2619
+ size="sm"
2620
+ class="flex-1"
2621
+ (click)="$event.stopPropagation(); removeRow(row)"
2622
+ >
2623
+ <svg lucideTrash2 size="15" aria-hidden="true" />
2624
+ <span>Eliminar</span>
2625
+ </button>
2626
+ }
2627
+ </div>
2628
+ }
2629
+ </li>
2630
+ }
2631
+ </ul>
2632
+ }
2633
+ }
2634
+ }
2635
+
2636
+ @if (quickEditRow(); as row) {
2637
+ <app-drawer-form
2638
+ [open]="true"
2639
+ [schema]="schema()"
2640
+ mode="edit"
2641
+ [recordKey]="keyOf(row)"
2642
+ [fields]="quickEditFields()"
2643
+ title="Edición rápida"
2644
+ size="sm"
2645
+ (close)="quickEditRow.set(null)"
2646
+ (saved)="quickEditSaved()"
2647
+ />
2648
+ }
2649
+ </section>
2650
+ `
2651
+ }]
2652
+ }],
2653
+ ctorParameters: () => [],
2654
+ propDecorators: {
2655
+ schema: [{
2656
+ type: i0.Input,
2657
+ args: [{
2658
+ isSignal: true,
2659
+ alias: "schema",
2660
+ required: true
2661
+ }]
2662
+ }],
2663
+ viewRecord: [{
2664
+ type: i0.Output,
2665
+ args: ["viewRecord"]
2666
+ }]
2667
+ }
2668
+ });
2669
+ var DetailView = class DetailView {
2670
+ api = inject(ApiService, { optional: true });
2671
+ actionService = inject(ActionService);
2672
+ auth = inject(AuthService);
2673
+ router = inject(Router);
2674
+ nav = inject(ViewNavigationService);
2675
+ notifications = inject(NotificationService);
2676
+ configService = inject(ConfigService);
2677
+ constructor() {
2678
+ this.configService.ensureLoaded();
2679
+ }
2680
+ schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
2681
+ record = input.required(...ngDevMode ? [{ debugName: "record" }] : /* istanbul ignore next */ []);
2682
+ edit = output();
2683
+ back = output();
2684
+ delete = output();
2685
+ refreshTick = signal(0, ...ngDevMode ? [{ debugName: "refreshTick" }] : /* istanbul ignore next */ []);
2686
+ fetched = toSignal(combineLatest([toObservable(this.record), toObservable(this.refreshTick)]).pipe(switchMap(([rec]) => {
2687
+ const schema = this.schema();
2688
+ const key = rec ? String(rec[schema.keyField] ?? "") : "";
2689
+ const getEndpoint = schema.endpoint?.get;
2690
+ if (!key || !getEndpoint || typeof this.api?.get !== "function") return of({
2691
+ phase: "done",
2692
+ data: null
2693
+ });
2694
+ return this.api.get(schema.endpoint, key).pipe(map((data) => ({
2695
+ phase: "done",
2696
+ data
2697
+ })), catchError((error) => of({
2698
+ phase: "error",
2699
+ error
2700
+ })));
2701
+ })), { initialValue: { phase: "pending" } });
2702
+ status = computed(() => {
2703
+ const fetched = this.fetched();
2704
+ if (fetched.phase === "pending") return "loading";
2705
+ if (fetched.phase === "error") return "error";
2706
+ return "ready";
2707
+ }, ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
2708
+ retry() {
2709
+ this.refreshTick.update((tick) => tick + 1);
2710
+ }
2711
+ errorDeCarga() {
2712
+ const fetched = this.fetched();
2713
+ return fetched.phase === "error" ? fetched.error : void 0;
2714
+ }
2715
+ volverDeError() {
2716
+ this.back.emit();
2717
+ this.nav.back();
2718
+ }
2719
+ effectiveRecord = computed(() => {
2720
+ const initial = this.record();
2721
+ const fetched = this.fetched();
2722
+ if (fetched.phase === "done" && fetched.data && typeof fetched.data === "object" && Object.keys(fetched.data).length > 0) return {
2723
+ ...initial,
2724
+ ...fetched.data
2725
+ };
2726
+ return initial;
2727
+ }, ...ngDevMode ? [{ debugName: "effectiveRecord" }] : /* istanbul ignore next */ []);
2728
+ detail = computed(() => this.schema().detail ?? { enabled: true }, ...ngDevMode ? [{ debugName: "detail" }] : /* istanbul ignore next */ []);
2729
+ title = computed(() => {
2730
+ const rec = this.effectiveRecord();
2731
+ const schema = this.schema();
2732
+ const raw = rec[schema.titleField];
2733
+ return raw !== void 0 && raw !== null && raw !== "" ? String(raw) : schema.label;
2734
+ }, ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
2735
+ recordKey = computed(() => {
2736
+ const rec = this.effectiveRecord();
2737
+ const schema = this.schema();
2738
+ return String(rec[schema.keyField] ?? "—");
2739
+ }, ...ngDevMode ? [{ debugName: "recordKey" }] : /* istanbul ignore next */ []);
2740
+ headerFields = computed(() => {
2741
+ return (this.detail().headerFields ?? []).map((key) => this.resolveFieldValue(key)).filter((f) => f !== null);
2742
+ }, ...ngDevMode ? [{ debugName: "headerFields" }] : /* istanbul ignore next */ []);
2743
+ sections = computed(() => {
2744
+ const schema = this.schema();
2745
+ const detailConfig = this.detail();
2746
+ if (detailConfig.sections && detailConfig.sections.length > 0) return detailConfig.sections.map((sec) => ({
2747
+ id: sec.id,
2748
+ label: sec.label,
2749
+ fields: sec.fields.map((key) => this.resolveFieldValue(key)).filter((f) => f !== null)
2750
+ })).filter((sec) => sec.fields.length > 0);
2751
+ const layoutSections = schema.layout?.sections ?? [];
2752
+ const sectionMap = /* @__PURE__ */ new Map();
2753
+ for (const layoutSection of layoutSections) sectionMap.set(layoutSection.id, {
2754
+ id: layoutSection.id,
2755
+ label: layoutSection.label,
2756
+ fields: []
2757
+ });
2758
+ const unsectioned = {
2759
+ id: "_unsectioned",
2760
+ label: schema.label,
2761
+ fields: []
2762
+ };
2763
+ for (const field of schema.fields) {
2764
+ const value = this.resolveFieldValue(field.key);
2765
+ if (value === null) continue;
2766
+ if (field.section && sectionMap.has(field.section)) sectionMap.get(field.section).fields.push(value);
2767
+ else unsectioned.fields.push(value);
2768
+ }
2769
+ const result = [];
2770
+ if (unsectioned.fields.length > 0) result.push(unsectioned);
2771
+ for (const sec of sectionMap.values()) if (sec.fields.length > 0) result.push(sec);
2772
+ return result;
2773
+ }, ...ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []);
2774
+ copyShareActions = computed(() => {
2775
+ return (this.schema().actions ?? this.detail().actions ?? []).filter(isShareAction);
2776
+ }, ...ngDevMode ? [{ debugName: "copyShareActions" }] : /* istanbul ignore next */ []);
2777
+ triggerActions = computed(() => {
2778
+ return (this.schema().actions ?? this.detail().actions ?? []).filter(isTriggerAction);
2779
+ }, ...ngDevMode ? [{ debugName: "triggerActions" }] : /* istanbul ignore next */ []);
2780
+ async runTrigger(action) {
2781
+ if (!this.api) return;
2782
+ const result = await this.actionService.run(action, {
2783
+ record: this.effectiveRecord(),
2784
+ resource: this.schema().id,
2785
+ api: this.api,
2786
+ router: this.router,
2787
+ toast: this.notifications,
2788
+ user: this.auth.user(),
2789
+ keyField: this.schema().keyField
2790
+ });
2791
+ if (result.ok && result.reload) this.refreshTick.update((tick) => tick + 1);
2792
+ }
2793
+ resolveFieldValue(key) {
2794
+ const schema = this.schema();
2795
+ const rec = this.effectiveRecord();
2796
+ const field = schema.fields.find((f) => f.key === key);
2797
+ const raw = getValue(rec, key);
2798
+ const declarado = field ?? {
2799
+ key,
2800
+ label: key.replace(/_/g, " "),
2801
+ type: "text"
2802
+ };
2803
+ if (!this.auth.canReadField(declarado)) return null;
2804
+ return {
2805
+ key,
2806
+ label: field?.label ?? declarado.label,
2807
+ field: declarado,
2808
+ raw
2809
+ };
2810
+ }
2811
+ vacio(valor) {
2812
+ return valor === null || valor === void 0 || valor === "";
2813
+ }
2814
+ esHistorial(campo) {
2815
+ return campo.field.type === "list" && !this.vacio(campo.raw);
2816
+ }
2817
+ regularFields(section) {
2818
+ return section.fields.filter((f) => !this.esHistorial(f));
2819
+ }
2820
+ listFields(section) {
2821
+ return section.fields.filter((f) => this.esHistorial(f));
2822
+ }
2823
+ hasLargeContent(section) {
2824
+ return section.fields.some((f) => this.esHistorial(f) || f.key === "description");
2825
+ }
2826
+ shareOf(action) {
2827
+ return resolveShare(action, this.actionService.vars(this.effectiveRecord()));
2828
+ }
2829
+ static ɵfac = i0.ɵɵngDeclareFactory({
2830
+ minVersion: "12.0.0",
2831
+ version: "22.2.3",
2832
+ ngImport: i0,
2833
+ type: DetailView,
2834
+ deps: [],
2835
+ target: i0.ɵɵFactoryTarget.Component
2836
+ });
2837
+ static ɵcmp = i0.ɵɵngDeclareComponent({
2838
+ minVersion: "17.0.0",
2839
+ version: "22.2.3",
2840
+ type: DetailView,
2841
+ isStandalone: true,
2842
+ selector: "app-detail-view",
2843
+ inputs: {
2844
+ schema: {
2845
+ classPropertyName: "schema",
2846
+ publicName: "schema",
2847
+ isSignal: true,
2848
+ isRequired: true,
2849
+ transformFunction: null
2850
+ },
2851
+ record: {
2852
+ classPropertyName: "record",
2853
+ publicName: "record",
2854
+ isSignal: true,
2855
+ isRequired: true,
2856
+ transformFunction: null
2857
+ }
2858
+ },
2859
+ outputs: {
2860
+ edit: "edit",
2861
+ back: "back",
2862
+ delete: "delete"
2863
+ },
2864
+ ngImport: i0,
2865
+ template: `
2866
+ <div class="space-y-6">
2867
+ <!-- ── Cabecera Principal ── -->
2868
+ <header
2869
+ class="flex flex-wrap items-start justify-between gap-4 rounded-2xl border border-neutral/15 bg-surface p-6 shadow-xs"
2870
+ >
2871
+ <div class="space-y-2">
2872
+ <div class="flex flex-wrap items-center gap-2.5">
2873
+ <h1 class="font-display text-2xl font-bold tracking-tight text-neutral-light">
2874
+ {{ title() }}
2875
+ </h1>
2876
+ @for (campo of headerFields(); track campo.key) {
2877
+ @if (!vacio(campo.raw)) {
2878
+ <!-- La cabecera siempre mostró los headerFields como chips:
2879
+ el matiz viaja al host, no un @switch por tipo acá. -->
2880
+ <app-render-host
2881
+ mode="display"
2882
+ [chip]="true"
2883
+ [type]="campo.field.type"
2884
+ [config]="campo.field"
2885
+ [value]="campo.raw"
2886
+ />
2887
+ }
2888
+ }
2889
+ </div>
2890
+ <p class="text-sm text-neutral">
2891
+ {{ schema().label }} · Identificador:
2892
+ <span class="font-mono font-medium text-neutral-light">{{ recordKey() }}</span>
2893
+ </p>
2894
+ </div>
2895
+
2896
+ <div class="flex flex-wrap items-center gap-2.5">
2897
+ <button app-button variant="outline" type="button" (click)="back.emit()">
2898
+ <svg lucideArrowLeft size="16" />
2899
+ Volver
2900
+ </button>
2901
+ @if (detail().allowEdit !== false && status() !== 'error') {
2902
+ <button app-button variant="primary" type="button" (click)="edit.emit(record())">
2903
+ <svg lucidePencil size="16" />
2904
+ Editar
2905
+ </button>
2906
+ }
2907
+ @if (detail().allowDelete !== false && status() !== 'error') {
2908
+ <button app-button variant="danger" type="button" (click)="delete.emit(record())">
2909
+ <svg lucideTrash2 size="16" />
2910
+ Eliminar
2911
+ </button>
2912
+ }
2913
+ </div>
2914
+ </header>
2915
+
2916
+ <!-- ── Acciones Destacadas del Registro (si existen) ── -->
2917
+ @if (copyShareActions().length > 0 || triggerActions().length > 0) {
2918
+ <section
2919
+ class="rounded-2xl border border-primary/25 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent p-5 shadow-xs"
2920
+ >
2921
+ <div class="mb-3 flex items-center gap-2">
2922
+ <svg lucideExternalLink size="16" class="text-primary" />
2923
+ <h2 class="text-sm font-semibold text-primary">Acciones del Registro</h2>
2924
+ </div>
2925
+ <div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap">
2926
+ @for (action of copyShareActions(); track action.id) {
2927
+ <div class="flex-1 min-w-[280px]">
2928
+ @let share = shareOf(action);
2929
+ <app-copy-share
2930
+ [url]="share.url"
2931
+ [shareText]="share.shareText"
2932
+ [label]="action.label"
2933
+ />
2934
+ </div>
2935
+ }
2936
+ @for (action of triggerActions(); track action.id) {
2937
+ <button app-button variant="outline" type="button" (click)="runTrigger(action)">
2938
+ {{ action.label }}
2939
+ </button>
2940
+ }
2941
+ </div>
2942
+ </section>
2943
+ }
2944
+
2945
+ <!-- ── Secciones de Datos ── -->
2946
+ @switch (status()) {
2947
+ @case ('loading') {
2948
+ <!-- Skeleton con la forma de las tarjetas de sección: reserva el
2949
+ espacio (sin layout jump) y distingue «cargando» de «vacío».
2950
+ La cabecera y las acciones de arriba sí se pintan ya: son la
2951
+ fila de la lista, datos reales que no están en disputa. -->
2952
+ <div aria-busy="true" class="grid grid-cols-1 gap-6 lg:grid-cols-2">
2953
+ @for (placeholder of [0, 1]; track placeholder) {
2954
+ <section
2955
+ class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden"
2956
+ >
2957
+ <header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
2958
+ <app-skeleton [lines]="1" [height]="14" width="30%" />
2959
+ </header>
2960
+ <div class="p-5">
2961
+ <app-skeleton [lines]="6" [height]="44" />
2962
+ </div>
2963
+ </section>
2964
+ }
2965
+ </div>
2966
+ }
2967
+ @case ('error') {
2968
+ <app-empty-state
2969
+ title="No se pudo cargar el registro"
2970
+ message="Revisa tu conexión e intenta de nuevo."
2971
+ >
2972
+ <button app-button variant="outline" type="button" (click)="retry()">
2973
+ <svg lucideRotateCw size="16" />
2974
+ Reintentar
2975
+ </button>
2976
+ <button app-button variant="outline" type="button" (click)="volverDeError()">
2977
+ <svg lucideArrowLeft size="16" />
2978
+ Volver
2979
+ </button>
2980
+ </app-empty-state>
2981
+ }
2982
+ @case ('ready') {
2983
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
2984
+ @for (section of sections(); track section.id) {
2985
+ <section
2986
+ class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden flex flex-col"
2987
+ [class.lg:col-span-2]="hasLargeContent(section)"
2988
+ >
2989
+ <header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
2990
+ <h2 class="text-sm font-semibold tracking-wide text-neutral-light uppercase">
2991
+ {{ section.label }}
2992
+ </h2>
2993
+ </header>
2994
+
2995
+ <div class="p-5 flex-1 space-y-4">
2996
+ <!-- Grid para campos regulares -->
2997
+ @if (regularFields(section).length > 0) {
2998
+ <dl class="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
2999
+ @for (campo of regularFields(section); track campo.key) {
3000
+ <div class="space-y-1">
3001
+ <dt class="text-xs font-medium text-neutral">{{ campo.label }}</dt>
3002
+ <dd class="text-sm font-medium text-neutral-light break-words">
3003
+ <app-render-host
3004
+ mode="display"
3005
+ [type]="campo.field.type"
3006
+ [config]="campo.field"
3007
+ [value]="campo.raw"
3008
+ />
3009
+ </dd>
3010
+ </div>
3011
+ }
3012
+ </dl>
3013
+ }
3014
+
3015
+ <!-- Renderizado de listas anidadas (Timeline de eventos / items) -->
3016
+ @for (campo of listFields(section); track campo.key) {
3017
+ <div class="pt-2">
3018
+ <h3 class="mb-3 text-xs font-semibold text-neutral uppercase">
3019
+ {{ campo.label }}
3020
+ </h3>
3021
+ <app-render-host
3022
+ mode="display"
3023
+ [type]="campo.field.type"
3024
+ [config]="campo.field"
3025
+ [value]="campo.raw"
3026
+ />
3027
+ </div>
3028
+ }
3029
+ </div>
3030
+ </section>
3031
+ }
3032
+ </div>
3033
+ }
3034
+ }
3035
+ </div>
3036
+ `,
3037
+ isInline: true,
3038
+ dependencies: [
3039
+ {
3040
+ kind: "component",
3041
+ type: Button,
3042
+ selector: "button[app-button]",
3043
+ inputs: [
3044
+ "variant",
3045
+ "size",
3046
+ "type",
3047
+ "disabled",
3048
+ "loading"
3049
+ ]
3050
+ },
3051
+ {
3052
+ kind: "component",
3053
+ type: CopyShare,
3054
+ selector: "app-copy-share",
3055
+ inputs: [
3056
+ "url",
3057
+ "label",
3058
+ "shareText"
3059
+ ]
3060
+ },
3061
+ {
3062
+ kind: "component",
3063
+ type: EmptyState,
3064
+ selector: "app-empty-state",
3065
+ inputs: ["title", "message"]
3066
+ },
3067
+ {
3068
+ kind: "component",
3069
+ type: LucideArrowLeft,
3070
+ selector: "svg[lucideArrowLeft]"
3071
+ },
3072
+ {
3073
+ kind: "component",
3074
+ type: LucideExternalLink,
3075
+ selector: "svg[lucideExternalLink]"
3076
+ },
3077
+ {
3078
+ kind: "component",
3079
+ type: LucidePencil,
3080
+ selector: "svg[lucidePencil]"
3081
+ },
3082
+ {
3083
+ kind: "component",
3084
+ type: LucideRotateCw,
3085
+ selector: "svg[lucideRotateCw]"
3086
+ },
3087
+ {
3088
+ kind: "component",
3089
+ type: LucideTrash2,
3090
+ selector: "svg[lucideTrash], svg[lucideTrash2]"
3091
+ },
3092
+ {
3093
+ kind: "component",
3094
+ type: RenderHost,
3095
+ selector: "app-render-host",
3096
+ inputs: [
3097
+ "kind",
3098
+ "type",
3099
+ "config",
3100
+ "ctx",
3101
+ "data",
3102
+ "mode",
3103
+ "value",
3104
+ "chip"
3105
+ ]
3106
+ },
3107
+ {
3108
+ kind: "component",
3109
+ type: Skeleton,
3110
+ selector: "app-skeleton",
3111
+ inputs: [
3112
+ "lines",
3113
+ "height",
3114
+ "width"
3115
+ ]
3116
+ }
3117
+ ]
3118
+ });
3119
+ };
3120
+ i0.ɵɵngDeclareClassMetadata({
3121
+ minVersion: "12.0.0",
3122
+ version: "22.2.3",
3123
+ ngImport: i0,
3124
+ type: DetailView,
3125
+ decorators: [{
3126
+ type: Component,
3127
+ args: [{
3128
+ selector: "app-detail-view",
3129
+ imports: [
3130
+ Button,
3131
+ CopyShare,
3132
+ EmptyState,
3133
+ LucideArrowLeft,
3134
+ LucideExternalLink,
3135
+ LucidePencil,
3136
+ LucideRotateCw,
3137
+ LucideTrash2,
3138
+ RenderHost,
3139
+ Skeleton
3140
+ ],
3141
+ template: `
3142
+ <div class="space-y-6">
3143
+ <!-- ── Cabecera Principal ── -->
3144
+ <header
3145
+ class="flex flex-wrap items-start justify-between gap-4 rounded-2xl border border-neutral/15 bg-surface p-6 shadow-xs"
3146
+ >
3147
+ <div class="space-y-2">
3148
+ <div class="flex flex-wrap items-center gap-2.5">
3149
+ <h1 class="font-display text-2xl font-bold tracking-tight text-neutral-light">
3150
+ {{ title() }}
3151
+ </h1>
3152
+ @for (campo of headerFields(); track campo.key) {
3153
+ @if (!vacio(campo.raw)) {
3154
+ <!-- La cabecera siempre mostró los headerFields como chips:
3155
+ el matiz viaja al host, no un @switch por tipo acá. -->
3156
+ <app-render-host
3157
+ mode="display"
3158
+ [chip]="true"
3159
+ [type]="campo.field.type"
3160
+ [config]="campo.field"
3161
+ [value]="campo.raw"
3162
+ />
3163
+ }
3164
+ }
3165
+ </div>
3166
+ <p class="text-sm text-neutral">
3167
+ {{ schema().label }} · Identificador:
3168
+ <span class="font-mono font-medium text-neutral-light">{{ recordKey() }}</span>
3169
+ </p>
3170
+ </div>
3171
+
3172
+ <div class="flex flex-wrap items-center gap-2.5">
3173
+ <button app-button variant="outline" type="button" (click)="back.emit()">
3174
+ <svg lucideArrowLeft size="16" />
3175
+ Volver
3176
+ </button>
3177
+ @if (detail().allowEdit !== false && status() !== 'error') {
3178
+ <button app-button variant="primary" type="button" (click)="edit.emit(record())">
3179
+ <svg lucidePencil size="16" />
3180
+ Editar
3181
+ </button>
3182
+ }
3183
+ @if (detail().allowDelete !== false && status() !== 'error') {
3184
+ <button app-button variant="danger" type="button" (click)="delete.emit(record())">
3185
+ <svg lucideTrash2 size="16" />
3186
+ Eliminar
3187
+ </button>
3188
+ }
3189
+ </div>
3190
+ </header>
3191
+
3192
+ <!-- ── Acciones Destacadas del Registro (si existen) ── -->
3193
+ @if (copyShareActions().length > 0 || triggerActions().length > 0) {
3194
+ <section
3195
+ class="rounded-2xl border border-primary/25 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent p-5 shadow-xs"
3196
+ >
3197
+ <div class="mb-3 flex items-center gap-2">
3198
+ <svg lucideExternalLink size="16" class="text-primary" />
3199
+ <h2 class="text-sm font-semibold text-primary">Acciones del Registro</h2>
3200
+ </div>
3201
+ <div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap">
3202
+ @for (action of copyShareActions(); track action.id) {
3203
+ <div class="flex-1 min-w-[280px]">
3204
+ @let share = shareOf(action);
3205
+ <app-copy-share
3206
+ [url]="share.url"
3207
+ [shareText]="share.shareText"
3208
+ [label]="action.label"
3209
+ />
3210
+ </div>
3211
+ }
3212
+ @for (action of triggerActions(); track action.id) {
3213
+ <button app-button variant="outline" type="button" (click)="runTrigger(action)">
3214
+ {{ action.label }}
3215
+ </button>
3216
+ }
3217
+ </div>
3218
+ </section>
3219
+ }
3220
+
3221
+ <!-- ── Secciones de Datos ── -->
3222
+ @switch (status()) {
3223
+ @case ('loading') {
3224
+ <!-- Skeleton con la forma de las tarjetas de sección: reserva el
3225
+ espacio (sin layout jump) y distingue «cargando» de «vacío».
3226
+ La cabecera y las acciones de arriba sí se pintan ya: son la
3227
+ fila de la lista, datos reales que no están en disputa. -->
3228
+ <div aria-busy="true" class="grid grid-cols-1 gap-6 lg:grid-cols-2">
3229
+ @for (placeholder of [0, 1]; track placeholder) {
3230
+ <section
3231
+ class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden"
3232
+ >
3233
+ <header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
3234
+ <app-skeleton [lines]="1" [height]="14" width="30%" />
3235
+ </header>
3236
+ <div class="p-5">
3237
+ <app-skeleton [lines]="6" [height]="44" />
3238
+ </div>
3239
+ </section>
3240
+ }
3241
+ </div>
3242
+ }
3243
+ @case ('error') {
3244
+ <app-empty-state
3245
+ title="No se pudo cargar el registro"
3246
+ message="Revisa tu conexión e intenta de nuevo."
3247
+ >
3248
+ <button app-button variant="outline" type="button" (click)="retry()">
3249
+ <svg lucideRotateCw size="16" />
3250
+ Reintentar
3251
+ </button>
3252
+ <button app-button variant="outline" type="button" (click)="volverDeError()">
3253
+ <svg lucideArrowLeft size="16" />
3254
+ Volver
3255
+ </button>
3256
+ </app-empty-state>
3257
+ }
3258
+ @case ('ready') {
3259
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
3260
+ @for (section of sections(); track section.id) {
3261
+ <section
3262
+ class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden flex flex-col"
3263
+ [class.lg:col-span-2]="hasLargeContent(section)"
3264
+ >
3265
+ <header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
3266
+ <h2 class="text-sm font-semibold tracking-wide text-neutral-light uppercase">
3267
+ {{ section.label }}
3268
+ </h2>
3269
+ </header>
3270
+
3271
+ <div class="p-5 flex-1 space-y-4">
3272
+ <!-- Grid para campos regulares -->
3273
+ @if (regularFields(section).length > 0) {
3274
+ <dl class="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
3275
+ @for (campo of regularFields(section); track campo.key) {
3276
+ <div class="space-y-1">
3277
+ <dt class="text-xs font-medium text-neutral">{{ campo.label }}</dt>
3278
+ <dd class="text-sm font-medium text-neutral-light break-words">
3279
+ <app-render-host
3280
+ mode="display"
3281
+ [type]="campo.field.type"
3282
+ [config]="campo.field"
3283
+ [value]="campo.raw"
3284
+ />
3285
+ </dd>
3286
+ </div>
3287
+ }
3288
+ </dl>
3289
+ }
3290
+
3291
+ <!-- Renderizado de listas anidadas (Timeline de eventos / items) -->
3292
+ @for (campo of listFields(section); track campo.key) {
3293
+ <div class="pt-2">
3294
+ <h3 class="mb-3 text-xs font-semibold text-neutral uppercase">
3295
+ {{ campo.label }}
3296
+ </h3>
3297
+ <app-render-host
3298
+ mode="display"
3299
+ [type]="campo.field.type"
3300
+ [config]="campo.field"
3301
+ [value]="campo.raw"
3302
+ />
3303
+ </div>
3304
+ }
3305
+ </div>
3306
+ </section>
3307
+ }
3308
+ </div>
3309
+ }
3310
+ }
3311
+ </div>
3312
+ `
3313
+ }]
3314
+ }],
3315
+ ctorParameters: () => [],
3316
+ propDecorators: {
3317
+ schema: [{
3318
+ type: i0.Input,
3319
+ args: [{
3320
+ isSignal: true,
3321
+ alias: "schema",
3322
+ required: true
3323
+ }]
3324
+ }],
3325
+ record: [{
3326
+ type: i0.Input,
3327
+ args: [{
3328
+ isSignal: true,
3329
+ alias: "record",
3330
+ required: true
3331
+ }]
3332
+ }],
3333
+ edit: [{
3334
+ type: i0.Output,
3335
+ args: ["edit"]
3336
+ }],
3337
+ back: [{
3338
+ type: i0.Output,
3339
+ args: ["back"]
3340
+ }],
3341
+ delete: [{
3342
+ type: i0.Output,
3343
+ args: ["delete"]
3344
+ }]
3345
+ }
3346
+ });
3347
+ var resource_page_exports = /* @__PURE__ */ __exportAll({ ResourcePage: () => ResourcePage });
3348
+ var ResourcePage = class ResourcePage {
3349
+ route = inject(ActivatedRoute);
3350
+ router = inject(Router);
3351
+ auth = inject(AuthService);
3352
+ params = toSignal(this.route.paramMap, { requireSync: true });
3353
+ routeData = toSignal(this.route.data ?? of({}), { requireSync: true });
3354
+ schema = computed(() => {
3355
+ const resourceId = this.routeData()["resourceId"] ?? this.params().get("id");
3356
+ return getSchema(typeof resourceId === "string" ? resourceId : "");
3357
+ }, ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
3358
+ currentView = signal("list", ...ngDevMode ? [{ debugName: "currentView" }] : /* istanbul ignore next */ []);
3359
+ selectedRecord = signal(null, ...ngDevMode ? [{ debugName: "selectedRecord" }] : /* istanbul ignore next */ []);
3360
+ constructor() {
3361
+ this.route.paramMap.pipe(map((params) => params.get("key")), distinctUntilChanged(), takeUntilDestroyed()).subscribe((key) => this.openFromRoute(key));
3362
+ this.route.data?.pipe(takeUntilDestroyed()).subscribe(() => this.openFromRoute(this.params().get("key")));
3363
+ }
3364
+ openFromRoute(key) {
3365
+ const schema = this.schema();
3366
+ if (!schema || schema.kind !== "collection") return;
3367
+ if ((this.routeData()["viewType"] === "detail" || key) && key && schema.detail?.enabled) {
3368
+ const current = this.selectedRecord();
3369
+ if (!current || String(current[schema.keyField]) !== key) this.selectedRecord.set({ [schema.keyField]: key });
3370
+ this.currentView.set("detail");
3371
+ } else if (this.currentView() === "detail") {
3372
+ this.selectedRecord.set(null);
3373
+ this.currentView.set("list");
3374
+ }
3375
+ }
3376
+ switchToDetail(record) {
3377
+ const schema = this.schema();
3378
+ const data = this.routeData();
3379
+ const binding = data["select"];
3380
+ if (binding !== void 0) {
3381
+ const key = schema && record[schema.keyField] !== void 0 && record[schema.keyField] !== null ? String(record[schema.keyField]) : "";
3382
+ if (binding.mode === "route") {
3383
+ const emit = data["emit"];
3384
+ emit?.({
3385
+ resource: schema?.id ?? String(data["resourceId"] ?? ""),
3386
+ key,
3387
+ action: binding.on
3388
+ });
3389
+ return;
3390
+ }
3391
+ if (binding.mode === "none") return;
3392
+ this.selectedRecord.set(record);
3393
+ this.currentView.set("detail");
3394
+ return;
3395
+ }
3396
+ this.selectedRecord.set(record);
3397
+ this.currentView.set("detail");
3398
+ const k = schema ? record[schema.keyField] : void 0;
3399
+ if (schema && k !== void 0 && k !== null && String(k) !== "") this.router.navigate([
3400
+ "/",
3401
+ schema.id,
3402
+ String(k)
3403
+ ]);
3404
+ }
3405
+ switchToList() {
3406
+ this.selectedRecord.set(null);
3407
+ this.currentView.set("list");
3408
+ const schema = this.schema();
3409
+ if (schema && this.params().get("key")) this.router.navigate(["/", schema.id]);
3410
+ }
3411
+ goEdit(record) {
3412
+ const schema = this.schema();
3413
+ if (!schema) return;
3414
+ const key = record[schema.keyField];
3415
+ if (key) this.router.navigate([
3416
+ "/",
3417
+ schema.id,
3418
+ key,
3419
+ "edit"
3420
+ ]);
3421
+ }
3422
+ hasNoAccess(schema) {
3423
+ if (schema.policies?.access) {
3424
+ if (schema.kind === "singleton") return !this.auth.canDoOperation(schema, "update");
3425
+ return !this.auth.canDoOperation(schema, "list") && !this.auth.canDoOperation(schema, "create") && !this.auth.canDoOperation(schema, "update") && !this.auth.canDoOperation(schema, "remove");
3426
+ }
3427
+ const permissions = schema.permissions;
3428
+ if (!permissions || schema.kind === "dashboard") return false;
3429
+ if (schema.kind === "singleton") return permissions.update === false;
3430
+ return permissions.create === false && permissions.update === false && permissions.remove === false;
3431
+ }
3432
+ static ɵfac = i0.ɵɵngDeclareFactory({
3433
+ minVersion: "12.0.0",
3434
+ version: "22.2.3",
3435
+ ngImport: i0,
3436
+ type: ResourcePage,
3437
+ deps: [],
3438
+ target: i0.ɵɵFactoryTarget.Component
3439
+ });
3440
+ static ɵcmp = i0.ɵɵngDeclareComponent({
3441
+ minVersion: "17.0.0",
3442
+ version: "22.2.3",
3443
+ type: ResourcePage,
3444
+ isStandalone: true,
3445
+ selector: "app-resource-page",
3446
+ ngImport: i0,
3447
+ template: `
3448
+ @if (schema(); as schema) {
3449
+ @if (hasNoAccess(schema)) {
3450
+ <app-empty-state
3451
+ title="Sin permisos"
3452
+ message="No tiene permisos para gestionar este recurso."
3453
+ />
3454
+ } @else {
3455
+ @switch (schema.kind) {
3456
+ @case ('collection') {
3457
+ @switch (currentView()) {
3458
+ @case ('detail') {
3459
+ @if (selectedRecord(); as rec) {
3460
+ <app-detail-view
3461
+ [schema]="schema"
3462
+ [record]="rec"
3463
+ (back)="switchToList()"
3464
+ (edit)="goEdit($event)"
3465
+ (delete)="switchToList()"
3466
+ />
3467
+ }
3468
+ }
3469
+ @default {
3470
+ <header class="mb-6">
3471
+ <h1 class="font-display text-2xl font-semibold">
3472
+ {{ schema.labelPlural }}
3473
+ </h1>
3474
+ <p class="text-sm text-neutral">Colección · {{ schema.fields.length }} campos</p>
3475
+ </header>
3476
+ <app-list-view [schema]="schema" (viewRecord)="switchToDetail($event)" />
3477
+ }
3478
+ }
3479
+ }
3480
+ @case ('dashboard') {
3481
+ <app-dashboard [schema]="schema" />
3482
+ }
3483
+ @default {
3484
+ <!-- singleton: formulario directo -->
3485
+ <app-form-view />
3486
+ }
3487
+ }
3488
+ }
3489
+ } @else {
3490
+ <app-empty-state
3491
+ title="Recurso no encontrado"
3492
+ message="No existe un schema con ese id en el registry."
3493
+ />
3494
+ }
3495
+ `,
3496
+ isInline: true,
3497
+ dependencies: [
3498
+ {
3499
+ kind: "component",
3500
+ type: Dashboard,
3501
+ selector: "app-dashboard",
3502
+ inputs: ["schema", "data"]
3503
+ },
3504
+ {
3505
+ kind: "component",
3506
+ type: DetailView,
3507
+ selector: "app-detail-view",
3508
+ inputs: ["schema", "record"],
3509
+ outputs: [
3510
+ "edit",
3511
+ "back",
3512
+ "delete"
3513
+ ]
3514
+ },
3515
+ {
3516
+ kind: "component",
3517
+ type: EmptyState,
3518
+ selector: "app-empty-state",
3519
+ inputs: ["title", "message"]
3520
+ },
3521
+ {
3522
+ kind: "component",
3523
+ type: FormView,
3524
+ selector: "app-form-view"
3525
+ },
3526
+ {
3527
+ kind: "component",
3528
+ type: ListView,
3529
+ selector: "app-list-view",
3530
+ inputs: ["schema"],
3531
+ outputs: ["viewRecord"]
3532
+ }
3533
+ ]
3534
+ });
3535
+ };
3536
+ i0.ɵɵngDeclareClassMetadata({
3537
+ minVersion: "12.0.0",
3538
+ version: "22.2.3",
3539
+ ngImport: i0,
3540
+ type: ResourcePage,
3541
+ decorators: [{
3542
+ type: Component,
3543
+ args: [{
3544
+ selector: "app-resource-page",
3545
+ imports: [
3546
+ Dashboard,
3547
+ DetailView,
3548
+ EmptyState,
3549
+ FormView,
3550
+ ListView
3551
+ ],
3552
+ template: `
3553
+ @if (schema(); as schema) {
3554
+ @if (hasNoAccess(schema)) {
3555
+ <app-empty-state
3556
+ title="Sin permisos"
3557
+ message="No tiene permisos para gestionar este recurso."
3558
+ />
3559
+ } @else {
3560
+ @switch (schema.kind) {
3561
+ @case ('collection') {
3562
+ @switch (currentView()) {
3563
+ @case ('detail') {
3564
+ @if (selectedRecord(); as rec) {
3565
+ <app-detail-view
3566
+ [schema]="schema"
3567
+ [record]="rec"
3568
+ (back)="switchToList()"
3569
+ (edit)="goEdit($event)"
3570
+ (delete)="switchToList()"
3571
+ />
3572
+ }
3573
+ }
3574
+ @default {
3575
+ <header class="mb-6">
3576
+ <h1 class="font-display text-2xl font-semibold">
3577
+ {{ schema.labelPlural }}
3578
+ </h1>
3579
+ <p class="text-sm text-neutral">Colección · {{ schema.fields.length }} campos</p>
3580
+ </header>
3581
+ <app-list-view [schema]="schema" (viewRecord)="switchToDetail($event)" />
3582
+ }
3583
+ }
3584
+ }
3585
+ @case ('dashboard') {
3586
+ <app-dashboard [schema]="schema" />
3587
+ }
3588
+ @default {
3589
+ <!-- singleton: formulario directo -->
3590
+ <app-form-view />
3591
+ }
3592
+ }
3593
+ }
3594
+ } @else {
3595
+ <app-empty-state
3596
+ title="Recurso no encontrado"
3597
+ message="No existe un schema con ese id en el registry."
3598
+ />
3599
+ }
3600
+ `
3601
+ }]
3602
+ }],
3603
+ ctorParameters: () => []
3604
+ });
3605
+ var ViewSlot = class ViewSlot {
3606
+ schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
3607
+ node = input.required(...ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []);
3608
+ parentInstanceId = input.required(...ngDevMode ? [{ debugName: "parentInstanceId" }] : /* istanbul ignore next */ []);
3609
+ depth = input(0, ...ngDevMode ? [{ debugName: "depth" }] : /* istanbul ignore next */ []);
3610
+ status = signal("loading", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
3611
+ error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
3612
+ payload = signal(null, ...ngDevMode ? [{ debugName: "payload" }] : /* istanbul ignore next */ []);
3613
+ nav = inject(ViewNavigationService);
3614
+ envInjector = inject(EnvironmentInjector);
3615
+ ancla = viewChild("host", {
3616
+ ...ngDevMode ? { debugName: "ancla" } : /* istanbul ignore next */ {},
3617
+ read: ViewContainerRef
3618
+ });
3619
+ epoch = 0;
3620
+ pendiente = null;
3621
+ constructor() {
3622
+ effect(() => this.monar());
3623
+ effect(() => {
3624
+ if (this.status() !== "ready") return;
3625
+ const pend = this.pendiente;
3626
+ let ancla;
3627
+ try {
3628
+ ancla = this.ancla();
3629
+ } catch {
3630
+ ancla = void 0;
3631
+ }
3632
+ if (!pend || !ancla) return;
3633
+ this.pendiente = null;
3634
+ ancla.clear();
3635
+ ancla.createComponent(pend.tipo, { injector: pend.injector });
3636
+ });
3637
+ }
3638
+ async monar() {
3639
+ const esquema = this.schema();
3640
+ const nodo = this.node();
3641
+ if (!esquema || !nodo) return;
3642
+ const turno = ++this.epoch;
3643
+ this.pendiente = null;
3644
+ if (this.depth() > 3) {
3645
+ this.error.set(/* @__PURE__ */ new Error(`Límite de profundidad de composición excedido (3) en ${esquema.id}.${nodo.id}`));
3646
+ this.status.set("error");
3647
+ return;
3648
+ }
3649
+ const resuelto = "ref" in nodo ? esquema.uiViews?.[nodo.ref] : nodo;
3650
+ if (resuelto === void 0) {
3651
+ const ref = "ref" in nodo ? nodo.ref : nodo.id;
3652
+ this.error.set(/* @__PURE__ */ new Error(`Referencia '${ref}' inexistente en ${esquema.id}`));
3653
+ this.status.set("error");
3654
+ return;
3655
+ }
3656
+ const descriptor = get("view", resuelto.type);
3657
+ if (!descriptor) {
3658
+ this.error.set(/* @__PURE__ */ new Error(`No existe descriptor kind:view type '${resuelto.type}' en ${esquema.id}`));
3659
+ this.status.set("error");
3660
+ return;
3661
+ }
3662
+ const instanceId = instanceIdFor(this.parentInstanceId(), nodo.id);
3663
+ const select = "ref" in nodo ? resuelto.select : nodo.select;
3664
+ const virtual = virtualSchemaFor(esquema, instanceId, resuelto);
3665
+ const emit = select?.mode === "route" ? (event) => {
3666
+ this.nav.dispatch(select, event);
3667
+ } : void 0;
3668
+ this.payload.set(createViewContext({
3669
+ schema: virtual,
3670
+ resourceId: esquema.id,
3671
+ uiView: instanceId,
3672
+ viewType: resuelto.type,
3673
+ depth: this.depth(),
3674
+ ...select !== void 0 ? { select } : {}
3675
+ }, emit));
3676
+ this.error.set(null);
3677
+ this.status.set("loading");
3678
+ const injector = Injector.create({
3679
+ providers: [{
3680
+ provide: ActivatedRoute,
3681
+ useValue: syntheticRoute(this.payload())
3682
+ }],
3683
+ parent: this.envInjector
3684
+ });
3685
+ try {
3686
+ const tipo = await descriptor.load();
3687
+ if (turno !== this.epoch) return;
3688
+ this.pendiente = {
3689
+ tipo,
3690
+ injector
3691
+ };
3692
+ this.status.set("ready");
3693
+ } catch (e) {
3694
+ if (turno !== this.epoch) return;
3695
+ this.error.set(e);
3696
+ this.status.set("error");
3697
+ }
3698
+ }
3699
+ mensaje() {
3700
+ const e = this.error();
3701
+ return e instanceof Error ? e.message : String(e);
3702
+ }
3703
+ static ɵfac = i0.ɵɵngDeclareFactory({
3704
+ minVersion: "12.0.0",
3705
+ version: "22.2.3",
3706
+ ngImport: i0,
3707
+ type: ViewSlot,
3708
+ deps: [],
3709
+ target: i0.ɵɵFactoryTarget.Component
3710
+ });
3711
+ static ɵcmp = i0.ɵɵngDeclareComponent({
3712
+ minVersion: "17.0.0",
3713
+ version: "22.2.3",
3714
+ type: ViewSlot,
3715
+ isStandalone: true,
3716
+ selector: "app-view-slot",
3717
+ inputs: {
3718
+ schema: {
3719
+ classPropertyName: "schema",
3720
+ publicName: "schema",
3721
+ isSignal: true,
3722
+ isRequired: true,
3723
+ transformFunction: null
3724
+ },
3725
+ node: {
3726
+ classPropertyName: "node",
3727
+ publicName: "node",
3728
+ isSignal: true,
3729
+ isRequired: true,
3730
+ transformFunction: null
3731
+ },
3732
+ parentInstanceId: {
3733
+ classPropertyName: "parentInstanceId",
3734
+ publicName: "parentInstanceId",
3735
+ isSignal: true,
3736
+ isRequired: true,
3737
+ transformFunction: null
3738
+ },
3739
+ depth: {
3740
+ classPropertyName: "depth",
3741
+ publicName: "depth",
3742
+ isSignal: true,
3743
+ isRequired: false,
3744
+ transformFunction: null
3745
+ }
3746
+ },
3747
+ viewQueries: [{
3748
+ propertyName: "ancla",
3749
+ first: true,
3750
+ predicate: ["host"],
3751
+ descendants: true,
3752
+ read: ViewContainerRef,
3753
+ isSignal: true
3754
+ }],
3755
+ ngImport: i0,
3756
+ template: `
3757
+ @if (status() === 'loading') {
3758
+ <div role="status" class="p-3 text-sm text-neutral">Cargando…</div>
3759
+ } @else if (status() === 'error') {
3760
+ <app-empty-state title="No se pudo mostrar esta vista" [message]="mensaje()" />
3761
+ } @else {
3762
+ <div #host></div>
3763
+ }
3764
+ `,
3765
+ isInline: true,
3766
+ dependencies: [{
3767
+ kind: "component",
3768
+ type: EmptyState,
3769
+ selector: "app-empty-state",
3770
+ inputs: ["title", "message"]
3771
+ }],
3772
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
3773
+ });
3774
+ };
3775
+ i0.ɵɵngDeclareClassMetadata({
3776
+ minVersion: "12.0.0",
3777
+ version: "22.2.3",
3778
+ ngImport: i0,
3779
+ type: ViewSlot,
3780
+ decorators: [{
3781
+ type: Component,
3782
+ args: [{
3783
+ selector: "app-view-slot",
3784
+ standalone: true,
3785
+ imports: [EmptyState],
3786
+ changeDetection: ChangeDetectionStrategy.OnPush,
3787
+ template: `
3788
+ @if (status() === 'loading') {
3789
+ <div role="status" class="p-3 text-sm text-neutral">Cargando…</div>
3790
+ } @else if (status() === 'error') {
3791
+ <app-empty-state title="No se pudo mostrar esta vista" [message]="mensaje()" />
3792
+ } @else {
3793
+ <div #host></div>
3794
+ }
3795
+ `
3796
+ }]
3797
+ }],
3798
+ ctorParameters: () => [],
3799
+ propDecorators: {
3800
+ schema: [{
3801
+ type: i0.Input,
3802
+ args: [{
3803
+ isSignal: true,
3804
+ alias: "schema",
3805
+ required: true
3806
+ }]
3807
+ }],
3808
+ node: [{
3809
+ type: i0.Input,
3810
+ args: [{
3811
+ isSignal: true,
3812
+ alias: "node",
3813
+ required: true
3814
+ }]
3815
+ }],
3816
+ parentInstanceId: [{
3817
+ type: i0.Input,
3818
+ args: [{
3819
+ isSignal: true,
3820
+ alias: "parentInstanceId",
3821
+ required: true
3822
+ }]
3823
+ }],
3824
+ depth: [{
3825
+ type: i0.Input,
3826
+ args: [{
3827
+ isSignal: true,
3828
+ alias: "depth",
3829
+ required: false
3830
+ }]
3831
+ }],
3832
+ ancla: [{
3833
+ type: i0.ViewChild,
3834
+ args: ["host", {
3835
+ read: ViewContainerRef,
3836
+ isSignal: true
3837
+ }]
3838
+ }]
3839
+ }
3840
+ });
3841
+ var composer_host_exports = /* @__PURE__ */ __exportAll({ ComposerHost: () => ComposerHost });
3842
+ var ComposerHost = class ComposerHost {
3843
+ route = inject(ActivatedRoute);
3844
+ routeData = toSignal(this.route.data, { requireSync: true });
3845
+ schema = computed(() => {
3846
+ const data = this.routeData();
3847
+ const explicit = data["schema"];
3848
+ if (explicit !== void 0) return explicit;
3849
+ const resourceId = data["resourceId"];
3850
+ return typeof resourceId === "string" ? getSchema(resourceId) : void 0;
3851
+ }, ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
3852
+ uiViewName = computed(() => {
3853
+ const v = this.routeData()["uiView"];
3854
+ return typeof v === "string" ? v : "";
3855
+ }, ...ngDevMode ? [{ debugName: "uiViewName" }] : /* istanbul ignore next */ []);
3856
+ profundidad = computed(() => {
3857
+ const d = this.routeData()["depth"];
3858
+ return typeof d === "number" ? d : 0;
3859
+ }, ...ngDevMode ? [{ debugName: "profundidad" }] : /* istanbul ignore next */ []);
3860
+ vista = computed(() => {
3861
+ const schema = this.schema();
3862
+ const nombre = this.uiViewName();
3863
+ if (schema === void 0 || nombre === "") return void 0;
3864
+ const vista = schema.uiViews?.[nombre];
3865
+ return vista !== void 0 && "config" in vista && vista.type === "tabs" ? vista : void 0;
3866
+ }, ...ngDevMode ? [{ debugName: "vista" }] : /* istanbul ignore next */ []);
3867
+ errorConfig = computed(() => {
3868
+ const data = this.routeData();
3869
+ const schema = this.schema();
3870
+ const nombre = this.uiViewName();
3871
+ if (schema === void 0) return `Schema no encontrado para '${String(data["resourceId"] ?? "")}'`;
3872
+ if (nombre === "") return "Ruta de composición sin uiView declarada";
3873
+ const vista = schema.uiViews?.[nombre];
3874
+ if (vista === void 0) return `Vista '${nombre}' no declarada en ${schema.id}`;
3875
+ if (vista.type !== "tabs") return `Configuración inválida: ${schema.id}.${nombre} declara '${vista.type}', no es una vista tabs`;
3876
+ return null;
3877
+ }, ...ngDevMode ? [{ debugName: "errorConfig" }] : /* istanbul ignore next */ []);
3878
+ errorValidacion = signal(null, ...ngDevMode ? [{ debugName: "errorValidacion" }] : /* istanbul ignore next */ []);
3879
+ error = computed(() => this.errorValidacion() ?? this.errorConfig(), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
3880
+ hijos = computed(() => this.vista()?.config.children ?? [], ...ngDevMode ? [{ debugName: "hijos" }] : /* istanbul ignore next */ []);
3881
+ visibles = computed(() => this.hijos().filter((h) => h.visibleWhen === void 0 || h.visibleWhen.every((cond) => evaluateCondition(cond, {}, {}))), ...ngDevMode ? [{ debugName: "visibles" }] : /* istanbul ignore next */ []);
3882
+ seleccion = signal("", ...ngDevMode ? [{ debugName: "seleccion" }] : /* istanbul ignore next */ []);
3883
+ activo = computed(() => {
3884
+ const vis = this.visibles();
3885
+ if (vis.length === 0) return "";
3886
+ const sel = this.seleccion();
3887
+ if (vis.some((h) => h.id === sel)) return sel;
3888
+ const inicial = this.vista()?.config.initial;
3889
+ return inicial !== void 0 && vis.some((h) => h.id === inicial) ? inicial : vis[0].id;
3890
+ }, ...ngDevMode ? [{ debugName: "activo" }] : /* istanbul ignore next */ []);
3891
+ visitados = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "visitados" }] : /* istanbul ignore next */ []);
3892
+ montados = computed(() => {
3893
+ const visibles = new Set(this.visibles().map((h) => h.id));
3894
+ const activo = this.activo();
3895
+ const visitados = this.visitados();
3896
+ return this.hijos().filter((h) => visibles.has(h.id) && (h.id === activo || h.retain === true && visitados.has(h.id)));
3897
+ }, ...ngDevMode ? [{ debugName: "montados" }] : /* istanbul ignore next */ []);
3898
+ constructor() {
3899
+ const schema = this.schema();
3900
+ if (schema !== void 0) try {
3901
+ validateComposition(schema);
3902
+ } catch (e) {
3903
+ this.errorValidacion.set(e instanceof Error ? e.message : String(e));
3904
+ }
3905
+ const inicial = this.activo();
3906
+ if (inicial !== "") this.visitados.set(/* @__PURE__ */ new Set([inicial]));
3907
+ }
3908
+ seleccionar(id, conFoco) {
3909
+ this.seleccion.set(id);
3910
+ this.visitados.update((v) => new Set(v).add(id));
3911
+ if (conFoco) setTimeout(() => document.getElementById(`tab-${id}`)?.focus(), 0);
3912
+ }
3913
+ onKeydown(event) {
3914
+ const vis = this.visibles();
3915
+ if (vis.length === 0) return;
3916
+ const i = vis.findIndex((h) => h.id === this.activo());
3917
+ let next = -1;
3918
+ if (event.key === "ArrowRight") next = (i + 1) % vis.length;
3919
+ else if (event.key === "ArrowLeft") next = (i - 1 + vis.length) % vis.length;
3920
+ else if (event.key === "Home") next = 0;
3921
+ else if (event.key === "End") next = vis.length - 1;
3922
+ if (next < 0) return;
3923
+ event.preventDefault();
3924
+ this.seleccionar(vis[next].id, true);
3925
+ }
3926
+ montado(id) {
3927
+ return this.montados().some((h) => h.id === id);
3928
+ }
3929
+ claseTab(id) {
3930
+ const base = "rounded-t-lg px-3 py-2 text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary";
3931
+ return id === this.activo() ? `${base} bg-primary/10 font-medium text-primary` : `${base} text-neutral hover:text-primary`;
3932
+ }
3933
+ etiqueta() {
3934
+ return this.schema()?.labelPlural ?? "Secciones";
3935
+ }
3936
+ static ɵfac = i0.ɵɵngDeclareFactory({
3937
+ minVersion: "12.0.0",
3938
+ version: "22.2.3",
3939
+ ngImport: i0,
3940
+ type: ComposerHost,
3941
+ deps: [],
3942
+ target: i0.ɵɵFactoryTarget.Component
3943
+ });
3944
+ static ɵcmp = i0.ɵɵngDeclareComponent({
3945
+ minVersion: "17.0.0",
3946
+ version: "22.2.3",
3947
+ type: ComposerHost,
3948
+ isStandalone: true,
3949
+ selector: "app-composer-host",
3950
+ ngImport: i0,
3951
+ template: `
3952
+ @if (error() !== null) {
3953
+ <app-empty-state title="Composición no disponible" [message]="error()!" />
3954
+ } @else if (visibles().length === 0) {
3955
+ <app-empty-state
3956
+ title="Sin secciones disponibles"
3957
+ message="La vista no declara secciones visibles."
3958
+ />
3959
+ } @else {
3960
+ <div
3961
+ role="tablist"
3962
+ class="flex flex-wrap gap-1 border-b border-neutral/20 px-1"
3963
+ [attr.aria-label]="etiqueta()"
3964
+ (keydown)="onKeydown($event)"
3965
+ >
3966
+ @for (h of visibles(); track h.id) {
3967
+ <button
3968
+ type="button"
3969
+ role="tab"
3970
+ [id]="'tab-' + h.id"
3971
+ [attr.aria-selected]="h.id === activo()"
3972
+ [attr.aria-controls]="montado(h.id) ? 'panel-' + h.id : null"
3973
+ [attr.tabindex]="h.id === activo() ? 0 : -1"
3974
+ [class]="claseTab(h.id)"
3975
+ (click)="seleccionar(h.id, true)"
3976
+ >
3977
+ {{ h.label }}
3978
+ </button>
3979
+ }
3980
+ </div>
3981
+
3982
+ @for (h of montados(); track h.id) {
3983
+ <div
3984
+ role="tabpanel"
3985
+ [id]="'panel-' + h.id"
3986
+ [attr.aria-labelledby]="'tab-' + h.id"
3987
+ [class.hidden]="h.id !== activo()"
3988
+ class="py-3 focus:outline-none"
3989
+ tabindex="0"
3990
+ >
3991
+ <app-view-slot
3992
+ [schema]="schema()!"
3993
+ [node]="h"
3994
+ [parentInstanceId]="uiViewName()"
3995
+ [depth]="profundidad() + 1"
3996
+ />
3997
+ </div>
3998
+ }
3999
+ }
4000
+ `,
4001
+ isInline: true,
4002
+ dependencies: [{
4003
+ kind: "component",
4004
+ type: ViewSlot,
4005
+ selector: "app-view-slot",
4006
+ inputs: [
4007
+ "schema",
4008
+ "node",
4009
+ "parentInstanceId",
4010
+ "depth"
4011
+ ]
4012
+ }, {
4013
+ kind: "component",
4014
+ type: EmptyState,
4015
+ selector: "app-empty-state",
4016
+ inputs: ["title", "message"]
4017
+ }],
4018
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
4019
+ });
4020
+ };
4021
+ i0.ɵɵngDeclareClassMetadata({
4022
+ minVersion: "12.0.0",
4023
+ version: "22.2.3",
4024
+ ngImport: i0,
4025
+ type: ComposerHost,
4026
+ decorators: [{
4027
+ type: Component,
4028
+ args: [{
4029
+ selector: "app-composer-host",
4030
+ standalone: true,
4031
+ imports: [ViewSlot, EmptyState],
4032
+ changeDetection: ChangeDetectionStrategy.OnPush,
4033
+ template: `
4034
+ @if (error() !== null) {
4035
+ <app-empty-state title="Composición no disponible" [message]="error()!" />
4036
+ } @else if (visibles().length === 0) {
4037
+ <app-empty-state
4038
+ title="Sin secciones disponibles"
4039
+ message="La vista no declara secciones visibles."
4040
+ />
4041
+ } @else {
4042
+ <div
4043
+ role="tablist"
4044
+ class="flex flex-wrap gap-1 border-b border-neutral/20 px-1"
4045
+ [attr.aria-label]="etiqueta()"
4046
+ (keydown)="onKeydown($event)"
4047
+ >
4048
+ @for (h of visibles(); track h.id) {
4049
+ <button
4050
+ type="button"
4051
+ role="tab"
4052
+ [id]="'tab-' + h.id"
4053
+ [attr.aria-selected]="h.id === activo()"
4054
+ [attr.aria-controls]="montado(h.id) ? 'panel-' + h.id : null"
4055
+ [attr.tabindex]="h.id === activo() ? 0 : -1"
4056
+ [class]="claseTab(h.id)"
4057
+ (click)="seleccionar(h.id, true)"
4058
+ >
4059
+ {{ h.label }}
4060
+ </button>
4061
+ }
4062
+ </div>
4063
+
4064
+ @for (h of montados(); track h.id) {
4065
+ <div
4066
+ role="tabpanel"
4067
+ [id]="'panel-' + h.id"
4068
+ [attr.aria-labelledby]="'tab-' + h.id"
4069
+ [class.hidden]="h.id !== activo()"
4070
+ class="py-3 focus:outline-none"
4071
+ tabindex="0"
4072
+ >
4073
+ <app-view-slot
4074
+ [schema]="schema()!"
4075
+ [node]="h"
4076
+ [parentInstanceId]="uiViewName()"
4077
+ [depth]="profundidad() + 1"
4078
+ />
4079
+ </div>
4080
+ }
4081
+ }
4082
+ `
4083
+ }]
4084
+ }],
4085
+ ctorParameters: () => []
4086
+ });
4087
+ var ConfirmDialog = class ConfirmDialog {
4088
+ options = input(null, ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
4089
+ danger = input(false, ...ngDevMode ? [{ debugName: "danger" }] : /* istanbul ignore next */ []);
4090
+ confirmed = output();
4091
+ cancelled = output();
4092
+ static ɵfac = i0.ɵɵngDeclareFactory({
4093
+ minVersion: "12.0.0",
4094
+ version: "22.2.3",
4095
+ ngImport: i0,
4096
+ type: ConfirmDialog,
4097
+ deps: [],
4098
+ target: i0.ɵɵFactoryTarget.Component
4099
+ });
4100
+ static ɵcmp = i0.ɵɵngDeclareComponent({
4101
+ minVersion: "17.0.0",
4102
+ version: "22.2.3",
4103
+ type: ConfirmDialog,
4104
+ isStandalone: true,
4105
+ selector: "app-confirm-dialog",
4106
+ inputs: {
4107
+ options: {
4108
+ classPropertyName: "options",
4109
+ publicName: "options",
4110
+ isSignal: true,
4111
+ isRequired: false,
4112
+ transformFunction: null
4113
+ },
4114
+ danger: {
4115
+ classPropertyName: "danger",
4116
+ publicName: "danger",
4117
+ isSignal: true,
4118
+ isRequired: false,
4119
+ transformFunction: null
4120
+ }
4121
+ },
4122
+ outputs: {
4123
+ confirmed: "confirmed",
4124
+ cancelled: "cancelled"
4125
+ },
4126
+ ngImport: i0,
4127
+ template: `
4128
+ @if (options(); as opts) {
4129
+ <app-modal [open]="true" [title]="opts.title" (close)="cancelled.emit()">
4130
+ <p class="text-sm text-neutral">{{ opts.message }}</p>
4131
+ <div class="mt-6 flex justify-end gap-3">
4132
+ <button app-button variant="outline" type="button" (click)="cancelled.emit()">
4133
+ {{ opts.cancelLabel ?? 'Cancel' }}
4134
+ </button>
4135
+ <button
4136
+ app-button
4137
+ type="button"
4138
+ [variant]="danger() ? 'danger' : 'primary'"
4139
+ (click)="confirmed.emit()"
4140
+ >
4141
+ {{ opts.confirmLabel ?? 'Confirm' }}
4142
+ </button>
4143
+ </div>
4144
+ </app-modal>
4145
+ }
4146
+ `,
4147
+ isInline: true,
4148
+ dependencies: [{
4149
+ kind: "component",
4150
+ type: Modal,
4151
+ selector: "app-modal",
4152
+ inputs: [
4153
+ "open",
4154
+ "title",
4155
+ "closeLabel"
4156
+ ],
4157
+ outputs: ["close"]
4158
+ }, {
4159
+ kind: "component",
4160
+ type: Button,
4161
+ selector: "button[app-button]",
4162
+ inputs: [
4163
+ "variant",
4164
+ "size",
4165
+ "type",
4166
+ "disabled",
4167
+ "loading"
4168
+ ]
4169
+ }]
4170
+ });
4171
+ };
4172
+ i0.ɵɵngDeclareClassMetadata({
4173
+ minVersion: "12.0.0",
4174
+ version: "22.2.3",
4175
+ ngImport: i0,
4176
+ type: ConfirmDialog,
4177
+ decorators: [{
4178
+ type: Component,
4179
+ args: [{
4180
+ selector: "app-confirm-dialog",
4181
+ imports: [Modal, Button],
4182
+ template: `
4183
+ @if (options(); as opts) {
4184
+ <app-modal [open]="true" [title]="opts.title" (close)="cancelled.emit()">
4185
+ <p class="text-sm text-neutral">{{ opts.message }}</p>
4186
+ <div class="mt-6 flex justify-end gap-3">
4187
+ <button app-button variant="outline" type="button" (click)="cancelled.emit()">
4188
+ {{ opts.cancelLabel ?? 'Cancel' }}
4189
+ </button>
4190
+ <button
4191
+ app-button
4192
+ type="button"
4193
+ [variant]="danger() ? 'danger' : 'primary'"
4194
+ (click)="confirmed.emit()"
4195
+ >
4196
+ {{ opts.confirmLabel ?? 'Confirm' }}
4197
+ </button>
4198
+ </div>
4199
+ </app-modal>
4200
+ }
4201
+ `
4202
+ }]
4203
+ }],
4204
+ propDecorators: {
4205
+ options: [{
4206
+ type: i0.Input,
4207
+ args: [{
4208
+ isSignal: true,
4209
+ alias: "options",
4210
+ required: false
4211
+ }]
4212
+ }],
4213
+ danger: [{
4214
+ type: i0.Input,
4215
+ args: [{
4216
+ isSignal: true,
4217
+ alias: "danger",
4218
+ required: false
4219
+ }]
4220
+ }],
4221
+ confirmed: [{
4222
+ type: i0.Output,
4223
+ args: ["confirmed"]
4224
+ }],
4225
+ cancelled: [{
4226
+ type: i0.Output,
4227
+ args: ["cancelled"]
4228
+ }]
4229
+ }
4230
+ });
4231
+ var Toast = class Toast {
4232
+ toasts = input([], ...ngDevMode ? [{ debugName: "toasts" }] : /* istanbul ignore next */ []);
4233
+ closeLabel = input("Cerrar", ...ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []);
4234
+ dismissed = output();
4235
+ static ɵfac = i0.ɵɵngDeclareFactory({
4236
+ minVersion: "12.0.0",
4237
+ version: "22.2.3",
4238
+ ngImport: i0,
4239
+ type: Toast,
4240
+ deps: [],
4241
+ target: i0.ɵɵFactoryTarget.Component
4242
+ });
4243
+ static ɵcmp = i0.ɵɵngDeclareComponent({
4244
+ minVersion: "17.0.0",
4245
+ version: "22.2.3",
4246
+ type: Toast,
4247
+ isStandalone: true,
4248
+ selector: "app-toast",
4249
+ inputs: {
4250
+ toasts: {
4251
+ classPropertyName: "toasts",
4252
+ publicName: "toasts",
4253
+ isSignal: true,
4254
+ isRequired: false,
4255
+ transformFunction: null
4256
+ },
4257
+ closeLabel: {
4258
+ classPropertyName: "closeLabel",
4259
+ publicName: "closeLabel",
4260
+ isSignal: true,
4261
+ isRequired: false,
4262
+ transformFunction: null
4263
+ }
4264
+ },
4265
+ outputs: { dismissed: "dismissed" },
4266
+ host: {
4267
+ attributes: {
4268
+ "role": "status",
4269
+ "aria-live": "polite"
4270
+ },
4271
+ classAttribute: "pointer-events-none fixed right-4 top-4 z-50 flex w-full max-w-sm flex-col gap-2"
4272
+ },
4273
+ ngImport: i0,
4274
+ template: `
4275
+ @for (toast of toasts(); track toast.id) {
4276
+ <div
4277
+ class="pointer-events-auto flex items-start gap-3 border-l-4 bg-surface-raised p-4 shadow-lg"
4278
+ [class.border-l-success]="toast.kind === 'success'"
4279
+ [class.border-l-danger]="toast.kind === 'error'"
4280
+ [class.border-l-primary]="toast.kind === 'info'"
4281
+ >
4282
+ @if (toast.kind === 'success') {
4283
+ <svg lucideCircleCheck size="18" class="mt-0.5 shrink-0 text-success" />
4284
+ } @else if (toast.kind === 'error') {
4285
+ <svg lucideCircleX size="18" class="mt-0.5 shrink-0 text-danger" />
4286
+ } @else {
4287
+ <svg lucideInfo size="18" class="mt-0.5 shrink-0 text-primary" />
4288
+ }
4289
+ <p class="flex-1 text-sm font-medium">{{ toast.message }}</p>
4290
+ <button
4291
+ type="button"
4292
+ class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
4293
+ [attr.aria-label]="closeLabel()"
4294
+ (click)="dismissed.emit(toast.id)"
4295
+ >
4296
+ <svg lucideX size="18" />
4297
+ </button>
4298
+ </div>
4299
+ }
4300
+ `,
4301
+ isInline: true,
4302
+ dependencies: [
4303
+ {
4304
+ kind: "component",
4305
+ type: LucideCircleCheck,
4306
+ selector: "svg[lucideCircleCheck], svg[lucideCheckCircle2]"
4307
+ },
4308
+ {
4309
+ kind: "component",
4310
+ type: LucideCircleX,
4311
+ selector: "svg[lucideCircleX], svg[lucideXCircle]"
4312
+ },
4313
+ {
4314
+ kind: "component",
4315
+ type: LucideInfo,
4316
+ selector: "svg[lucideInfo]"
4317
+ },
4318
+ {
4319
+ kind: "component",
4320
+ type: LucideX,
4321
+ selector: "svg[lucideX]"
4322
+ }
4323
+ ]
4324
+ });
4325
+ };
4326
+ i0.ɵɵngDeclareClassMetadata({
4327
+ minVersion: "12.0.0",
4328
+ version: "22.2.3",
4329
+ ngImport: i0,
4330
+ type: Toast,
4331
+ decorators: [{
4332
+ type: Component,
4333
+ args: [{
4334
+ selector: "app-toast",
4335
+ imports: [
4336
+ LucideCircleCheck,
4337
+ LucideCircleX,
4338
+ LucideInfo,
4339
+ LucideX
4340
+ ],
4341
+ host: {
4342
+ class: "pointer-events-none fixed right-4 top-4 z-50 flex w-full max-w-sm flex-col gap-2",
4343
+ role: "status",
4344
+ "aria-live": "polite"
4345
+ },
4346
+ template: `
4347
+ @for (toast of toasts(); track toast.id) {
4348
+ <div
4349
+ class="pointer-events-auto flex items-start gap-3 border-l-4 bg-surface-raised p-4 shadow-lg"
4350
+ [class.border-l-success]="toast.kind === 'success'"
4351
+ [class.border-l-danger]="toast.kind === 'error'"
4352
+ [class.border-l-primary]="toast.kind === 'info'"
4353
+ >
4354
+ @if (toast.kind === 'success') {
4355
+ <svg lucideCircleCheck size="18" class="mt-0.5 shrink-0 text-success" />
4356
+ } @else if (toast.kind === 'error') {
4357
+ <svg lucideCircleX size="18" class="mt-0.5 shrink-0 text-danger" />
4358
+ } @else {
4359
+ <svg lucideInfo size="18" class="mt-0.5 shrink-0 text-primary" />
4360
+ }
4361
+ <p class="flex-1 text-sm font-medium">{{ toast.message }}</p>
4362
+ <button
4363
+ type="button"
4364
+ class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
4365
+ [attr.aria-label]="closeLabel()"
4366
+ (click)="dismissed.emit(toast.id)"
4367
+ >
4368
+ <svg lucideX size="18" />
4369
+ </button>
4370
+ </div>
4371
+ }
4372
+ `
4373
+ }]
4374
+ }],
4375
+ propDecorators: {
4376
+ toasts: [{
4377
+ type: i0.Input,
4378
+ args: [{
4379
+ isSignal: true,
4380
+ alias: "toasts",
4381
+ required: false
4382
+ }]
4383
+ }],
4384
+ closeLabel: [{
4385
+ type: i0.Input,
4386
+ args: [{
4387
+ isSignal: true,
4388
+ alias: "closeLabel",
4389
+ required: false
4390
+ }]
4391
+ }],
4392
+ dismissed: [{
4393
+ type: i0.Output,
4394
+ args: ["dismissed"]
4395
+ }]
4396
+ }
4397
+ });
4398
+ export { API_URL, ApiError, ApiService, AuthService, COLUMN_FORMAT_TYPES, ComposerHost, ConfirmDialog, Dashboard, DataSourceService, ErrorMapperService, FieldHost, FormView, GAS_SCHEMAS, ListView, Login, MAX_DEPTH, NotificationService, ResourcePage, SESSION_TOKEN_KEY, SESSION_USER_KEY, SUPPORTED_FIELD_TYPE_LIST, SchemaCompositionError, Sidebar, Toast, UploadService, actionType, actionTypes, actionsFor, allSchemas, apiErrorInterceptor, authGuard, buildFormSchema, buildPayload, childTree, columnFormatDescriptors, fieldDescriptors, getAction, getAuth, get as getRenderDescriptor, getSchema, groupTree, interpolateTemplate, isSupported as isRenderSupported, isSupported, isShareAction, isTriggerAction, itemTree, layoutDescriptors, listTree, resource_page_exports as n, provideGasAdmin, provideRenderDescriptors, registerAction, register as registerRenderDescriptor, renderKinds, renewedTokenOf, resolveShare, resourceRoutes, seedModel, setSchemaCatalog, shellRoutes, stringItemTree, composer_host_exports as t, unregister as unregisterRenderDescriptor, validateComposition, viewDescriptors };
4399
+
4400
+ //# sourceMappingURL=tinya-code-gas-admin-ui.mjs.map