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

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