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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +63 -2
  2. package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs +281 -0
  3. package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs.map +1 -0
  4. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +84 -0
  5. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +1 -0
  6. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +140 -0
  7. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +1 -0
  8. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +113 -0
  9. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +1 -0
  10. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +163 -0
  11. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +1 -0
  12. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs +1022 -0
  13. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs.map +1 -0
  14. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +278 -0
  15. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +1 -0
  16. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs +101 -0
  17. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs.map +1 -0
  18. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +141 -0
  19. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +1 -0
  20. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs +89 -0
  21. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs.map +1 -0
  22. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +107 -0
  23. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +1 -0
  24. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +140 -0
  25. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +1 -0
  26. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +119 -0
  27. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +1 -0
  28. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +125 -0
  29. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +1 -0
  30. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +152 -0
  31. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +1 -0
  32. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +211 -0
  33. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +1 -0
  34. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +116 -0
  35. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +1 -0
  36. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +111 -0
  37. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +1 -0
  38. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +107 -0
  39. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +1 -0
  40. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +109 -0
  41. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +1 -0
  42. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs +45 -0
  43. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs.map +1 -0
  44. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +50 -0
  45. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +1 -0
  46. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +24 -0
  47. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +1 -0
  48. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +27 -0
  49. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +1 -0
  50. package/fesm2022/tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs +408 -0
  51. package/fesm2022/tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs.map +1 -0
  52. package/fesm2022/tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs +4514 -0
  53. package/fesm2022/tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map +1 -0
  54. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +82 -0
  55. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +1 -0
  56. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +105 -0
  57. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +1 -0
  58. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +65 -0
  59. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +1 -0
  60. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +65 -0
  61. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +1 -0
  62. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +65 -0
  63. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +1 -0
  64. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +59 -0
  65. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +1 -0
  66. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +94 -0
  67. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +1 -0
  68. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +72 -0
  69. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +1 -0
  70. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +238 -0
  71. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +1 -0
  72. package/fesm2022/tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs +416 -0
  73. package/fesm2022/tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs.map +1 -0
  74. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs +439 -0
  75. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs.map +1 -0
  76. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +513 -0
  77. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +1 -0
  78. package/fesm2022/tinya-code-gas-admin-ui-list-CMzl9MkX.mjs +637 -0
  79. package/fesm2022/tinya-code-gas-admin-ui-list-CMzl9MkX.mjs.map +1 -0
  80. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +127 -0
  81. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +1 -0
  82. package/fesm2022/tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs +251 -0
  83. package/fesm2022/tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs.map +1 -0
  84. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +66 -0
  85. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +1 -0
  86. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +114 -0
  87. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +1 -0
  88. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +116 -0
  89. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +1 -0
  90. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +235 -0
  91. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +1 -0
  92. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +8 -0
  93. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +1 -0
  94. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +124 -0
  95. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +1 -0
  96. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +249 -0
  97. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +1 -0
  98. package/fesm2022/tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs +309 -0
  99. package/fesm2022/tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs.map +1 -0
  100. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +39 -0
  101. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +1 -0
  102. package/fesm2022/tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs +546 -0
  103. package/fesm2022/tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs.map +1 -0
  104. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +11 -0
  105. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs +131 -0
  106. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs.map +1 -0
  107. package/fesm2022/tinya-code-gas-admin-ui-seed-CNbQtREA.mjs +80 -0
  108. package/fesm2022/tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map +1 -0
  109. package/fesm2022/tinya-code-gas-admin-ui-select-DzaRfHXO.mjs +292 -0
  110. package/fesm2022/tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map +1 -0
  111. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +119 -0
  112. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +1 -0
  113. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +158 -0
  114. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +1 -0
  115. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +26 -0
  116. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +1 -0
  117. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +205 -0
  118. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +1 -0
  119. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +432 -0
  120. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +1 -0
  121. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +36 -0
  122. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +1 -0
  123. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +107 -0
  124. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +1 -0
  125. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +223 -0
  126. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +1 -0
  127. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +107 -0
  128. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +1 -0
  129. package/fesm2022/tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs +60 -0
  130. package/fesm2022/tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs.map +1 -0
  131. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +141 -0
  132. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +1 -0
  133. package/fesm2022/tinya-code-gas-admin-ui.mjs +4400 -0
  134. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -0
  135. package/package.json +31 -3
  136. package/types/tinya-code-gas-admin-ui.d.ts +3091 -0
@@ -0,0 +1,4514 @@
1
+ import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
2
+ import { n as AuthService, t as ApiService } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
3
+ import { i as getSchema, s as ConfigService } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
4
+ import { n as get, r as isSupported, s as resolve, t as RENDER_CONTEXT_VACIO } 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 ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs";
10
+ import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
11
+ import { t as FIELD_ARIA } from "./tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs";
12
+ import { i as listTree, n as groupTree, r as itemTree, t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
13
+ import { t as FieldFile } from "./tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs";
14
+ import { t as FieldImage } from "./tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs";
15
+ import { t as FieldMultiselect } from "./tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs";
16
+ import { t as esGrupoRadio } from "./tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs";
17
+ import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
18
+ import { a as colSpanClass, i as seedFields, n as mergeRecord, r as seedFieldValue, t as isRecord } from "./tinya-code-gas-admin-ui-seed-CNbQtREA.mjs";
19
+ import { t as RelationDrawerService } from "./tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs";
20
+ import * as i0 from "@angular/core";
21
+ import { ApplicationRef, Component, DestroyRef, InjectionToken, Injector, PendingTasks, afterRenderEffect, computed, effect, forwardRef, inject, input, isDevMode, output, resource, signal, viewChild } from "@angular/core";
22
+ import { firstValueFrom, of } from "rxjs";
23
+ import { ActivatedRoute, Router } from "@angular/router";
24
+ import { LucideCheckCircle2, LucideCircleAlert, LucideCopy, LucideInfo, LucideShare, LucideShare2, LucideX } from "@lucide/angular";
25
+ import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
26
+ import { NgComponentOutlet } from "@angular/common";
27
+ import { FormField, applyEach, disabled, email, form, hidden, max, maxLength, min, minLength, pattern, readonly, required, submit, validate, validateAsync } from "@angular/forms/signals";
28
+ const TEXTO = () => import("./tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs").then((module) => module.FieldDisplayText);
29
+ const FECHA = () => import("./tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs").then((module) => module.FieldDisplayDate);
30
+ const NUMERO = () => import("./tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs").then((module) => module.FieldDisplayNumber);
31
+ const BADGE = () => import("./tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs").then((n) => n.t).then((module) => module.FieldDisplayBadge);
32
+ const IMAGEN = () => import("./tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs").then((module) => module.FieldDisplayImage);
33
+ const ARCHIVO = () => import("./tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs").then((module) => module.FieldDisplayFile);
34
+ const LISTA = () => import("./tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs").then((module) => module.FieldDisplayList);
35
+ const DISPLAY_POR_DEFECTO = {
36
+ text: TEXTO,
37
+ textarea: TEXTO,
38
+ slug: TEXTO,
39
+ url: TEXTO,
40
+ email: TEXTO,
41
+ phone: TEXTO,
42
+ "readonly-text": TEXTO,
43
+ relation: TEXTO,
44
+ group: TEXTO,
45
+ "key-value": TEXTO,
46
+ "string-list": TEXTO,
47
+ multiselect: TEXTO,
48
+ time: TEXTO,
49
+ date: FECHA,
50
+ number: NUMERO,
51
+ currency: NUMERO,
52
+ boolean: BADGE,
53
+ select: BADGE,
54
+ image: IMAGEN,
55
+ file: ARCHIVO,
56
+ list: LISTA
57
+ };
58
+ function displayLoader(type) {
59
+ return Object.prototype.hasOwnProperty.call(DISPLAY_POR_DEFECTO, type) ? DISPLAY_POR_DEFECTO[type] : void 0;
60
+ }
61
+ var UnknownRender = class UnknownRender {
62
+ type = input.required(...ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []);
63
+ label = input("", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
64
+ static ɵfac = i0.ɵɵngDeclareFactory({
65
+ minVersion: "12.0.0",
66
+ version: "22.2.3",
67
+ ngImport: i0,
68
+ type: UnknownRender,
69
+ deps: [],
70
+ target: i0.ɵɵFactoryTarget.Component
71
+ });
72
+ static ɵcmp = i0.ɵɵngDeclareComponent({
73
+ minVersion: "17.0.0",
74
+ version: "22.2.3",
75
+ type: UnknownRender,
76
+ isStandalone: true,
77
+ selector: "app-unknown-render",
78
+ inputs: {
79
+ type: {
80
+ classPropertyName: "type",
81
+ publicName: "type",
82
+ isSignal: true,
83
+ isRequired: true,
84
+ transformFunction: null
85
+ },
86
+ label: {
87
+ classPropertyName: "label",
88
+ publicName: "label",
89
+ isSignal: true,
90
+ isRequired: false,
91
+ transformFunction: null
92
+ }
93
+ },
94
+ ngImport: i0,
95
+ template: `
96
+ <div
97
+ class="mb-4 rounded-lg border border-amber-400/50 bg-amber-500/10 px-3 py-2"
98
+ data-field-unknown
99
+ >
100
+ <p class="flex items-center gap-1.5 text-xs font-medium text-amber-600">
101
+ <svg lucideInfo size="14" class="shrink-0" aria-hidden="true" />
102
+ Campo sin renderizador en esta versión
103
+ </p>
104
+ <p class="mt-1 text-help text-neutral">
105
+ Tipo <code class="rounded bg-amber-500/20 px-1 font-mono">{{ type() }}</code>
106
+ @if (label()) {
107
+ del campo «{{ label() }}»
108
+ }
109
+ — el valor existe en el schema y se conserva; sólo no se dibuja.
110
+ </p>
111
+ </div>
112
+ `,
113
+ isInline: true,
114
+ dependencies: [{
115
+ kind: "component",
116
+ type: LucideInfo,
117
+ selector: "svg[lucideInfo]"
118
+ }]
119
+ });
120
+ };
121
+ i0.ɵɵngDeclareClassMetadata({
122
+ minVersion: "12.0.0",
123
+ version: "22.2.3",
124
+ ngImport: i0,
125
+ type: UnknownRender,
126
+ decorators: [{
127
+ type: Component,
128
+ args: [{
129
+ selector: "app-unknown-render",
130
+ imports: [LucideInfo],
131
+ template: `
132
+ <div
133
+ class="mb-4 rounded-lg border border-amber-400/50 bg-amber-500/10 px-3 py-2"
134
+ data-field-unknown
135
+ >
136
+ <p class="flex items-center gap-1.5 text-xs font-medium text-amber-600">
137
+ <svg lucideInfo size="14" class="shrink-0" aria-hidden="true" />
138
+ Campo sin renderizador en esta versión
139
+ </p>
140
+ <p class="mt-1 text-help text-neutral">
141
+ Tipo <code class="rounded bg-amber-500/20 px-1 font-mono">{{ type() }}</code>
142
+ @if (label()) {
143
+ del campo «{{ label() }}»
144
+ }
145
+ — el valor existe en el schema y se conserva; sólo no se dibuja.
146
+ </p>
147
+ </div>
148
+ `
149
+ }]
150
+ }],
151
+ propDecorators: {
152
+ type: [{
153
+ type: i0.Input,
154
+ args: [{
155
+ isSignal: true,
156
+ alias: "type",
157
+ required: true
158
+ }]
159
+ }],
160
+ label: [{
161
+ type: i0.Input,
162
+ args: [{
163
+ isSignal: true,
164
+ alias: "label",
165
+ required: false
166
+ }]
167
+ }]
168
+ }
169
+ });
170
+ const RENDER_CONTEXT = new InjectionToken("RENDER_CONTEXT");
171
+ var RenderHost = class RenderHost {
172
+ kind = input("field", ...ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []);
173
+ type = input.required(...ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []);
174
+ config = input.required(...ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []);
175
+ ctx = input(RENDER_CONTEXT_VACIO, ...ngDevMode ? [{ debugName: "ctx" }] : /* istanbul ignore next */ []);
176
+ data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
177
+ mode = input("form", ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
178
+ value = input(null, ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
179
+ chip = input(false, ...ngDevMode ? [{ debugName: "chip" }] : /* istanbul ignore next */ []);
180
+ dev = isDevMode();
181
+ pendientes = inject(PendingTasks);
182
+ esCampo = computed(() => this.kind() === "field", ...ngDevMode ? [{ debugName: "esCampo" }] : /* istanbul ignore next */ []);
183
+ esDisplay = computed(() => this.mode() === "display", ...ngDevMode ? [{ debugName: "esDisplay" }] : /* istanbul ignore next */ []);
184
+ declaraDisplay = computed(() => get(this.kind(), this.type())?.surfaces?.includes("display") ?? false, ...ngDevMode ? [{ debugName: "declaraDisplay" }] : /* istanbul ignore next */ []);
185
+ loaderDisplay = computed(() => this.declaraDisplay() ? displayLoader(this.type()) : void 0, ...ngDevMode ? [{ debugName: "loaderDisplay" }] : /* istanbul ignore next */ []);
186
+ soportaDisplay = computed(() => this.loaderDisplay() !== void 0, ...ngDevMode ? [{ debugName: "soportaDisplay" }] : /* istanbul ignore next */ []);
187
+ vacio = computed(() => {
188
+ const valor = this.value();
189
+ return valor === null || valor === void 0 || valor === "";
190
+ }, ...ngDevMode ? [{ debugName: "vacio" }] : /* istanbul ignore next */ []);
191
+ cfgCampo = computed(() => this.config(), ...ngDevMode ? [{ debugName: "cfgCampo" }] : /* istanbul ignore next */ []);
192
+ node = computed(() => childTree(this.ctx().tree, this.cfgCampo().key), ...ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []);
193
+ state = computed(() => this.node()(), ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
194
+ name = computed(() => this.state().name(), ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
195
+ supported = computed(() => {
196
+ if (!isSupported(this.kind(), this.type())) return false;
197
+ return this.esCampo() ? !this.state().hidden() : true;
198
+ }, ...ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []);
199
+ unsupported = computed(() => this.esDisplay() ? !this.soportaDisplay() : !isSupported(this.kind(), this.type()), ...ngDevMode ? [{ debugName: "unsupported" }] : /* istanbul ignore next */ []);
200
+ showUnknown = computed(() => this.unsupported() && this.dev, ...ngDevMode ? [{ debugName: "showUnknown" }] : /* istanbul ignore next */ []);
201
+ _trazaTipoDesconocido = effect(() => {
202
+ if (!this.unsupported() || this.dev) return;
203
+ const superficie = this.esDisplay() ? "display" : "form";
204
+ console.warn(`[render-host] tipo sin renderizador (${superficie}): "${this.type()}" — se omite el control.`);
205
+ }, ...ngDevMode ? [{ debugName: "_trazaTipoDesconocido" }] : /* istanbul ignore next */ []);
206
+ control = signal(null, ...ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []);
207
+ carga = 0;
208
+ _cargar = effect(() => {
209
+ const type = this.type();
210
+ const carga = ++this.carga;
211
+ this.control.set(null);
212
+ if (this.esDisplay()) {
213
+ const loader = this.loaderDisplay();
214
+ if (!loader) return;
215
+ this.pendientes.run(async () => {
216
+ const componente = await loader();
217
+ if (this.carga === carga) this.control.set(componente);
218
+ });
219
+ return;
220
+ }
221
+ if (!isSupported(this.kind(), type)) return;
222
+ this.pendientes.run(async () => {
223
+ const componente = await resolve(this.kind(), type, UnknownRender);
224
+ if (this.carga === carga) this.control.set(componente);
225
+ });
226
+ }, ...ngDevMode ? [{ debugName: "_cargar" }] : /* istanbul ignore next */ []);
227
+ displayInputs = computed(() => ({
228
+ field: this.config(),
229
+ value: this.value(),
230
+ chip: this.chip()
231
+ }), ...ngDevMode ? [{ debugName: "displayInputs" }] : /* istanbul ignore next */ []);
232
+ controlInputs = computed(() => {
233
+ const cfg = this.config();
234
+ const extras = {
235
+ ctx: this.ctx(),
236
+ data: this.data(),
237
+ groupContent: this.groupTpl(),
238
+ childTemplate: this.childTpl()
239
+ };
240
+ const descriptor = get(this.kind(), this.type());
241
+ if (descriptor?.inputs) return descriptor.inputs(cfg, extras);
242
+ return this.esCampo() ? {
243
+ field: cfg,
244
+ tree: this.ctx().tree
245
+ } : {
246
+ widget: cfg,
247
+ data: this.data()
248
+ };
249
+ }, ...ngDevMode ? [{ debugName: "controlInputs" }] : /* istanbul ignore next */ []);
250
+ group = computed(() => this.config(), ...ngDevMode ? [{ debugName: "group" }] : /* istanbul ignore next */ []);
251
+ list = computed(() => this.config(), ...ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []);
252
+ groupTpl = viewChild("groupContent", ...ngDevMode ? [{ debugName: "groupTpl" }] : /* istanbul ignore next */ []);
253
+ childTpl = viewChild("childTpl", ...ngDevMode ? [{ debugName: "childTpl" }] : /* istanbul ignore next */ []);
254
+ groupChildCtx = computed(() => ({
255
+ ...this.ctx(),
256
+ tree: groupTree(this.ctx().tree, this.config().key),
257
+ pathPrefix: `${this.ctx().pathPrefix}${this.config().key}.`
258
+ }), ...ngDevMode ? [{ debugName: "groupChildCtx" }] : /* istanbul ignore next */ []);
259
+ listChildCtx(child) {
260
+ return {
261
+ tree: child.tree,
262
+ serverErrors: child.errors,
263
+ pathPrefix: child.prefix,
264
+ exists: child.exists,
265
+ resource: this.ctx().resource
266
+ };
267
+ }
268
+ colSpan(width) {
269
+ return colSpanClass(width);
270
+ }
271
+ multiselectCfg = computed(() => this.config(), ...ngDevMode ? [{ debugName: "multiselectCfg" }] : /* istanbul ignore next */ []);
272
+ imageCfg = computed(() => this.config(), ...ngDevMode ? [{ debugName: "imageCfg" }] : /* istanbul ignore next */ []);
273
+ fileCfg = computed(() => this.config(), ...ngDevMode ? [{ debugName: "fileCfg" }] : /* istanbul ignore next */ []);
274
+ multiselectNode = computed(() => childTree(this.ctx().tree, this.config().key), ...ngDevMode ? [{ debugName: "multiselectNode" }] : /* istanbul ignore next */ []);
275
+ imageNode = computed(() => childTree(this.ctx().tree, this.config().key), ...ngDevMode ? [{ debugName: "imageNode" }] : /* istanbul ignore next */ []);
276
+ fileNode = computed(() => childTree(this.ctx().tree, this.config().key), ...ngDevMode ? [{ debugName: "fileNode" }] : /* istanbul ignore next */ []);
277
+ errorMessage = computed(() => {
278
+ const serverError = this.ctx().serverErrors[`${this.ctx().pathPrefix}${this.config().key}`];
279
+ if (serverError) return serverError;
280
+ const state = this.state();
281
+ if (!state.invalid() || !state.touched()) return null;
282
+ return state.errors()[0]?.message ?? "Revisa este campo.";
283
+ }, ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
284
+ composite = computed(() => {
285
+ const cfg = this.cfgCampo();
286
+ if ((/* @__PURE__ */ new Set([
287
+ "list",
288
+ "key-value",
289
+ "string-list",
290
+ "multiselect"
291
+ ])).has(cfg.type)) return true;
292
+ return cfg.type === "select" && esGrupoRadio(cfg);
293
+ }, ...ngDevMode ? [{ debugName: "composite" }] : /* istanbul ignore next */ []);
294
+ helpId = computed(() => this.cfgCampo().help && !this.errorMessage() ? `${this.name()}-help` : null, ...ngDevMode ? [{ debugName: "helpId" }] : /* istanbul ignore next */ []);
295
+ errorId = computed(() => this.errorMessage() ? `${this.name()}-error` : null, ...ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []);
296
+ describedBy = computed(() => [this.helpId(), this.errorId()].filter(Boolean).join(" ") || null, ...ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []);
297
+ invalid = computed(() => this.errorMessage() !== null, ...ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []);
298
+ required = computed(() => this.cfgCampo().required ?? false, ...ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []);
299
+ schema = computed(() => getSchema(this.ctx().resource), ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
300
+ markOptionals = computed(() => {
301
+ const fields = this.schema()?.fields ?? [];
302
+ if (fields.length === 0) return false;
303
+ return fields.filter((field) => field.required).length / fields.length >= .75;
304
+ }, ...ngDevMode ? [{ debugName: "markOptionals" }] : /* istanbul ignore next */ []);
305
+ showRequiredMark = computed(() => !this.markOptionals() && this.cfgCampo().required, ...ngDevMode ? [{ debugName: "showRequiredMark" }] : /* istanbul ignore next */ []);
306
+ showOptionalMark = computed(() => this.markOptionals() && !this.cfgCampo().required, ...ngDevMode ? [{ debugName: "showOptionalMark" }] : /* istanbul ignore next */ []);
307
+ get tree() {
308
+ return this.ctx().tree;
309
+ }
310
+ get serverErrors() {
311
+ return this.ctx().serverErrors;
312
+ }
313
+ get pathPrefix() {
314
+ return this.ctx().pathPrefix;
315
+ }
316
+ get exists() {
317
+ return this.ctx().exists;
318
+ }
319
+ get resource() {
320
+ return this.ctx().resource;
321
+ }
322
+ static ɵfac = i0.ɵɵngDeclareFactory({
323
+ minVersion: "12.0.0",
324
+ version: "22.2.3",
325
+ ngImport: i0,
326
+ type: RenderHost,
327
+ deps: [],
328
+ target: i0.ɵɵFactoryTarget.Component
329
+ });
330
+ static ɵcmp = i0.ɵɵngDeclareComponent({
331
+ minVersion: "17.0.0",
332
+ version: "22.2.3",
333
+ type: RenderHost,
334
+ isStandalone: true,
335
+ selector: "app-render-host",
336
+ inputs: {
337
+ kind: {
338
+ classPropertyName: "kind",
339
+ publicName: "kind",
340
+ isSignal: true,
341
+ isRequired: false,
342
+ transformFunction: null
343
+ },
344
+ type: {
345
+ classPropertyName: "type",
346
+ publicName: "type",
347
+ isSignal: true,
348
+ isRequired: true,
349
+ transformFunction: null
350
+ },
351
+ config: {
352
+ classPropertyName: "config",
353
+ publicName: "config",
354
+ isSignal: true,
355
+ isRequired: true,
356
+ transformFunction: null
357
+ },
358
+ ctx: {
359
+ classPropertyName: "ctx",
360
+ publicName: "ctx",
361
+ isSignal: true,
362
+ isRequired: false,
363
+ transformFunction: null
364
+ },
365
+ data: {
366
+ classPropertyName: "data",
367
+ publicName: "data",
368
+ isSignal: true,
369
+ isRequired: false,
370
+ transformFunction: null
371
+ },
372
+ mode: {
373
+ classPropertyName: "mode",
374
+ publicName: "mode",
375
+ isSignal: true,
376
+ isRequired: false,
377
+ transformFunction: null
378
+ },
379
+ value: {
380
+ classPropertyName: "value",
381
+ publicName: "value",
382
+ isSignal: true,
383
+ isRequired: false,
384
+ transformFunction: null
385
+ },
386
+ chip: {
387
+ classPropertyName: "chip",
388
+ publicName: "chip",
389
+ isSignal: true,
390
+ isRequired: false,
391
+ transformFunction: null
392
+ }
393
+ },
394
+ providers: [{
395
+ provide: FIELD_ARIA,
396
+ useExisting: forwardRef(() => RenderHost)
397
+ }, {
398
+ provide: RENDER_CONTEXT,
399
+ useExisting: forwardRef(() => RenderHost)
400
+ }],
401
+ viewQueries: [{
402
+ propertyName: "groupTpl",
403
+ first: true,
404
+ predicate: ["groupContent"],
405
+ descendants: true,
406
+ isSignal: true
407
+ }, {
408
+ propertyName: "childTpl",
409
+ first: true,
410
+ predicate: ["childTpl"],
411
+ descendants: true,
412
+ isSignal: true
413
+ }],
414
+ ngImport: i0,
415
+ template: `
416
+ <ng-template #groupContent>
417
+ @for (child of group().fields; track child.key) {
418
+ <div [class]="colSpan(child.width)">
419
+ <app-render-host
420
+ [kind]="kind()"
421
+ [type]="child.type"
422
+ [config]="child"
423
+ [ctx]="groupChildCtx()"
424
+ />
425
+ </div>
426
+ }
427
+ </ng-template>
428
+
429
+ <ng-template #childTpl let-child>
430
+ @for (sub of child.fields; track sub.key) {
431
+ <div [class]="child.row ? 'min-w-0 grow' : colSpan(sub.width)">
432
+ <app-render-host
433
+ [kind]="kind()"
434
+ [type]="sub.type"
435
+ [config]="sub"
436
+ [ctx]="listChildCtx(child)"
437
+ />
438
+ </div>
439
+ }
440
+ </ng-template>
441
+
442
+ @if (esDisplay()) {
443
+ <!-- Rama de display (R4.1 · R4.2): sólo el valor crudo. El host no
444
+ lee el árbol, no dibuja etiqueta ni error y no conoce la
445
+ sección: detail-view agrupa y este bloque pinta. -->
446
+ @if (vacio()) {
447
+ <span aria-hidden="true">—</span>
448
+ } @else if (soportaDisplay()) {
449
+ @if (control()) {
450
+ <ng-container
451
+ [ngComponentOutlet]="control()"
452
+ [ngComponentOutletInputs]="displayInputs()"
453
+ />
454
+ } @else {
455
+ <app-skeleton [lines]="1" [height]="20" />
456
+ }
457
+ } @else if (showUnknown()) {
458
+ <app-unknown-render [type]="type()" [label]="config().label" />
459
+ }
460
+ } @else if (supported()) {
461
+ @if (esCampo()) {
462
+ <div class="mb-4" [class.field-enter]="cfgCampo().visibleWhen" [attr.data-field]="name()">
463
+ @if (type() !== 'group') {
464
+ @if (composite()) {
465
+ <!-- Sin control único no existe ancla para el atributo for del
466
+ label: apuntaría a un id inexistente. Se dibuja como texto
467
+ (en listas/key-value los hijos reciben su etiqueta por
468
+ campo anidado; en un select en modo radio cada opción
469
+ trae su label implícito y el grupo va con aria-label). -->
470
+ <div class="mb-1 text-label font-medium">
471
+ {{ config().label }}
472
+ @if (showRequiredMark()) {
473
+ <span aria-hidden="true" class="text-danger">*</span>
474
+ }
475
+ @if (showOptionalMark()) {
476
+ <span class="font-normal text-neutral">(opcional)</span>
477
+ }
478
+ </div>
479
+ } @else {
480
+ <label [for]="name()" class="mb-1 block text-label font-medium">
481
+ {{ config().label }}
482
+ @if (showRequiredMark()) {
483
+ <span aria-hidden="true" class="text-danger">*</span>
484
+ }
485
+ @if (showOptionalMark()) {
486
+ <span class="font-normal text-neutral">(opcional)</span>
487
+ }
488
+ </label>
489
+ }
490
+ }
491
+
492
+ @if (type() === 'multiselect') {
493
+ <!-- Tres de los veintiuno no pueden pasar por "NgComponentOutlet": su
494
+ sincronización con Signal Forms vive en ɵɵcontrol, una
495
+ instrucción que el compilador emite únicamente junto al binding
496
+ formField de la propia plantilla. Sin ese binding escrito en el
497
+ HTML el model() de value nunca se inicializa y el control lanza
498
+ NG0952. Se declaran a mano (D27) y el resto de tipos siguen
499
+ saliendo del registro. -->
500
+ <app-field-multiselect
501
+ [id]="name()"
502
+ [field]="multiselectCfg()"
503
+ [formField]="multiselectNode()"
504
+ [tree]="ctx().tree"
505
+ />
506
+ } @else if (type() === 'image') {
507
+ <app-field-image
508
+ [id]="name()"
509
+ [field]="imageCfg()"
510
+ [formField]="imageNode()"
511
+ [resource]="ctx().resource"
512
+ />
513
+ } @else if (type() === 'file') {
514
+ <app-field-file
515
+ [id]="name()"
516
+ [field]="fileCfg()"
517
+ [formField]="fileNode()"
518
+ [resource]="ctx().resource"
519
+ />
520
+ } @else {
521
+ @if (control()) {
522
+ <ng-container
523
+ [ngComponentOutlet]="control()"
524
+ [ngComponentOutletInputs]="controlInputs()"
525
+ />
526
+ } @else {
527
+ <app-skeleton [lines]="2" [height]="32" />
528
+ }
529
+ }
530
+
531
+ @if (errorMessage(); as message) {
532
+ <p
533
+ [id]="errorId()"
534
+ class="mt-1 flex items-center gap-1 text-help text-danger"
535
+ role="alert"
536
+ >
537
+ <svg lucideCircleAlert size="14" class="shrink-0" />
538
+ {{ message }}
539
+ </p>
540
+ } @else if (cfgCampo().help; as help) {
541
+ <p [id]="helpId()" class="mt-1 text-help text-neutral">{{ help }}</p>
542
+ }
543
+ </div>
544
+ } @else {
545
+ <!-- Rama de widget (R3.6): el mismo outlet y el mismo cableado que
546
+ un campo, pero sin el envoltorio — un widget no tiene etiqueta
547
+ asociada, ni marca required, ni error, ni ayuda. El ancho en la
548
+ grilla lo pone su host (app-widget-host), no el motor. -->
549
+ @if (control()) {
550
+ <ng-container
551
+ [ngComponentOutlet]="control()"
552
+ [ngComponentOutletInputs]="controlInputs()"
553
+ />
554
+ } @else {
555
+ <app-skeleton [lines]="2" [height]="32" />
556
+ }
557
+ }
558
+ } @else if (showUnknown()) {
559
+ <!-- R1.10: degradación controlada. Un tipo fuera del catálogo no
560
+ revienta; en dev se avisa y en producción sólo se omite. -->
561
+ <app-unknown-render [type]="type()" [label]="config().label" />
562
+ }
563
+ `,
564
+ isInline: true,
565
+ dependencies: [
566
+ {
567
+ kind: "component",
568
+ type: RenderHost,
569
+ selector: "app-render-host",
570
+ inputs: [
571
+ "kind",
572
+ "type",
573
+ "config",
574
+ "ctx",
575
+ "data",
576
+ "mode",
577
+ "value",
578
+ "chip"
579
+ ]
580
+ },
581
+ {
582
+ kind: "component",
583
+ type: FieldFile,
584
+ selector: "app-field-file",
585
+ inputs: [
586
+ "field",
587
+ "resource",
588
+ "value",
589
+ "disabled"
590
+ ],
591
+ outputs: ["valueChange", "touch"]
592
+ },
593
+ {
594
+ kind: "component",
595
+ type: FieldImage,
596
+ selector: "app-field-image",
597
+ inputs: [
598
+ "field",
599
+ "resource",
600
+ "value",
601
+ "disabled"
602
+ ],
603
+ outputs: ["valueChange", "touch"]
604
+ },
605
+ {
606
+ kind: "component",
607
+ type: FieldMultiselect,
608
+ selector: "app-field-multiselect",
609
+ inputs: [
610
+ "field",
611
+ "tree",
612
+ "value",
613
+ "disabled"
614
+ ],
615
+ outputs: ["valueChange"]
616
+ },
617
+ {
618
+ kind: "directive",
619
+ type: FormField,
620
+ selector: "[formField]",
621
+ inputs: ["formField"],
622
+ exportAs: ["formField"]
623
+ },
624
+ {
625
+ kind: "component",
626
+ type: LucideCircleAlert,
627
+ selector: "svg[lucideCircleAlert], svg[lucideAlertCircle]"
628
+ },
629
+ {
630
+ kind: "directive",
631
+ type: NgComponentOutlet,
632
+ selector: "[ngComponentOutlet]",
633
+ inputs: [
634
+ "ngComponentOutlet",
635
+ "ngComponentOutletInputs",
636
+ "ngComponentOutletInjector",
637
+ "ngComponentOutletEnvironmentInjector",
638
+ "ngComponentOutletContent",
639
+ "ngComponentOutletNgModule"
640
+ ],
641
+ exportAs: ["ngComponentOutlet"]
642
+ },
643
+ {
644
+ kind: "component",
645
+ type: Skeleton,
646
+ selector: "app-skeleton",
647
+ inputs: [
648
+ "lines",
649
+ "height",
650
+ "width"
651
+ ]
652
+ },
653
+ {
654
+ kind: "component",
655
+ type: UnknownRender,
656
+ selector: "app-unknown-render",
657
+ inputs: ["type", "label"]
658
+ }
659
+ ]
660
+ });
661
+ };
662
+ i0.ɵɵngDeclareClassMetadata({
663
+ minVersion: "12.0.0",
664
+ version: "22.2.3",
665
+ ngImport: i0,
666
+ type: RenderHost,
667
+ decorators: [{
668
+ type: Component,
669
+ args: [{
670
+ selector: "app-render-host",
671
+ imports: [
672
+ FieldFile,
673
+ FieldImage,
674
+ FieldMultiselect,
675
+ FormField,
676
+ LucideCircleAlert,
677
+ NgComponentOutlet,
678
+ RenderHost,
679
+ Skeleton,
680
+ UnknownRender
681
+ ],
682
+ providers: [{
683
+ provide: FIELD_ARIA,
684
+ useExisting: forwardRef(() => RenderHost)
685
+ }, {
686
+ provide: RENDER_CONTEXT,
687
+ useExisting: forwardRef(() => RenderHost)
688
+ }],
689
+ template: `
690
+ <ng-template #groupContent>
691
+ @for (child of group().fields; track child.key) {
692
+ <div [class]="colSpan(child.width)">
693
+ <app-render-host
694
+ [kind]="kind()"
695
+ [type]="child.type"
696
+ [config]="child"
697
+ [ctx]="groupChildCtx()"
698
+ />
699
+ </div>
700
+ }
701
+ </ng-template>
702
+
703
+ <ng-template #childTpl let-child>
704
+ @for (sub of child.fields; track sub.key) {
705
+ <div [class]="child.row ? 'min-w-0 grow' : colSpan(sub.width)">
706
+ <app-render-host
707
+ [kind]="kind()"
708
+ [type]="sub.type"
709
+ [config]="sub"
710
+ [ctx]="listChildCtx(child)"
711
+ />
712
+ </div>
713
+ }
714
+ </ng-template>
715
+
716
+ @if (esDisplay()) {
717
+ <!-- Rama de display (R4.1 · R4.2): sólo el valor crudo. El host no
718
+ lee el árbol, no dibuja etiqueta ni error y no conoce la
719
+ sección: detail-view agrupa y este bloque pinta. -->
720
+ @if (vacio()) {
721
+ <span aria-hidden="true">—</span>
722
+ } @else if (soportaDisplay()) {
723
+ @if (control()) {
724
+ <ng-container
725
+ [ngComponentOutlet]="control()"
726
+ [ngComponentOutletInputs]="displayInputs()"
727
+ />
728
+ } @else {
729
+ <app-skeleton [lines]="1" [height]="20" />
730
+ }
731
+ } @else if (showUnknown()) {
732
+ <app-unknown-render [type]="type()" [label]="config().label" />
733
+ }
734
+ } @else if (supported()) {
735
+ @if (esCampo()) {
736
+ <div class="mb-4" [class.field-enter]="cfgCampo().visibleWhen" [attr.data-field]="name()">
737
+ @if (type() !== 'group') {
738
+ @if (composite()) {
739
+ <!-- Sin control único no existe ancla para el atributo for del
740
+ label: apuntaría a un id inexistente. Se dibuja como texto
741
+ (en listas/key-value los hijos reciben su etiqueta por
742
+ campo anidado; en un select en modo radio cada opción
743
+ trae su label implícito y el grupo va con aria-label). -->
744
+ <div class="mb-1 text-label font-medium">
745
+ {{ config().label }}
746
+ @if (showRequiredMark()) {
747
+ <span aria-hidden="true" class="text-danger">*</span>
748
+ }
749
+ @if (showOptionalMark()) {
750
+ <span class="font-normal text-neutral">(opcional)</span>
751
+ }
752
+ </div>
753
+ } @else {
754
+ <label [for]="name()" class="mb-1 block text-label font-medium">
755
+ {{ config().label }}
756
+ @if (showRequiredMark()) {
757
+ <span aria-hidden="true" class="text-danger">*</span>
758
+ }
759
+ @if (showOptionalMark()) {
760
+ <span class="font-normal text-neutral">(opcional)</span>
761
+ }
762
+ </label>
763
+ }
764
+ }
765
+
766
+ @if (type() === 'multiselect') {
767
+ <!-- Tres de los veintiuno no pueden pasar por "NgComponentOutlet": su
768
+ sincronización con Signal Forms vive en ɵɵcontrol, una
769
+ instrucción que el compilador emite únicamente junto al binding
770
+ formField de la propia plantilla. Sin ese binding escrito en el
771
+ HTML el model() de value nunca se inicializa y el control lanza
772
+ NG0952. Se declaran a mano (D27) y el resto de tipos siguen
773
+ saliendo del registro. -->
774
+ <app-field-multiselect
775
+ [id]="name()"
776
+ [field]="multiselectCfg()"
777
+ [formField]="multiselectNode()"
778
+ [tree]="ctx().tree"
779
+ />
780
+ } @else if (type() === 'image') {
781
+ <app-field-image
782
+ [id]="name()"
783
+ [field]="imageCfg()"
784
+ [formField]="imageNode()"
785
+ [resource]="ctx().resource"
786
+ />
787
+ } @else if (type() === 'file') {
788
+ <app-field-file
789
+ [id]="name()"
790
+ [field]="fileCfg()"
791
+ [formField]="fileNode()"
792
+ [resource]="ctx().resource"
793
+ />
794
+ } @else {
795
+ @if (control()) {
796
+ <ng-container
797
+ [ngComponentOutlet]="control()"
798
+ [ngComponentOutletInputs]="controlInputs()"
799
+ />
800
+ } @else {
801
+ <app-skeleton [lines]="2" [height]="32" />
802
+ }
803
+ }
804
+
805
+ @if (errorMessage(); as message) {
806
+ <p
807
+ [id]="errorId()"
808
+ class="mt-1 flex items-center gap-1 text-help text-danger"
809
+ role="alert"
810
+ >
811
+ <svg lucideCircleAlert size="14" class="shrink-0" />
812
+ {{ message }}
813
+ </p>
814
+ } @else if (cfgCampo().help; as help) {
815
+ <p [id]="helpId()" class="mt-1 text-help text-neutral">{{ help }}</p>
816
+ }
817
+ </div>
818
+ } @else {
819
+ <!-- Rama de widget (R3.6): el mismo outlet y el mismo cableado que
820
+ un campo, pero sin el envoltorio — un widget no tiene etiqueta
821
+ asociada, ni marca required, ni error, ni ayuda. El ancho en la
822
+ grilla lo pone su host (app-widget-host), no el motor. -->
823
+ @if (control()) {
824
+ <ng-container
825
+ [ngComponentOutlet]="control()"
826
+ [ngComponentOutletInputs]="controlInputs()"
827
+ />
828
+ } @else {
829
+ <app-skeleton [lines]="2" [height]="32" />
830
+ }
831
+ }
832
+ } @else if (showUnknown()) {
833
+ <!-- R1.10: degradación controlada. Un tipo fuera del catálogo no
834
+ revienta; en dev se avisa y en producción sólo se omite. -->
835
+ <app-unknown-render [type]="type()" [label]="config().label" />
836
+ }
837
+ `
838
+ }]
839
+ }],
840
+ propDecorators: {
841
+ kind: [{
842
+ type: i0.Input,
843
+ args: [{
844
+ isSignal: true,
845
+ alias: "kind",
846
+ required: false
847
+ }]
848
+ }],
849
+ type: [{
850
+ type: i0.Input,
851
+ args: [{
852
+ isSignal: true,
853
+ alias: "type",
854
+ required: true
855
+ }]
856
+ }],
857
+ config: [{
858
+ type: i0.Input,
859
+ args: [{
860
+ isSignal: true,
861
+ alias: "config",
862
+ required: true
863
+ }]
864
+ }],
865
+ ctx: [{
866
+ type: i0.Input,
867
+ args: [{
868
+ isSignal: true,
869
+ alias: "ctx",
870
+ required: false
871
+ }]
872
+ }],
873
+ data: [{
874
+ type: i0.Input,
875
+ args: [{
876
+ isSignal: true,
877
+ alias: "data",
878
+ required: false
879
+ }]
880
+ }],
881
+ mode: [{
882
+ type: i0.Input,
883
+ args: [{
884
+ isSignal: true,
885
+ alias: "mode",
886
+ required: false
887
+ }]
888
+ }],
889
+ value: [{
890
+ type: i0.Input,
891
+ args: [{
892
+ isSignal: true,
893
+ alias: "value",
894
+ required: false
895
+ }]
896
+ }],
897
+ chip: [{
898
+ type: i0.Input,
899
+ args: [{
900
+ isSignal: true,
901
+ alias: "chip",
902
+ required: false
903
+ }]
904
+ }],
905
+ groupTpl: [{
906
+ type: i0.ViewChild,
907
+ args: ["groupContent", { isSignal: true }]
908
+ }],
909
+ childTpl: [{
910
+ type: i0.ViewChild,
911
+ args: ["childTpl", { isSignal: true }]
912
+ }]
913
+ }
914
+ });
915
+ function buildShareMessage(url, shareText) {
916
+ const text = (shareText ?? "").trim();
917
+ if (!text) return url;
918
+ if (url && mentionsSite(text, url)) return text;
919
+ return url ? `${text} ${url}` : text;
920
+ }
921
+ function mentionsSite(text, url) {
922
+ if (text.includes(url)) return true;
923
+ try {
924
+ const { origin } = new URL(url);
925
+ return origin !== "null" && text.includes(origin);
926
+ } catch {
927
+ return false;
928
+ }
929
+ }
930
+ async function writeClipboard(text) {
931
+ const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
932
+ if (clipboard && typeof clipboard.writeText === "function") try {
933
+ await clipboard.writeText(text);
934
+ return true;
935
+ } catch {}
936
+ return legacyCopy(text);
937
+ }
938
+ function legacyCopy(text) {
939
+ if (typeof document === "undefined" || typeof document.execCommand !== "function") return false;
940
+ const area = document.createElement("textarea");
941
+ area.value = text;
942
+ area.setAttribute("readonly", "");
943
+ area.style.position = "fixed";
944
+ area.style.top = "0";
945
+ area.style.opacity = "0";
946
+ document.body.appendChild(area);
947
+ area.select();
948
+ let copied = false;
949
+ try {
950
+ copied = document.execCommand("copy");
951
+ } catch {
952
+ copied = false;
953
+ }
954
+ document.body.removeChild(area);
955
+ return copied;
956
+ }
957
+ function isShareAction(action) {
958
+ return action.type === "copy-share";
959
+ }
960
+ function isTriggerAction(action) {
961
+ return action.type === "trigger";
962
+ }
963
+ function resolveShare(action, vars) {
964
+ const template = action.shareTextTemplate || vars["share_message_template"];
965
+ return {
966
+ url: interpolateTemplate(action.urlTemplate, vars),
967
+ shareText: template ? interpolateTemplate(template, vars) : ""
968
+ };
969
+ }
970
+ const actionDescriptors = [
971
+ {
972
+ type: "copy-share",
973
+ surfaces: [
974
+ "row",
975
+ "detail",
976
+ "post-create"
977
+ ],
978
+ icon: LucideShare2,
979
+ run: async (ctx, action) => {
980
+ const { url, shareText } = resolveShare(action, ctx.vars);
981
+ const message = buildShareMessage(url, shareText);
982
+ if (await writeClipboard(message)) ctx.toast.success("Mensaje copiado al portapapeles.");
983
+ else ctx.toast.info(`Copiar manualmente: ${message}`);
984
+ return { ok: true };
985
+ }
986
+ },
987
+ {
988
+ type: "navigate",
989
+ surfaces: ["row", "detail"],
990
+ run: async (ctx, action) => {
991
+ const url = interpolateTemplate(action.urlTemplate, ctx.vars);
992
+ if (/^https?:\/\//.test(url)) window.open(url, "_blank", "noopener");
993
+ else await ctx.router.navigateByUrl(url);
994
+ return { ok: true };
995
+ }
996
+ },
997
+ {
998
+ type: "trigger",
999
+ surfaces: ["row", "detail"],
1000
+ confirm: true,
1001
+ run: async (ctx, action) => {
1002
+ const declared = action;
1003
+ const rawKey = ctx.record?.[ctx.keyField];
1004
+ if (rawKey === null || rawKey === void 0 || rawKey === "") throw new Error(`La acción «${declared.label}» necesita la clave ${ctx.keyField} del registro.`);
1005
+ const name = declared.name ?? declared.id;
1006
+ const route = `/admin/${ctx.resource}/${String(rawKey)}/actions/${name}`;
1007
+ await firstValueFrom(ctx.api.request("POST", route));
1008
+ return {
1009
+ ok: true,
1010
+ reload: true
1011
+ };
1012
+ }
1013
+ }
1014
+ ];
1015
+ const ACCIONES = /* @__PURE__ */ new Map();
1016
+ function registerAction(descriptor) {
1017
+ ACCIONES.set(descriptor.type, descriptor);
1018
+ }
1019
+ function getAction(type) {
1020
+ return ACCIONES.get(type);
1021
+ }
1022
+ function actionsFor(surface) {
1023
+ return [...ACCIONES.values()].filter((descriptor) => descriptor.surfaces.includes(surface));
1024
+ }
1025
+ function actionType(action) {
1026
+ return action.type ?? "navigate";
1027
+ }
1028
+ function actionTypes() {
1029
+ return [...ACCIONES.keys()];
1030
+ }
1031
+ for (const descriptor of actionDescriptors) registerAction(descriptor);
1032
+ var FieldHost = class FieldHost {
1033
+ field = input.required(...ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []);
1034
+ tree = input.required(...ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []);
1035
+ exists = input(false, ...ngDevMode ? [{ debugName: "exists" }] : /* istanbul ignore next */ []);
1036
+ serverErrors = input({}, ...ngDevMode ? [{ debugName: "serverErrors" }] : /* istanbul ignore next */ []);
1037
+ pathPrefix = input("", ...ngDevMode ? [{ debugName: "pathPrefix" }] : /* istanbul ignore next */ []);
1038
+ resource = input("", ...ngDevMode ? [{ debugName: "resource" }] : /* istanbul ignore next */ []);
1039
+ ctx = computed(() => ({
1040
+ tree: this.tree(),
1041
+ serverErrors: this.serverErrors(),
1042
+ pathPrefix: this.pathPrefix(),
1043
+ exists: this.exists(),
1044
+ resource: this.resource()
1045
+ }), ...ngDevMode ? [{ debugName: "ctx" }] : /* istanbul ignore next */ []);
1046
+ static ɵfac = i0.ɵɵngDeclareFactory({
1047
+ minVersion: "12.0.0",
1048
+ version: "22.2.3",
1049
+ ngImport: i0,
1050
+ type: FieldHost,
1051
+ deps: [],
1052
+ target: i0.ɵɵFactoryTarget.Component
1053
+ });
1054
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1055
+ minVersion: "17.1.0",
1056
+ version: "22.2.3",
1057
+ type: FieldHost,
1058
+ isStandalone: true,
1059
+ selector: "app-field-host",
1060
+ inputs: {
1061
+ field: {
1062
+ classPropertyName: "field",
1063
+ publicName: "field",
1064
+ isSignal: true,
1065
+ isRequired: true,
1066
+ transformFunction: null
1067
+ },
1068
+ tree: {
1069
+ classPropertyName: "tree",
1070
+ publicName: "tree",
1071
+ isSignal: true,
1072
+ isRequired: true,
1073
+ transformFunction: null
1074
+ },
1075
+ exists: {
1076
+ classPropertyName: "exists",
1077
+ publicName: "exists",
1078
+ isSignal: true,
1079
+ isRequired: false,
1080
+ transformFunction: null
1081
+ },
1082
+ serverErrors: {
1083
+ classPropertyName: "serverErrors",
1084
+ publicName: "serverErrors",
1085
+ isSignal: true,
1086
+ isRequired: false,
1087
+ transformFunction: null
1088
+ },
1089
+ pathPrefix: {
1090
+ classPropertyName: "pathPrefix",
1091
+ publicName: "pathPrefix",
1092
+ isSignal: true,
1093
+ isRequired: false,
1094
+ transformFunction: null
1095
+ },
1096
+ resource: {
1097
+ classPropertyName: "resource",
1098
+ publicName: "resource",
1099
+ isSignal: true,
1100
+ isRequired: false,
1101
+ transformFunction: null
1102
+ }
1103
+ },
1104
+ ngImport: i0,
1105
+ template: `
1106
+ <app-render-host kind="field" [type]="field().type" [config]="field()" [ctx]="ctx()" />
1107
+ `,
1108
+ isInline: true,
1109
+ dependencies: [{
1110
+ kind: "component",
1111
+ type: RenderHost,
1112
+ selector: "app-render-host",
1113
+ inputs: [
1114
+ "kind",
1115
+ "type",
1116
+ "config",
1117
+ "ctx",
1118
+ "data",
1119
+ "mode",
1120
+ "value",
1121
+ "chip"
1122
+ ]
1123
+ }]
1124
+ });
1125
+ };
1126
+ i0.ɵɵngDeclareClassMetadata({
1127
+ minVersion: "12.0.0",
1128
+ version: "22.2.3",
1129
+ ngImport: i0,
1130
+ type: FieldHost,
1131
+ decorators: [{
1132
+ type: Component,
1133
+ args: [{
1134
+ selector: "app-field-host",
1135
+ imports: [RenderHost],
1136
+ template: `
1137
+ <app-render-host kind="field" [type]="field().type" [config]="field()" [ctx]="ctx()" />
1138
+ `
1139
+ }]
1140
+ }],
1141
+ propDecorators: {
1142
+ field: [{
1143
+ type: i0.Input,
1144
+ args: [{
1145
+ isSignal: true,
1146
+ alias: "field",
1147
+ required: true
1148
+ }]
1149
+ }],
1150
+ tree: [{
1151
+ type: i0.Input,
1152
+ args: [{
1153
+ isSignal: true,
1154
+ alias: "tree",
1155
+ required: true
1156
+ }]
1157
+ }],
1158
+ exists: [{
1159
+ type: i0.Input,
1160
+ args: [{
1161
+ isSignal: true,
1162
+ alias: "exists",
1163
+ required: false
1164
+ }]
1165
+ }],
1166
+ serverErrors: [{
1167
+ type: i0.Input,
1168
+ args: [{
1169
+ isSignal: true,
1170
+ alias: "serverErrors",
1171
+ required: false
1172
+ }]
1173
+ }],
1174
+ pathPrefix: [{
1175
+ type: i0.Input,
1176
+ args: [{
1177
+ isSignal: true,
1178
+ alias: "pathPrefix",
1179
+ required: false
1180
+ }]
1181
+ }],
1182
+ resource: [{
1183
+ type: i0.Input,
1184
+ args: [{
1185
+ isSignal: true,
1186
+ alias: "resource",
1187
+ required: false
1188
+ }]
1189
+ }]
1190
+ }
1191
+ });
1192
+ const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
1193
+ function isVisibleNow(element) {
1194
+ if (element.hasAttribute("hidden") || element.closest("[hidden]") !== null) return false;
1195
+ const check = element.checkVisibility;
1196
+ return typeof check !== "function" || check.call(element);
1197
+ }
1198
+ const WIDTHS = {
1199
+ sm: "max-w-sm",
1200
+ md: "max-w-lg",
1201
+ lg: "max-w-2xl"
1202
+ };
1203
+ let titleCounter$1 = 0;
1204
+ var Drawer = class Drawer {
1205
+ open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
1206
+ title = input("Editar", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
1207
+ subtitle = input(null, ...ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []);
1208
+ size = input("md", ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
1209
+ closeLabel = input("Cerrar", ...ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []);
1210
+ close = output();
1211
+ titleId = `drawer-title-${++titleCounter$1}`;
1212
+ widthClass = computed(() => WIDTHS[this.size()], ...ngDevMode ? [{ debugName: "widthClass" }] : /* istanbul ignore next */ []);
1213
+ dialogRef = viewChild("dialog", ...ngDevMode ? [{ debugName: "dialogRef" }] : /* istanbul ignore next */ []);
1214
+ lastFocused = null;
1215
+ focusEffect = afterRenderEffect(() => {
1216
+ const open = this.open();
1217
+ const dialog = this.dialogRef();
1218
+ if (open && dialog) {
1219
+ const active = document.activeElement;
1220
+ if (active !== dialog.nativeElement && this.lastFocused === null) this.lastFocused = active instanceof HTMLElement ? active : null;
1221
+ dialog.nativeElement.focus();
1222
+ } else if (!open && this.lastFocused !== null) {
1223
+ this.lastFocused.focus();
1224
+ this.lastFocused = null;
1225
+ }
1226
+ }, ...ngDevMode ? [{ debugName: "focusEffect" }] : /* istanbul ignore next */ []);
1227
+ ngOnDestroy() {
1228
+ if (this.lastFocused?.isConnected) this.lastFocused.focus();
1229
+ this.lastFocused = null;
1230
+ }
1231
+ onEscape() {
1232
+ if (this.open()) this.close.emit();
1233
+ }
1234
+ onTab(rawEvent) {
1235
+ const event = rawEvent;
1236
+ const dialog = this.dialogRef()?.nativeElement;
1237
+ if (!this.open() || !dialog) return;
1238
+ const focusable = Array.from(dialog.querySelectorAll(FOCUSABLE)).filter((element) => isVisibleNow(element));
1239
+ if (focusable.length === 0) {
1240
+ event.preventDefault();
1241
+ dialog.focus();
1242
+ return;
1243
+ }
1244
+ const first = focusable[0];
1245
+ const last = focusable[focusable.length - 1];
1246
+ const active = document.activeElement;
1247
+ if (event.shiftKey && (active === first || active === dialog)) {
1248
+ event.preventDefault();
1249
+ last.focus();
1250
+ } else if (!event.shiftKey && active === last) {
1251
+ event.preventDefault();
1252
+ first.focus();
1253
+ }
1254
+ }
1255
+ static ɵfac = i0.ɵɵngDeclareFactory({
1256
+ minVersion: "12.0.0",
1257
+ version: "22.2.3",
1258
+ ngImport: i0,
1259
+ type: Drawer,
1260
+ deps: [],
1261
+ target: i0.ɵɵFactoryTarget.Component
1262
+ });
1263
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1264
+ minVersion: "17.0.0",
1265
+ version: "22.2.3",
1266
+ type: Drawer,
1267
+ isStandalone: true,
1268
+ selector: "app-drawer",
1269
+ inputs: {
1270
+ open: {
1271
+ classPropertyName: "open",
1272
+ publicName: "open",
1273
+ isSignal: true,
1274
+ isRequired: false,
1275
+ transformFunction: null
1276
+ },
1277
+ title: {
1278
+ classPropertyName: "title",
1279
+ publicName: "title",
1280
+ isSignal: true,
1281
+ isRequired: false,
1282
+ transformFunction: null
1283
+ },
1284
+ subtitle: {
1285
+ classPropertyName: "subtitle",
1286
+ publicName: "subtitle",
1287
+ isSignal: true,
1288
+ isRequired: false,
1289
+ transformFunction: null
1290
+ },
1291
+ size: {
1292
+ classPropertyName: "size",
1293
+ publicName: "size",
1294
+ isSignal: true,
1295
+ isRequired: false,
1296
+ transformFunction: null
1297
+ },
1298
+ closeLabel: {
1299
+ classPropertyName: "closeLabel",
1300
+ publicName: "closeLabel",
1301
+ isSignal: true,
1302
+ isRequired: false,
1303
+ transformFunction: null
1304
+ }
1305
+ },
1306
+ outputs: { close: "close" },
1307
+ host: { listeners: {
1308
+ "document:keydown.escape": "onEscape()",
1309
+ "document:keydown.tab": "onTab($event)",
1310
+ "document:keydown.shift.tab": "onTab($event)"
1311
+ } },
1312
+ viewQueries: [{
1313
+ propertyName: "dialogRef",
1314
+ first: true,
1315
+ predicate: ["dialog"],
1316
+ descendants: true,
1317
+ isSignal: true
1318
+ }],
1319
+ ngImport: i0,
1320
+ template: `
1321
+ @if (open()) {
1322
+ <div class="fixed inset-0 z-50">
1323
+ <div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
1324
+ <section
1325
+ #dialog
1326
+ role="dialog"
1327
+ aria-modal="true"
1328
+ [attr.aria-labelledby]="titleId"
1329
+ tabindex="-1"
1330
+ class="absolute inset-y-0 right-0 flex w-full flex-col bg-surface-raised shadow-2xl focus:outline-none"
1331
+ [class]="widthClass()"
1332
+ >
1333
+ <header
1334
+ class="flex items-start justify-between gap-4 border-b border-neutral/20 px-5 py-4"
1335
+ >
1336
+ <div>
1337
+ <h2 [id]="titleId" class="font-display text-section font-semibold">{{ title() }}</h2>
1338
+ @if (subtitle(); as subtitleText) {
1339
+ <p class="mt-1 text-sm text-neutral/70">{{ subtitleText }}</p>
1340
+ }
1341
+ </div>
1342
+ <button
1343
+ type="button"
1344
+ 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"
1345
+ [attr.aria-label]="closeLabel()"
1346
+ (click)="close.emit()"
1347
+ >
1348
+ <svg lucideX size="18" />
1349
+ </button>
1350
+ </header>
1351
+
1352
+ <div class="flex-1 overflow-y-auto px-5 py-4">
1353
+ <ng-content />
1354
+ </div>
1355
+
1356
+ <div class="border-t border-neutral/20 px-5 py-4">
1357
+ <ng-content select="[drawer-actions]" />
1358
+ </div>
1359
+ </section>
1360
+ </div>
1361
+ }
1362
+ `,
1363
+ isInline: true,
1364
+ dependencies: [{
1365
+ kind: "component",
1366
+ type: LucideX,
1367
+ selector: "svg[lucideX]"
1368
+ }]
1369
+ });
1370
+ };
1371
+ i0.ɵɵngDeclareClassMetadata({
1372
+ minVersion: "12.0.0",
1373
+ version: "22.2.3",
1374
+ ngImport: i0,
1375
+ type: Drawer,
1376
+ decorators: [{
1377
+ type: Component,
1378
+ args: [{
1379
+ selector: "app-drawer",
1380
+ imports: [LucideX],
1381
+ host: {
1382
+ "(document:keydown.escape)": "onEscape()",
1383
+ "(document:keydown.tab)": "onTab($event)",
1384
+ "(document:keydown.shift.tab)": "onTab($event)"
1385
+ },
1386
+ template: `
1387
+ @if (open()) {
1388
+ <div class="fixed inset-0 z-50">
1389
+ <div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
1390
+ <section
1391
+ #dialog
1392
+ role="dialog"
1393
+ aria-modal="true"
1394
+ [attr.aria-labelledby]="titleId"
1395
+ tabindex="-1"
1396
+ class="absolute inset-y-0 right-0 flex w-full flex-col bg-surface-raised shadow-2xl focus:outline-none"
1397
+ [class]="widthClass()"
1398
+ >
1399
+ <header
1400
+ class="flex items-start justify-between gap-4 border-b border-neutral/20 px-5 py-4"
1401
+ >
1402
+ <div>
1403
+ <h2 [id]="titleId" class="font-display text-section font-semibold">{{ title() }}</h2>
1404
+ @if (subtitle(); as subtitleText) {
1405
+ <p class="mt-1 text-sm text-neutral/70">{{ subtitleText }}</p>
1406
+ }
1407
+ </div>
1408
+ <button
1409
+ type="button"
1410
+ 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"
1411
+ [attr.aria-label]="closeLabel()"
1412
+ (click)="close.emit()"
1413
+ >
1414
+ <svg lucideX size="18" />
1415
+ </button>
1416
+ </header>
1417
+
1418
+ <div class="flex-1 overflow-y-auto px-5 py-4">
1419
+ <ng-content />
1420
+ </div>
1421
+
1422
+ <div class="border-t border-neutral/20 px-5 py-4">
1423
+ <ng-content select="[drawer-actions]" />
1424
+ </div>
1425
+ </section>
1426
+ </div>
1427
+ }
1428
+ `
1429
+ }]
1430
+ }],
1431
+ propDecorators: {
1432
+ open: [{
1433
+ type: i0.Input,
1434
+ args: [{
1435
+ isSignal: true,
1436
+ alias: "open",
1437
+ required: false
1438
+ }]
1439
+ }],
1440
+ title: [{
1441
+ type: i0.Input,
1442
+ args: [{
1443
+ isSignal: true,
1444
+ alias: "title",
1445
+ required: false
1446
+ }]
1447
+ }],
1448
+ subtitle: [{
1449
+ type: i0.Input,
1450
+ args: [{
1451
+ isSignal: true,
1452
+ alias: "subtitle",
1453
+ required: false
1454
+ }]
1455
+ }],
1456
+ size: [{
1457
+ type: i0.Input,
1458
+ args: [{
1459
+ isSignal: true,
1460
+ alias: "size",
1461
+ required: false
1462
+ }]
1463
+ }],
1464
+ closeLabel: [{
1465
+ type: i0.Input,
1466
+ args: [{
1467
+ isSignal: true,
1468
+ alias: "closeLabel",
1469
+ required: false
1470
+ }]
1471
+ }],
1472
+ close: [{
1473
+ type: i0.Output,
1474
+ args: ["close"]
1475
+ }],
1476
+ dialogRef: [{
1477
+ type: i0.ViewChild,
1478
+ args: ["dialog", { isSignal: true }]
1479
+ }]
1480
+ }
1481
+ });
1482
+ function validateMinWords(value, min) {
1483
+ const words = wordCount(value);
1484
+ if (words === null) return null;
1485
+ return words >= min ? null : `Debe tener al menos ${min} palabras.`;
1486
+ }
1487
+ function validateMaxWords(value, max) {
1488
+ const words = wordCount(value);
1489
+ if (words === null) return null;
1490
+ return words <= max ? null : `Como máximo ${max} palabras.`;
1491
+ }
1492
+ function isEmpty(value) {
1493
+ return value === void 0 || value === null || value === "";
1494
+ }
1495
+ function wordCount(value) {
1496
+ if (isEmpty(value) || typeof value !== "string") return null;
1497
+ return value.trim().split(/\s+/).filter(Boolean).length;
1498
+ }
1499
+ const TEXT_TYPES = /* @__PURE__ */ new Set([
1500
+ "text",
1501
+ "textarea",
1502
+ "email",
1503
+ "url",
1504
+ "slug",
1505
+ "phone",
1506
+ "time",
1507
+ "readonly-text",
1508
+ "select",
1509
+ "relation"
1510
+ ]);
1511
+ const LIST_TYPES = /* @__PURE__ */ new Set([
1512
+ "list",
1513
+ "string-list",
1514
+ "key-value"
1515
+ ]);
1516
+ function minLengthOf(field) {
1517
+ return TEXT_TYPES.has(field.type) ? field.validators?.minLength : void 0;
1518
+ }
1519
+ function maxLengthOf(field) {
1520
+ if (!TEXT_TYPES.has(field.type)) return void 0;
1521
+ if (field.type === "text") return field.validators?.maxLength ?? field.maxLength;
1522
+ return field.validators?.maxLength;
1523
+ }
1524
+ function patternOf(field) {
1525
+ if (!TEXT_TYPES.has(field.type)) return void 0;
1526
+ if (field.type === "text") return field.validators?.pattern ?? field.pattern;
1527
+ return field.validators?.pattern;
1528
+ }
1529
+ function minWordsOf(field) {
1530
+ if (!TEXT_TYPES.has(field.type)) return void 0;
1531
+ if (field.type === "textarea") return field.validators?.minWords ?? field.minWords;
1532
+ return field.validators?.minWords;
1533
+ }
1534
+ function maxWordsOf(field) {
1535
+ if (!TEXT_TYPES.has(field.type)) return void 0;
1536
+ if (field.type === "textarea") return field.validators?.maxWords ?? field.maxWords;
1537
+ return field.validators?.maxWords;
1538
+ }
1539
+ function numberBoundOf(field, bound) {
1540
+ if (field.type === "number") return field.validators?.[bound] ?? (bound === "min" ? field.min : field.max);
1541
+ if (field.type === "currency") return field.validators?.[bound];
1542
+ }
1543
+ function minItemsOf(field) {
1544
+ if (!LIST_TYPES.has(field.type)) return void 0;
1545
+ const list = field;
1546
+ return list.required ? Math.max(list.min ?? 1, 1) : list.min;
1547
+ }
1548
+ function maxItemsOf(field) {
1549
+ if (!LIST_TYPES.has(field.type)) return void 0;
1550
+ return field.max;
1551
+ }
1552
+ function toError(kind, message) {
1553
+ return message === null ? null : {
1554
+ kind,
1555
+ message
1556
+ };
1557
+ }
1558
+ function patternMessageFor(field) {
1559
+ const hint = field.help?.trim().replace(/[.。]+$/, "");
1560
+ return hint ? `Formato esperado: ${hint}.` : "Revisa el formato de este campo.";
1561
+ }
1562
+ function validateUrl(value) {
1563
+ if (value === void 0 || value === null || value === "") return null;
1564
+ if (typeof value !== "string") return URL_MESSAGE;
1565
+ return /^https?:\/\/\S+$/i.test(value) ? null : URL_MESSAGE;
1566
+ }
1567
+ const URL_MESSAGE = "Debe ser una URL válida (http o https).";
1568
+ function dateError(value, bound, iso) {
1569
+ if (typeof value !== "string" || value === "") return null;
1570
+ if (!(bound === "min" ? value < iso : value > iso)) return null;
1571
+ return {
1572
+ kind: `${bound}-date`,
1573
+ message: bound === "min" ? `La fecha no puede ser anterior a ${iso}.` : `La fecha no puede ser posterior a ${iso}.`
1574
+ };
1575
+ }
1576
+ function registerUrlRule(path) {
1577
+ validate(path, (ctx) => toError("url", validateUrl(ctx.value())));
1578
+ }
1579
+ function registerEmailRule(path) {
1580
+ email(path, { message: "Debe ser un correo electrónico válido." });
1581
+ }
1582
+ function registerUniqueRule(ctx) {
1583
+ const { path, field, schema, api, excludeKey } = ctx;
1584
+ const endpoint = schema.endpoint;
1585
+ const keyField = schema.keyField;
1586
+ const fieldName = field.key;
1587
+ validateAsync(path, {
1588
+ debounce: 300,
1589
+ params: (c) => {
1590
+ const value = c.value().trim();
1591
+ return value === "" ? void 0 : {
1592
+ value,
1593
+ exclude: excludeKey
1594
+ };
1595
+ },
1596
+ factory: (params) => resource({
1597
+ params,
1598
+ loader: async ({ params: request }) => {
1599
+ const rows = await firstValueFrom(api.list(endpoint));
1600
+ if (!Array.isArray(rows)) return [];
1601
+ return rows.filter((row) => row[fieldName] === request.value && (request.exclude === void 0 || row[keyField] !== request.exclude));
1602
+ }
1603
+ }),
1604
+ onError: () => null,
1605
+ onSuccess: (rows, c) => {
1606
+ if (c.value().trim() === "" || !Array.isArray(rows) || rows.length === 0) return null;
1607
+ return {
1608
+ kind: "unique",
1609
+ message: "Este valor ya existe en el registro."
1610
+ };
1611
+ }
1612
+ });
1613
+ }
1614
+ const FIELD_VALIDATORS = [
1615
+ {
1616
+ name: "required",
1617
+ declares: ({ field }) => field.required === true && field.type !== "boolean" && field.type !== "group" && !LIST_TYPES.has(field.type),
1618
+ register: ({ path }) => required(path, { message: "Campo obligatorio." })
1619
+ },
1620
+ {
1621
+ name: "email",
1622
+ declares: ({ field }) => field.type === "email",
1623
+ register: ({ path }) => registerEmailRule(path)
1624
+ },
1625
+ {
1626
+ name: "url",
1627
+ declares: ({ field }) => field.type === "url",
1628
+ register: ({ path }) => registerUrlRule(path)
1629
+ },
1630
+ {
1631
+ name: "min-length",
1632
+ declares: ({ field }) => minLengthOf(field) !== void 0,
1633
+ register: ({ path, field }) => {
1634
+ const min = minLengthOf(field);
1635
+ if (min !== void 0) minLength(path, min, { message: `Debe tener al menos ${min} caracteres.` });
1636
+ }
1637
+ },
1638
+ {
1639
+ name: "max-length",
1640
+ declares: ({ field }) => maxLengthOf(field) !== void 0,
1641
+ register: ({ path, field }) => {
1642
+ const max = maxLengthOf(field);
1643
+ if (max !== void 0) maxLength(path, max, { message: `Como máximo ${max} caracteres.` });
1644
+ }
1645
+ },
1646
+ {
1647
+ name: "pattern",
1648
+ declares: ({ field }) => patternOf(field) !== void 0,
1649
+ register: ({ path, field }) => {
1650
+ const pattern$1 = patternOf(field);
1651
+ if (pattern$1 !== void 0) pattern(path, new RegExp(pattern$1), { message: patternMessageFor(field) });
1652
+ }
1653
+ },
1654
+ {
1655
+ name: "min-words",
1656
+ declares: ({ field }) => minWordsOf(field) !== void 0,
1657
+ register: ({ path, field }) => {
1658
+ const min = minWordsOf(field);
1659
+ if (min !== void 0) validate(path, (ctx) => toError("min-words", validateMinWords(ctx.value(), min)));
1660
+ }
1661
+ },
1662
+ {
1663
+ name: "max-words",
1664
+ declares: ({ field }) => maxWordsOf(field) !== void 0,
1665
+ register: ({ path, field }) => {
1666
+ const max = maxWordsOf(field);
1667
+ if (max !== void 0) validate(path, (ctx) => toError("max-words", validateMaxWords(ctx.value(), max)));
1668
+ }
1669
+ },
1670
+ {
1671
+ name: "min",
1672
+ declares: ({ field }) => numberBoundOf(field, "min") !== void 0,
1673
+ register: ({ path, field }) => {
1674
+ const min$1 = numberBoundOf(field, "min");
1675
+ if (min$1 !== void 0) min(path, min$1, { message: `Debe ser como mínimo ${min$1}.` });
1676
+ }
1677
+ },
1678
+ {
1679
+ name: "max",
1680
+ declares: ({ field }) => numberBoundOf(field, "max") !== void 0,
1681
+ register: ({ path, field }) => {
1682
+ const max$1 = numberBoundOf(field, "max");
1683
+ if (max$1 !== void 0) max(path, max$1, { message: `Debe ser como máximo ${max$1}.` });
1684
+ }
1685
+ },
1686
+ {
1687
+ name: "min-items",
1688
+ declares: ({ field }) => minItemsOf(field) !== void 0,
1689
+ register: ({ path, field }) => {
1690
+ const min = minItemsOf(field);
1691
+ if (min !== void 0) minLength(path, min, { message: min === 1 ? "Debe tener al menos 1 elemento." : `Debe tener al menos ${min} elementos.` });
1692
+ }
1693
+ },
1694
+ {
1695
+ name: "max-items",
1696
+ declares: ({ field }) => maxItemsOf(field) !== void 0,
1697
+ register: ({ path, field }) => {
1698
+ const max = maxItemsOf(field);
1699
+ if (max !== void 0) maxLength(path, max, { message: max === 1 ? "Como máximo 1 elemento." : `Como máximo ${max} elementos.` });
1700
+ }
1701
+ },
1702
+ {
1703
+ name: "date-min",
1704
+ declares: ({ field }) => field.type === "date" && field.min !== void 0,
1705
+ register: ({ path, field }) => {
1706
+ const min = field.type === "date" ? field.min : void 0;
1707
+ if (min !== void 0) validate(path, (ctx) => dateError(ctx.value(), "min", min));
1708
+ }
1709
+ },
1710
+ {
1711
+ name: "date-max",
1712
+ declares: ({ field }) => field.type === "date" && field.max !== void 0,
1713
+ register: ({ path, field }) => {
1714
+ const max = field.type === "date" ? field.max : void 0;
1715
+ if (max !== void 0) validate(path, (ctx) => dateError(ctx.value(), "max", max));
1716
+ }
1717
+ },
1718
+ {
1719
+ name: "unique",
1720
+ declares: ({ field, schema }) => field.validators?.unique === true && TEXT_TYPES.has(field.type) && !!schema.endpoint.list,
1721
+ register: (ctx) => registerUniqueRule(ctx)
1722
+ }
1723
+ ];
1724
+ FIELD_VALIDATORS.map((entry) => entry.name);
1725
+ function buildFormSchema(schema, api, excludeKey) {
1726
+ return (root) => {
1727
+ applyRulesToFields({
1728
+ schema,
1729
+ api,
1730
+ excludeKey,
1731
+ root,
1732
+ container: root,
1733
+ siblings: schema.fields
1734
+ }, schema.fields);
1735
+ };
1736
+ }
1737
+ function applyRulesToFields(scope, fields) {
1738
+ for (const field of fields) {
1739
+ if (!isSupported("field", field.type)) continue;
1740
+ applyRulesToField(scope, field, pathAt(scope.container, field.key));
1741
+ }
1742
+ }
1743
+ function applyRulesToField(scope, field, path) {
1744
+ applyLifecycleRules(scope, field, path);
1745
+ const ctx = {
1746
+ path,
1747
+ field,
1748
+ schema: scope.schema,
1749
+ api: scope.api,
1750
+ excludeKey: scope.excludeKey
1751
+ };
1752
+ for (const entry of FIELD_VALIDATORS) if (entry.declares(ctx)) entry.register(ctx);
1753
+ switch (field.type) {
1754
+ case "group":
1755
+ applyRulesToFields({
1756
+ ...scope,
1757
+ container: path,
1758
+ siblings: field.fields
1759
+ }, field.fields);
1760
+ break;
1761
+ case "list":
1762
+ applyEach(path, (item) => applyRulesToFields({
1763
+ ...scope,
1764
+ container: item,
1765
+ siblings: field.itemFields
1766
+ }, field.itemFields));
1767
+ break;
1768
+ case "string-list": applyEach(path, (item) => applyStringItemRules(item, field.itemType));
1769
+ }
1770
+ }
1771
+ function modeAppliesTo(mode, isEdit) {
1772
+ return mode === "always" || mode === "update" && isEdit || mode === "create" && !isEdit;
1773
+ }
1774
+ function applyLifecycleRules(scope, field, path) {
1775
+ if (field.visibleWhen) {
1776
+ const condition = field.visibleWhen;
1777
+ hidden(path, { when: (ctx) => !evaluateCondition(condition, ctx.valueOf(conditionPath(scope, condition.field))) });
1778
+ }
1779
+ if (field.dependsOn) {
1780
+ const parentKey = field.dependsOn;
1781
+ const parentPath = pathAt(scope.siblings.some((candidate) => candidate.key === parentKey) ? scope.container : scope.root, parentKey);
1782
+ disabled(path, ({ valueOf }) => isEmptyValue(valueOf(parentPath)));
1783
+ }
1784
+ if (field.readonly) readonly(path, { when: () => true });
1785
+ if (field.readonlyOn && modeAppliesTo(field.readonlyOn, scope.excludeKey !== void 0)) readonly(path, { when: () => true });
1786
+ if (field.hiddenOn && modeAppliesTo(field.hiddenOn, scope.excludeKey !== void 0)) hidden(path, { when: () => true });
1787
+ if (field.readonlyWhen) {
1788
+ const condition = field.readonlyWhen;
1789
+ readonly(path, { when: (ctx) => evaluateCondition(condition, ctx.valueOf(conditionPath(scope, condition.field))) });
1790
+ }
1791
+ }
1792
+ function isEmptyValue(value) {
1793
+ if (value === void 0 || value === null || value === "") return true;
1794
+ return Array.isArray(value) && value.length === 0;
1795
+ }
1796
+ function conditionPath(scope, key) {
1797
+ return scope.siblings.some((field) => field.key === key) ? scope.container : scope.root;
1798
+ }
1799
+ function applyStringItemRules(path, itemType) {
1800
+ if (itemType === "email") registerEmailRule(path);
1801
+ if (itemType === "url") registerUrlRule(path);
1802
+ }
1803
+ function pathAt(parent, key) {
1804
+ const path = parent[key];
1805
+ if (path === void 0) throw new Error(`El formulario no contiene la ruta "${key}".`);
1806
+ return path;
1807
+ }
1808
+ function seedModel(schema) {
1809
+ return seedFields(schema.fields);
1810
+ }
1811
+ function buildPayload(schema, root) {
1812
+ const payload = {};
1813
+ collectPayload(root, schema.fields, payload, schema.positionField);
1814
+ return payload;
1815
+ }
1816
+ function collectPayload(tree, fields, payload, excludedKey) {
1817
+ for (const field of fields) {
1818
+ if (excludedKey !== void 0 && field.key === excludedKey) continue;
1819
+ const state = childTree(tree, field.key)();
1820
+ if (state.hidden()) continue;
1821
+ if (field.type === "group") {
1822
+ const nested = {};
1823
+ collectPayload(childTree(tree, field.key), field.fields, nested, void 0);
1824
+ payload[field.key] = nested;
1825
+ continue;
1826
+ }
1827
+ if (field.type === "list") {
1828
+ const items = listTree(tree, field.key);
1829
+ const raw = state.value() ?? [];
1830
+ const list = [];
1831
+ for (let index = 0; index < raw.length; index++) {
1832
+ const nested = {};
1833
+ collectPayload(itemTree(items, index), field.itemFields, nested, void 0);
1834
+ const positionKey = field.positionField;
1835
+ const position = positionKey !== void 0 ? raw[index]?.[positionKey] : void 0;
1836
+ if (positionKey !== void 0 && position !== void 0) nested[positionKey] = position;
1837
+ list.push(nested);
1838
+ }
1839
+ payload[field.key] = list;
1840
+ continue;
1841
+ }
1842
+ payload[field.key] = state.value();
1843
+ }
1844
+ }
1845
+ var DrawerForm = class DrawerForm {
1846
+ open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
1847
+ schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
1848
+ fields = input(void 0, ...ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []);
1849
+ mode = input("create", ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
1850
+ recordKey = input(null, ...ngDevMode ? [{ debugName: "recordKey" }] : /* istanbul ignore next */ []);
1851
+ title = input(null, ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
1852
+ size = input("md", ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
1853
+ saved = output();
1854
+ close = output();
1855
+ api = inject(ApiService);
1856
+ notifications = inject(NotificationService);
1857
+ errorMapper = inject(ErrorMapperService);
1858
+ injector = inject(Injector);
1859
+ destroyRef = inject(DestroyRef);
1860
+ tree = signal(null, ...ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []);
1861
+ status = signal("idle", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
1862
+ saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
1863
+ generalError = signal("", ...ngDevMode ? [{ debugName: "generalError" }] : /* istanbul ignore next */ []);
1864
+ fieldErrors = signal({}, ...ngDevMode ? [{ debugName: "fieldErrors" }] : /* istanbul ignore next */ []);
1865
+ loadError = signal("", ...ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []);
1866
+ visibleFields = computed(() => this.fields() ?? this.schema().fields, ...ngDevMode ? [{ debugName: "visibleFields" }] : /* istanbul ignore next */ []);
1867
+ drawerTitle = computed(() => {
1868
+ if (this.title() !== null) return this.title();
1869
+ const label = this.schema().label;
1870
+ return this.mode() === "create" ? `Crear ${label}` : `Editar ${label}`;
1871
+ }, ...ngDevMode ? [{ debugName: "drawerTitle" }] : /* istanbul ignore next */ []);
1872
+ ngOnInit() {
1873
+ this.build();
1874
+ }
1875
+ build() {
1876
+ const schema = this.schema();
1877
+ const recordKey = this.recordKey();
1878
+ const schemaFn = buildFormSchema(schema, this.api, recordKey ?? void 0);
1879
+ const tree = form(signal(seedModel(schema)), schemaFn, { injector: this.injector });
1880
+ if (this.mode() === "create" || recordKey === null) {
1881
+ this.tree.set(tree);
1882
+ this.status.set("ready");
1883
+ return;
1884
+ }
1885
+ this.status.set("loading");
1886
+ this.api.get(schema.endpoint, recordKey).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
1887
+ next: (record) => {
1888
+ tree().value.set({
1889
+ ...seedModel(schema),
1890
+ ...record
1891
+ });
1892
+ this.tree.set(tree);
1893
+ this.status.set("ready");
1894
+ },
1895
+ error: (error) => {
1896
+ this.loadError.set(this.errorMapper.messageOf(error));
1897
+ this.status.set("error");
1898
+ }
1899
+ });
1900
+ }
1901
+ async save() {
1902
+ const tree = this.tree();
1903
+ const schema = this.schema();
1904
+ if (tree === null || this.saving()) return;
1905
+ this.saving.set(true);
1906
+ this.generalError.set("");
1907
+ this.fieldErrors.set({});
1908
+ try {
1909
+ if (!await submit(tree, {
1910
+ action: async () => {
1911
+ await this.persist(tree, schema);
1912
+ return null;
1913
+ },
1914
+ onInvalid: () => this.markTouched(tree)
1915
+ }) || Object.keys(this.fieldErrors()).length > 0 || this.generalError() !== "") this.markTouched(tree);
1916
+ } finally {
1917
+ this.saving.set(false);
1918
+ }
1919
+ }
1920
+ markTouched(tree) {
1921
+ for (const field of this.visibleFields()) childTree(tree, field.key)().markAsTouched();
1922
+ }
1923
+ async persist(tree, schema) {
1924
+ const payload = buildPayload(schema, tree);
1925
+ try {
1926
+ if (this.mode() === "create") {
1927
+ const created = await firstValueFrom(this.api.create(schema.endpoint, payload));
1928
+ this.notifications.success("Registro creado.");
1929
+ this.saved.emit(created ?? {});
1930
+ this.close.emit();
1931
+ return;
1932
+ }
1933
+ const updated = await firstValueFrom(this.api.update(schema.endpoint, this.recordKey() ?? void 0, payload));
1934
+ this.notifications.success("Registro guardado.");
1935
+ this.saved.emit(updated ?? {});
1936
+ this.close.emit();
1937
+ } catch (error) {
1938
+ const mapped = this.errorMapper.map(error);
1939
+ this.fieldErrors.set(mapped.fields);
1940
+ const withoutFields = Object.keys(mapped.fields).length === 0;
1941
+ this.generalError.set(mapped.general ?? (withoutFields ? this.errorMapper.messageOf(error) : ""));
1942
+ }
1943
+ }
1944
+ static ɵfac = i0.ɵɵngDeclareFactory({
1945
+ minVersion: "12.0.0",
1946
+ version: "22.2.3",
1947
+ ngImport: i0,
1948
+ type: DrawerForm,
1949
+ deps: [],
1950
+ target: i0.ɵɵFactoryTarget.Component
1951
+ });
1952
+ static ɵcmp = i0.ɵɵngDeclareComponent({
1953
+ minVersion: "17.0.0",
1954
+ version: "22.2.3",
1955
+ type: DrawerForm,
1956
+ isStandalone: true,
1957
+ selector: "app-drawer-form",
1958
+ inputs: {
1959
+ open: {
1960
+ classPropertyName: "open",
1961
+ publicName: "open",
1962
+ isSignal: true,
1963
+ isRequired: false,
1964
+ transformFunction: null
1965
+ },
1966
+ schema: {
1967
+ classPropertyName: "schema",
1968
+ publicName: "schema",
1969
+ isSignal: true,
1970
+ isRequired: true,
1971
+ transformFunction: null
1972
+ },
1973
+ fields: {
1974
+ classPropertyName: "fields",
1975
+ publicName: "fields",
1976
+ isSignal: true,
1977
+ isRequired: false,
1978
+ transformFunction: null
1979
+ },
1980
+ mode: {
1981
+ classPropertyName: "mode",
1982
+ publicName: "mode",
1983
+ isSignal: true,
1984
+ isRequired: false,
1985
+ transformFunction: null
1986
+ },
1987
+ recordKey: {
1988
+ classPropertyName: "recordKey",
1989
+ publicName: "recordKey",
1990
+ isSignal: true,
1991
+ isRequired: false,
1992
+ transformFunction: null
1993
+ },
1994
+ title: {
1995
+ classPropertyName: "title",
1996
+ publicName: "title",
1997
+ isSignal: true,
1998
+ isRequired: false,
1999
+ transformFunction: null
2000
+ },
2001
+ size: {
2002
+ classPropertyName: "size",
2003
+ publicName: "size",
2004
+ isSignal: true,
2005
+ isRequired: false,
2006
+ transformFunction: null
2007
+ }
2008
+ },
2009
+ outputs: {
2010
+ saved: "saved",
2011
+ close: "close"
2012
+ },
2013
+ ngImport: i0,
2014
+ template: `
2015
+ <app-drawer [open]="open()" [title]="drawerTitle()" [size]="size()" (close)="close.emit()">
2016
+ @if (status() === 'loading') {
2017
+ <app-skeleton [lines]="4" />
2018
+ } @else if (status() === 'error') {
2019
+ <p role="alert" class="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">
2020
+ {{ loadError() }}
2021
+ </p>
2022
+ } @else if (tree(); as formTree) {
2023
+ @if (generalError()) {
2024
+ <p
2025
+ role="alert"
2026
+ class="mb-4 rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger"
2027
+ data-drawer-error
2028
+ >
2029
+ {{ generalError() }}
2030
+ </p>
2031
+ }
2032
+ @for (field of visibleFields(); track field.key) {
2033
+ <div class="mb-4">
2034
+ <app-field-host
2035
+ [field]="field"
2036
+ [tree]="formTree"
2037
+ [exists]="mode() === 'edit'"
2038
+ [serverErrors]="fieldErrors()"
2039
+ [resource]="schema().id"
2040
+ />
2041
+ </div>
2042
+ }
2043
+ }
2044
+
2045
+ <div drawer-actions class="flex justify-end gap-2">
2046
+ <button app-button variant="outline" type="button" (click)="close.emit()">Cancelar</button>
2047
+ <button app-button type="button" [loading]="saving()" (click)="save()">Guardar</button>
2048
+ </div>
2049
+ </app-drawer>
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: "component",
2067
+ type: Drawer,
2068
+ selector: "app-drawer",
2069
+ inputs: [
2070
+ "open",
2071
+ "title",
2072
+ "subtitle",
2073
+ "size",
2074
+ "closeLabel"
2075
+ ],
2076
+ outputs: ["close"]
2077
+ },
2078
+ {
2079
+ kind: "component",
2080
+ type: FieldHost,
2081
+ selector: "app-field-host",
2082
+ inputs: [
2083
+ "field",
2084
+ "tree",
2085
+ "exists",
2086
+ "serverErrors",
2087
+ "pathPrefix",
2088
+ "resource"
2089
+ ]
2090
+ },
2091
+ {
2092
+ kind: "component",
2093
+ type: Skeleton,
2094
+ selector: "app-skeleton",
2095
+ inputs: [
2096
+ "lines",
2097
+ "height",
2098
+ "width"
2099
+ ]
2100
+ }
2101
+ ]
2102
+ });
2103
+ };
2104
+ i0.ɵɵngDeclareClassMetadata({
2105
+ minVersion: "12.0.0",
2106
+ version: "22.2.3",
2107
+ ngImport: i0,
2108
+ type: DrawerForm,
2109
+ decorators: [{
2110
+ type: Component,
2111
+ args: [{
2112
+ selector: "app-drawer-form",
2113
+ imports: [
2114
+ Button,
2115
+ Drawer,
2116
+ FieldHost,
2117
+ Skeleton
2118
+ ],
2119
+ template: `
2120
+ <app-drawer [open]="open()" [title]="drawerTitle()" [size]="size()" (close)="close.emit()">
2121
+ @if (status() === 'loading') {
2122
+ <app-skeleton [lines]="4" />
2123
+ } @else if (status() === 'error') {
2124
+ <p role="alert" class="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">
2125
+ {{ loadError() }}
2126
+ </p>
2127
+ } @else if (tree(); as formTree) {
2128
+ @if (generalError()) {
2129
+ <p
2130
+ role="alert"
2131
+ class="mb-4 rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger"
2132
+ data-drawer-error
2133
+ >
2134
+ {{ generalError() }}
2135
+ </p>
2136
+ }
2137
+ @for (field of visibleFields(); track field.key) {
2138
+ <div class="mb-4">
2139
+ <app-field-host
2140
+ [field]="field"
2141
+ [tree]="formTree"
2142
+ [exists]="mode() === 'edit'"
2143
+ [serverErrors]="fieldErrors()"
2144
+ [resource]="schema().id"
2145
+ />
2146
+ </div>
2147
+ }
2148
+ }
2149
+
2150
+ <div drawer-actions class="flex justify-end gap-2">
2151
+ <button app-button variant="outline" type="button" (click)="close.emit()">Cancelar</button>
2152
+ <button app-button type="button" [loading]="saving()" (click)="save()">Guardar</button>
2153
+ </div>
2154
+ </app-drawer>
2155
+ `
2156
+ }]
2157
+ }],
2158
+ propDecorators: {
2159
+ open: [{
2160
+ type: i0.Input,
2161
+ args: [{
2162
+ isSignal: true,
2163
+ alias: "open",
2164
+ required: false
2165
+ }]
2166
+ }],
2167
+ schema: [{
2168
+ type: i0.Input,
2169
+ args: [{
2170
+ isSignal: true,
2171
+ alias: "schema",
2172
+ required: true
2173
+ }]
2174
+ }],
2175
+ fields: [{
2176
+ type: i0.Input,
2177
+ args: [{
2178
+ isSignal: true,
2179
+ alias: "fields",
2180
+ required: false
2181
+ }]
2182
+ }],
2183
+ mode: [{
2184
+ type: i0.Input,
2185
+ args: [{
2186
+ isSignal: true,
2187
+ alias: "mode",
2188
+ required: false
2189
+ }]
2190
+ }],
2191
+ recordKey: [{
2192
+ type: i0.Input,
2193
+ args: [{
2194
+ isSignal: true,
2195
+ alias: "recordKey",
2196
+ required: false
2197
+ }]
2198
+ }],
2199
+ title: [{
2200
+ type: i0.Input,
2201
+ args: [{
2202
+ isSignal: true,
2203
+ alias: "title",
2204
+ required: false
2205
+ }]
2206
+ }],
2207
+ size: [{
2208
+ type: i0.Input,
2209
+ args: [{
2210
+ isSignal: true,
2211
+ alias: "size",
2212
+ required: false
2213
+ }]
2214
+ }],
2215
+ saved: [{
2216
+ type: i0.Output,
2217
+ args: ["saved"]
2218
+ }],
2219
+ close: [{
2220
+ type: i0.Output,
2221
+ args: ["close"]
2222
+ }]
2223
+ }
2224
+ });
2225
+ function collectDependsOn(fields, container = []) {
2226
+ const links = [];
2227
+ for (const field of fields) {
2228
+ if (field.type === "group") {
2229
+ links.push(...collectDependsOn(field.fields, [...container, field.key]));
2230
+ continue;
2231
+ }
2232
+ if (field.dependsOn === void 0) continue;
2233
+ links.push({
2234
+ container,
2235
+ childKey: field.key,
2236
+ parentKey: field.dependsOn,
2237
+ resetValue: seedFieldValue(field),
2238
+ id: `${container.join(".")}.${field.key}<-${field.dependsOn}`
2239
+ });
2240
+ }
2241
+ return links;
2242
+ }
2243
+ var CopyShare = class CopyShare {
2244
+ url = input.required(...ngDevMode ? [{ debugName: "url" }] : /* istanbul ignore next */ []);
2245
+ label = input("Copiar enlace", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
2246
+ shareText = input("", ...ngDevMode ? [{ debugName: "shareText" }] : /* istanbul ignore next */ []);
2247
+ notify = inject(NotificationService);
2248
+ canShare = computed(() => typeof navigator !== "undefined" && typeof navigator.share === "function", ...ngDevMode ? [{ debugName: "canShare" }] : /* istanbul ignore next */ []);
2249
+ waUrl = computed(() => {
2250
+ const text = buildShareMessage(this.url(), this.shareText());
2251
+ return `https://wa.me/?text=${encodeURIComponent(text)}`;
2252
+ }, ...ngDevMode ? [{ debugName: "waUrl" }] : /* istanbul ignore next */ []);
2253
+ async copy() {
2254
+ if (await writeClipboard(buildShareMessage(this.url(), this.shareText()))) this.notify.success("Mensaje copiado.");
2255
+ else this.notify.error("No se pudo copiar el mensaje.");
2256
+ }
2257
+ async share() {
2258
+ if (!this.canShare()) {
2259
+ this.shareWhatsApp();
2260
+ return;
2261
+ }
2262
+ try {
2263
+ await navigator.share({
2264
+ url: this.url(),
2265
+ text: this.shareText()
2266
+ });
2267
+ } catch {}
2268
+ }
2269
+ shareWhatsApp() {
2270
+ window.open(this.waUrl(), "_blank", "noopener,noreferrer");
2271
+ }
2272
+ static ɵfac = i0.ɵɵngDeclareFactory({
2273
+ minVersion: "12.0.0",
2274
+ version: "22.2.3",
2275
+ ngImport: i0,
2276
+ type: CopyShare,
2277
+ deps: [],
2278
+ target: i0.ɵɵFactoryTarget.Component
2279
+ });
2280
+ static ɵcmp = i0.ɵɵngDeclareComponent({
2281
+ minVersion: "17.0.0",
2282
+ version: "22.2.3",
2283
+ type: CopyShare,
2284
+ isStandalone: true,
2285
+ selector: "app-copy-share",
2286
+ inputs: {
2287
+ url: {
2288
+ classPropertyName: "url",
2289
+ publicName: "url",
2290
+ isSignal: true,
2291
+ isRequired: true,
2292
+ transformFunction: null
2293
+ },
2294
+ label: {
2295
+ classPropertyName: "label",
2296
+ publicName: "label",
2297
+ isSignal: true,
2298
+ isRequired: false,
2299
+ transformFunction: null
2300
+ },
2301
+ shareText: {
2302
+ classPropertyName: "shareText",
2303
+ publicName: "shareText",
2304
+ isSignal: true,
2305
+ isRequired: false,
2306
+ transformFunction: null
2307
+ }
2308
+ },
2309
+ ngImport: i0,
2310
+ template: `
2311
+ <div class="flex flex-wrap items-center gap-2">
2312
+ <button app-button type="button" variant="outline" (click)="copy()">
2313
+ <svg lucideCopy size="16" />
2314
+ {{ label() }}
2315
+ </button>
2316
+
2317
+ @if (canShare()) {
2318
+ <button app-button type="button" variant="ghost" (click)="share()">
2319
+ <svg lucideShare size="16" />
2320
+ Compartir
2321
+ </button>
2322
+ } @else {
2323
+ <button app-button type="button" variant="ghost" (click)="shareWhatsApp()">
2324
+ <svg lucideShare size="16" />
2325
+ WhatsApp
2326
+ </button>
2327
+ }
2328
+ </div>
2329
+ `,
2330
+ isInline: true,
2331
+ dependencies: [
2332
+ {
2333
+ kind: "component",
2334
+ type: Button,
2335
+ selector: "button[app-button]",
2336
+ inputs: [
2337
+ "variant",
2338
+ "size",
2339
+ "type",
2340
+ "disabled",
2341
+ "loading"
2342
+ ]
2343
+ },
2344
+ {
2345
+ kind: "component",
2346
+ type: LucideCopy,
2347
+ selector: "svg[lucideCopy]"
2348
+ },
2349
+ {
2350
+ kind: "component",
2351
+ type: LucideShare,
2352
+ selector: "svg[lucideShare]"
2353
+ }
2354
+ ]
2355
+ });
2356
+ };
2357
+ i0.ɵɵngDeclareClassMetadata({
2358
+ minVersion: "12.0.0",
2359
+ version: "22.2.3",
2360
+ ngImport: i0,
2361
+ type: CopyShare,
2362
+ decorators: [{
2363
+ type: Component,
2364
+ args: [{
2365
+ selector: "app-copy-share",
2366
+ imports: [
2367
+ Button,
2368
+ LucideCopy,
2369
+ LucideShare
2370
+ ],
2371
+ template: `
2372
+ <div class="flex flex-wrap items-center gap-2">
2373
+ <button app-button type="button" variant="outline" (click)="copy()">
2374
+ <svg lucideCopy size="16" />
2375
+ {{ label() }}
2376
+ </button>
2377
+
2378
+ @if (canShare()) {
2379
+ <button app-button type="button" variant="ghost" (click)="share()">
2380
+ <svg lucideShare size="16" />
2381
+ Compartir
2382
+ </button>
2383
+ } @else {
2384
+ <button app-button type="button" variant="ghost" (click)="shareWhatsApp()">
2385
+ <svg lucideShare size="16" />
2386
+ WhatsApp
2387
+ </button>
2388
+ }
2389
+ </div>
2390
+ `
2391
+ }]
2392
+ }],
2393
+ propDecorators: {
2394
+ url: [{
2395
+ type: i0.Input,
2396
+ args: [{
2397
+ isSignal: true,
2398
+ alias: "url",
2399
+ required: true
2400
+ }]
2401
+ }],
2402
+ label: [{
2403
+ type: i0.Input,
2404
+ args: [{
2405
+ isSignal: true,
2406
+ alias: "label",
2407
+ required: false
2408
+ }]
2409
+ }],
2410
+ shareText: [{
2411
+ type: i0.Input,
2412
+ args: [{
2413
+ isSignal: true,
2414
+ alias: "shareText",
2415
+ required: false
2416
+ }]
2417
+ }]
2418
+ }
2419
+ });
2420
+ let titleCounter = 0;
2421
+ var Modal = class Modal {
2422
+ open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
2423
+ title = input.required(...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
2424
+ closeLabel = input("Cerrar", ...ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []);
2425
+ close = output();
2426
+ titleId = `modal-title-${++titleCounter}`;
2427
+ dialogRef = viewChild("dialog", ...ngDevMode ? [{ debugName: "dialogRef" }] : /* istanbul ignore next */ []);
2428
+ lastFocused = null;
2429
+ focusEffect = afterRenderEffect(() => {
2430
+ const open = this.open();
2431
+ const dialog = this.dialogRef();
2432
+ if (open && dialog) {
2433
+ const active = document.activeElement;
2434
+ if (active !== dialog.nativeElement && this.lastFocused === null) this.lastFocused = active instanceof HTMLElement ? active : null;
2435
+ dialog.nativeElement.focus();
2436
+ } else if (!open && this.lastFocused !== null) {
2437
+ this.lastFocused.focus();
2438
+ this.lastFocused = null;
2439
+ }
2440
+ }, ...ngDevMode ? [{ debugName: "focusEffect" }] : /* istanbul ignore next */ []);
2441
+ ngOnDestroy() {
2442
+ if (this.lastFocused?.isConnected) this.lastFocused.focus();
2443
+ this.lastFocused = null;
2444
+ }
2445
+ onEscape() {
2446
+ if (this.open()) this.close.emit();
2447
+ }
2448
+ onTab(rawEvent) {
2449
+ const event = rawEvent;
2450
+ const dialog = this.dialogRef()?.nativeElement;
2451
+ if (!this.open() || !dialog) return;
2452
+ const focusable = Array.from(dialog.querySelectorAll(FOCUSABLE)).filter((element) => isVisibleNow(element));
2453
+ if (focusable.length === 0) {
2454
+ event.preventDefault();
2455
+ dialog.focus();
2456
+ return;
2457
+ }
2458
+ const first = focusable[0];
2459
+ const last = focusable[focusable.length - 1];
2460
+ const active = document.activeElement;
2461
+ if (event.shiftKey && (active === first || active === dialog)) {
2462
+ event.preventDefault();
2463
+ last.focus();
2464
+ } else if (!event.shiftKey && active === last) {
2465
+ event.preventDefault();
2466
+ first.focus();
2467
+ }
2468
+ }
2469
+ static ɵfac = i0.ɵɵngDeclareFactory({
2470
+ minVersion: "12.0.0",
2471
+ version: "22.2.3",
2472
+ ngImport: i0,
2473
+ type: Modal,
2474
+ deps: [],
2475
+ target: i0.ɵɵFactoryTarget.Component
2476
+ });
2477
+ static ɵcmp = i0.ɵɵngDeclareComponent({
2478
+ minVersion: "17.0.0",
2479
+ version: "22.2.3",
2480
+ type: Modal,
2481
+ isStandalone: true,
2482
+ selector: "app-modal",
2483
+ inputs: {
2484
+ open: {
2485
+ classPropertyName: "open",
2486
+ publicName: "open",
2487
+ isSignal: true,
2488
+ isRequired: false,
2489
+ transformFunction: null
2490
+ },
2491
+ title: {
2492
+ classPropertyName: "title",
2493
+ publicName: "title",
2494
+ isSignal: true,
2495
+ isRequired: true,
2496
+ transformFunction: null
2497
+ },
2498
+ closeLabel: {
2499
+ classPropertyName: "closeLabel",
2500
+ publicName: "closeLabel",
2501
+ isSignal: true,
2502
+ isRequired: false,
2503
+ transformFunction: null
2504
+ }
2505
+ },
2506
+ outputs: { close: "close" },
2507
+ host: { listeners: {
2508
+ "document:keydown.escape": "onEscape()",
2509
+ "document:keydown.tab": "onTab($event)",
2510
+ "document:keydown.shift.tab": "onTab($event)"
2511
+ } },
2512
+ viewQueries: [{
2513
+ propertyName: "dialogRef",
2514
+ first: true,
2515
+ predicate: ["dialog"],
2516
+ descendants: true,
2517
+ isSignal: true
2518
+ }],
2519
+ ngImport: i0,
2520
+ template: `
2521
+ @if (open()) {
2522
+ <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
2523
+ <div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
2524
+ <div
2525
+ #dialog
2526
+ role="dialog"
2527
+ aria-modal="true"
2528
+ [attr.aria-labelledby]="titleId"
2529
+ tabindex="-1"
2530
+ class="relative w-full max-w-lg rounded-xl bg-surface-raised p-6 shadow-xl focus:outline-none"
2531
+ >
2532
+ <div class="mb-4 flex items-start justify-between gap-4">
2533
+ <h2 [id]="titleId" class="font-display text-section font-semibold">
2534
+ {{ title() }}
2535
+ </h2>
2536
+ <button
2537
+ type="button"
2538
+ 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"
2539
+ [attr.aria-label]="closeLabel()"
2540
+ (click)="close.emit()"
2541
+ >
2542
+ <svg lucideX size="18" />
2543
+ </button>
2544
+ </div>
2545
+ <ng-content />
2546
+ </div>
2547
+ </div>
2548
+ }
2549
+ `,
2550
+ isInline: true,
2551
+ dependencies: [{
2552
+ kind: "component",
2553
+ type: LucideX,
2554
+ selector: "svg[lucideX]"
2555
+ }]
2556
+ });
2557
+ };
2558
+ i0.ɵɵngDeclareClassMetadata({
2559
+ minVersion: "12.0.0",
2560
+ version: "22.2.3",
2561
+ ngImport: i0,
2562
+ type: Modal,
2563
+ decorators: [{
2564
+ type: Component,
2565
+ args: [{
2566
+ selector: "app-modal",
2567
+ imports: [LucideX],
2568
+ host: {
2569
+ "(document:keydown.escape)": "onEscape()",
2570
+ "(document:keydown.tab)": "onTab($event)",
2571
+ "(document:keydown.shift.tab)": "onTab($event)"
2572
+ },
2573
+ template: `
2574
+ @if (open()) {
2575
+ <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
2576
+ <div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
2577
+ <div
2578
+ #dialog
2579
+ role="dialog"
2580
+ aria-modal="true"
2581
+ [attr.aria-labelledby]="titleId"
2582
+ tabindex="-1"
2583
+ class="relative w-full max-w-lg rounded-xl bg-surface-raised p-6 shadow-xl focus:outline-none"
2584
+ >
2585
+ <div class="mb-4 flex items-start justify-between gap-4">
2586
+ <h2 [id]="titleId" class="font-display text-section font-semibold">
2587
+ {{ title() }}
2588
+ </h2>
2589
+ <button
2590
+ type="button"
2591
+ 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"
2592
+ [attr.aria-label]="closeLabel()"
2593
+ (click)="close.emit()"
2594
+ >
2595
+ <svg lucideX size="18" />
2596
+ </button>
2597
+ </div>
2598
+ <ng-content />
2599
+ </div>
2600
+ </div>
2601
+ }
2602
+ `
2603
+ }]
2604
+ }],
2605
+ propDecorators: {
2606
+ open: [{
2607
+ type: i0.Input,
2608
+ args: [{
2609
+ isSignal: true,
2610
+ alias: "open",
2611
+ required: false
2612
+ }]
2613
+ }],
2614
+ title: [{
2615
+ type: i0.Input,
2616
+ args: [{
2617
+ isSignal: true,
2618
+ alias: "title",
2619
+ required: true
2620
+ }]
2621
+ }],
2622
+ closeLabel: [{
2623
+ type: i0.Input,
2624
+ args: [{
2625
+ isSignal: true,
2626
+ alias: "closeLabel",
2627
+ required: false
2628
+ }]
2629
+ }],
2630
+ close: [{
2631
+ type: i0.Output,
2632
+ args: ["close"]
2633
+ }],
2634
+ dialogRef: [{
2635
+ type: i0.ViewChild,
2636
+ args: ["dialog", { isSignal: true }]
2637
+ }]
2638
+ }
2639
+ });
2640
+ var PostCreateModal = class PostCreateModal {
2641
+ schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
2642
+ config = input.required(...ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []);
2643
+ record = input.required(...ngDevMode ? [{ debugName: "record" }] : /* istanbul ignore next */ []);
2644
+ configValues = input({}, ...ngDevMode ? [{ debugName: "configValues" }] : /* istanbul ignore next */ []);
2645
+ closed = output();
2646
+ configService = inject(ConfigService);
2647
+ constructor() {
2648
+ this.configService.ensureLoaded();
2649
+ }
2650
+ context = computed(() => ({
2651
+ ...this.configService.buildInterpolationContext(this.record()),
2652
+ ...this.configValues()
2653
+ }), ...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
2654
+ title = computed(() => {
2655
+ const raw = this.config().title || "¡Registro Creado con Éxito!";
2656
+ return interpolateTemplate(raw, this.context());
2657
+ }, ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
2658
+ description = computed(() => {
2659
+ const raw = this.config().description || "El registro fue creado correctamente y ya se encuentra guardado en el sistema.";
2660
+ return interpolateTemplate(raw, this.context());
2661
+ }, ...ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []);
2662
+ summaryItems = computed(() => {
2663
+ const fields = this.config().summaryFields || [];
2664
+ const rec = this.record();
2665
+ const schemaFields = this.schema().fields || [];
2666
+ return fields.map((key) => {
2667
+ const label = schemaFields.find((f) => f.key === key)?.label || key.replace(/_/g, " ");
2668
+ const rawVal = rec[key];
2669
+ return {
2670
+ key,
2671
+ label,
2672
+ value: rawVal === null || rawVal === void 0 || rawVal === "" ? "—" : String(rawVal)
2673
+ };
2674
+ });
2675
+ }, ...ngDevMode ? [{ debugName: "summaryItems" }] : /* istanbul ignore next */ []);
2676
+ copyShareAction = computed(() => (this.config().actions || []).find(isShareAction) ?? null, ...ngDevMode ? [{ debugName: "copyShareAction" }] : /* istanbul ignore next */ []);
2677
+ shareOf(action) {
2678
+ return resolveShare(action, this.context());
2679
+ }
2680
+ static ɵfac = i0.ɵɵngDeclareFactory({
2681
+ minVersion: "12.0.0",
2682
+ version: "22.2.3",
2683
+ ngImport: i0,
2684
+ type: PostCreateModal,
2685
+ deps: [],
2686
+ target: i0.ɵɵFactoryTarget.Component
2687
+ });
2688
+ static ɵcmp = i0.ɵɵngDeclareComponent({
2689
+ minVersion: "17.0.0",
2690
+ version: "22.2.3",
2691
+ type: PostCreateModal,
2692
+ isStandalone: true,
2693
+ selector: "app-post-create-modal",
2694
+ inputs: {
2695
+ schema: {
2696
+ classPropertyName: "schema",
2697
+ publicName: "schema",
2698
+ isSignal: true,
2699
+ isRequired: true,
2700
+ transformFunction: null
2701
+ },
2702
+ config: {
2703
+ classPropertyName: "config",
2704
+ publicName: "config",
2705
+ isSignal: true,
2706
+ isRequired: true,
2707
+ transformFunction: null
2708
+ },
2709
+ record: {
2710
+ classPropertyName: "record",
2711
+ publicName: "record",
2712
+ isSignal: true,
2713
+ isRequired: true,
2714
+ transformFunction: null
2715
+ },
2716
+ configValues: {
2717
+ classPropertyName: "configValues",
2718
+ publicName: "configValues",
2719
+ isSignal: true,
2720
+ isRequired: false,
2721
+ transformFunction: null
2722
+ }
2723
+ },
2724
+ outputs: { closed: "closed" },
2725
+ ngImport: i0,
2726
+ template: `
2727
+ <app-modal [open]="true" [title]="title()" (close)="closed.emit()">
2728
+ <div class="flex flex-col gap-4">
2729
+ <!-- Encabezado con icono de éxito -->
2730
+ <div class="flex items-center gap-3">
2731
+ <div
2732
+ class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500"
2733
+ >
2734
+ <svg lucideCheckCircle2 size="22" />
2735
+ </div>
2736
+ <div>
2737
+ <p class="text-sm text-neutral-light">{{ description() }}</p>
2738
+ </div>
2739
+ </div>
2740
+
2741
+ <!-- Tarjeta de campos de resumen -->
2742
+ @if (summaryItems().length > 0) {
2743
+ <div class="rounded-lg border border-neutral/15 bg-surface p-3.5">
2744
+ <dl class="grid grid-cols-1 gap-x-4 gap-y-2.5 sm:grid-cols-2">
2745
+ @for (item of summaryItems(); track item.key) {
2746
+ <div class="flex flex-col">
2747
+ <dt class="text-xs font-medium text-neutral">{{ item.label }}</dt>
2748
+ <dd class="text-sm font-semibold text-neutral-light truncate">
2749
+ {{ item.value }}
2750
+ </dd>
2751
+ </div>
2752
+ }
2753
+ </dl>
2754
+ </div>
2755
+ }
2756
+
2757
+ <!-- Acciones declaradas (ej. CopyShare) -->
2758
+ @if (copyShareAction(); as action) {
2759
+ <div class="rounded-lg border border-primary/20 bg-primary/5 p-3.5">
2760
+ <p class="mb-2 text-xs font-medium text-primary">Enlace de Seguimiento</p>
2761
+ @let share = shareOf(action);
2762
+ <app-copy-share
2763
+ [url]="share.url"
2764
+ [shareText]="share.shareText"
2765
+ [label]="action.label"
2766
+ />
2767
+ </div>
2768
+ }
2769
+
2770
+ <!-- Botón de cierre primario -->
2771
+ <div class="mt-2 flex justify-end">
2772
+ <button app-button type="button" variant="primary" (click)="closed.emit()">
2773
+ Continuar
2774
+ </button>
2775
+ </div>
2776
+ </div>
2777
+ </app-modal>
2778
+ `,
2779
+ isInline: true,
2780
+ dependencies: [
2781
+ {
2782
+ kind: "component",
2783
+ type: Modal,
2784
+ selector: "app-modal",
2785
+ inputs: [
2786
+ "open",
2787
+ "title",
2788
+ "closeLabel"
2789
+ ],
2790
+ outputs: ["close"]
2791
+ },
2792
+ {
2793
+ kind: "component",
2794
+ type: Button,
2795
+ selector: "button[app-button]",
2796
+ inputs: [
2797
+ "variant",
2798
+ "size",
2799
+ "type",
2800
+ "disabled",
2801
+ "loading"
2802
+ ]
2803
+ },
2804
+ {
2805
+ kind: "component",
2806
+ type: CopyShare,
2807
+ selector: "app-copy-share",
2808
+ inputs: [
2809
+ "url",
2810
+ "label",
2811
+ "shareText"
2812
+ ]
2813
+ },
2814
+ {
2815
+ kind: "component",
2816
+ type: LucideCheckCircle2,
2817
+ selector: "svg[lucideCircleCheck], svg[lucideCheckCircle2]"
2818
+ }
2819
+ ]
2820
+ });
2821
+ };
2822
+ i0.ɵɵngDeclareClassMetadata({
2823
+ minVersion: "12.0.0",
2824
+ version: "22.2.3",
2825
+ ngImport: i0,
2826
+ type: PostCreateModal,
2827
+ decorators: [{
2828
+ type: Component,
2829
+ args: [{
2830
+ selector: "app-post-create-modal",
2831
+ imports: [
2832
+ Modal,
2833
+ Button,
2834
+ CopyShare,
2835
+ LucideCheckCircle2
2836
+ ],
2837
+ template: `
2838
+ <app-modal [open]="true" [title]="title()" (close)="closed.emit()">
2839
+ <div class="flex flex-col gap-4">
2840
+ <!-- Encabezado con icono de éxito -->
2841
+ <div class="flex items-center gap-3">
2842
+ <div
2843
+ class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500"
2844
+ >
2845
+ <svg lucideCheckCircle2 size="22" />
2846
+ </div>
2847
+ <div>
2848
+ <p class="text-sm text-neutral-light">{{ description() }}</p>
2849
+ </div>
2850
+ </div>
2851
+
2852
+ <!-- Tarjeta de campos de resumen -->
2853
+ @if (summaryItems().length > 0) {
2854
+ <div class="rounded-lg border border-neutral/15 bg-surface p-3.5">
2855
+ <dl class="grid grid-cols-1 gap-x-4 gap-y-2.5 sm:grid-cols-2">
2856
+ @for (item of summaryItems(); track item.key) {
2857
+ <div class="flex flex-col">
2858
+ <dt class="text-xs font-medium text-neutral">{{ item.label }}</dt>
2859
+ <dd class="text-sm font-semibold text-neutral-light truncate">
2860
+ {{ item.value }}
2861
+ </dd>
2862
+ </div>
2863
+ }
2864
+ </dl>
2865
+ </div>
2866
+ }
2867
+
2868
+ <!-- Acciones declaradas (ej. CopyShare) -->
2869
+ @if (copyShareAction(); as action) {
2870
+ <div class="rounded-lg border border-primary/20 bg-primary/5 p-3.5">
2871
+ <p class="mb-2 text-xs font-medium text-primary">Enlace de Seguimiento</p>
2872
+ @let share = shareOf(action);
2873
+ <app-copy-share
2874
+ [url]="share.url"
2875
+ [shareText]="share.shareText"
2876
+ [label]="action.label"
2877
+ />
2878
+ </div>
2879
+ }
2880
+
2881
+ <!-- Botón de cierre primario -->
2882
+ <div class="mt-2 flex justify-end">
2883
+ <button app-button type="button" variant="primary" (click)="closed.emit()">
2884
+ Continuar
2885
+ </button>
2886
+ </div>
2887
+ </div>
2888
+ </app-modal>
2889
+ `
2890
+ }]
2891
+ }],
2892
+ ctorParameters: () => [],
2893
+ propDecorators: {
2894
+ schema: [{
2895
+ type: i0.Input,
2896
+ args: [{
2897
+ isSignal: true,
2898
+ alias: "schema",
2899
+ required: true
2900
+ }]
2901
+ }],
2902
+ config: [{
2903
+ type: i0.Input,
2904
+ args: [{
2905
+ isSignal: true,
2906
+ alias: "config",
2907
+ required: true
2908
+ }]
2909
+ }],
2910
+ record: [{
2911
+ type: i0.Input,
2912
+ args: [{
2913
+ isSignal: true,
2914
+ alias: "record",
2915
+ required: true
2916
+ }]
2917
+ }],
2918
+ configValues: [{
2919
+ type: i0.Input,
2920
+ args: [{
2921
+ isSignal: true,
2922
+ alias: "configValues",
2923
+ required: false
2924
+ }]
2925
+ }],
2926
+ closed: [{
2927
+ type: i0.Output,
2928
+ args: ["closed"]
2929
+ }]
2930
+ }
2931
+ });
2932
+ var form_view_exports = /* @__PURE__ */ __exportAll({ FormView: () => FormView });
2933
+ const TAB_ACTIVE = "border-b-2 border-primary px-3 py-2 text-sm font-medium text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
2934
+ const TAB_INACTIVE = "border-b-2 border-transparent px-3 py-2 text-sm text-neutral hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
2935
+ const DRAFT_DEBOUNCE_MS = 800;
2936
+ function readTimestamp(record) {
2937
+ if (!record) return;
2938
+ for (const key of [
2939
+ "updated_at",
2940
+ "updatedAt",
2941
+ "updated"
2942
+ ]) {
2943
+ const raw = record[key];
2944
+ if (typeof raw === "number") return raw < 0xe8d4a51000 ? raw * 1e3 : raw;
2945
+ if (typeof raw === "string") {
2946
+ const parsed = Date.parse(raw);
2947
+ if (!Number.isNaN(parsed)) return parsed;
2948
+ }
2949
+ }
2950
+ }
2951
+ var FormView = class FormView {
2952
+ relationDrawer = inject(RelationDrawerService);
2953
+ route = inject(ActivatedRoute);
2954
+ router = inject(Router);
2955
+ nav = inject(ViewNavigationService);
2956
+ api = inject(ApiService);
2957
+ auth = inject(AuthService);
2958
+ notifications = inject(NotificationService);
2959
+ errorMapper = inject(ErrorMapperService);
2960
+ injector = inject(Injector);
2961
+ appRef = inject(ApplicationRef);
2962
+ params = toSignal(this.route.paramMap, { requireSync: true });
2963
+ routeData = toSignal(this.route.data ?? of({}), { requireSync: true });
2964
+ schema = computed(() => {
2965
+ const resourceId = this.routeData()["resourceId"] ?? this.params().get("id");
2966
+ return getSchema(typeof resourceId === "string" ? resourceId : "");
2967
+ }, ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
2968
+ resourceId = computed(() => this.schema()?.id ?? "", ...ngDevMode ? [{ debugName: "resourceId" }] : /* istanbul ignore next */ []);
2969
+ createdWithSummary = signal(null, ...ngDevMode ? [{ debugName: "createdWithSummary" }] : /* istanbul ignore next */ []);
2970
+ mode = computed(() => {
2971
+ const schema = this.schema();
2972
+ if (!schema) return "single";
2973
+ if (this.params().get("key") !== null) return "edit";
2974
+ return schema.kind === "singleton" ? "single" : "create";
2975
+ }, ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
2976
+ heading = computed(() => {
2977
+ const schema = this.schema();
2978
+ if (!schema) return "";
2979
+ switch (this.mode()) {
2980
+ case "create": return `Nueva ${schema.label}`;
2981
+ case "edit": return `Editar ${schema.label}`;
2982
+ case "single": return schema.labelPlural;
2983
+ }
2984
+ }, ...ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []);
2985
+ status = signal("loading", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
2986
+ errorCarga = void 0;
2987
+ errorDeCarga() {
2988
+ return this.status() === "error" ? this.errorCarga : void 0;
2989
+ }
2990
+ saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
2991
+ serverErrors = signal({ fields: {} }, ...ngDevMode ? [{ debugName: "serverErrors" }] : /* istanbul ignore next */ []);
2992
+ activeSection = signal(null, ...ngDevMode ? [{ debugName: "activeSection" }] : /* istanbul ignore next */ []);
2993
+ attemptedSteps = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "attemptedSteps" }] : /* istanbul ignore next */ []);
2994
+ openSections = signal({}, ...ngDevMode ? [{ debugName: "openSections" }] : /* istanbul ignore next */ []);
2995
+ formTree = signal(null, ...ngDevMode ? [{ debugName: "formTree" }] : /* istanbul ignore next */ []);
2996
+ loadEpoch = 0;
2997
+ draftOffer = signal(null, ...ngDevMode ? [{ debugName: "draftOffer" }] : /* istanbul ignore next */ []);
2998
+ loadedSnapshot = "";
2999
+ loadedRecord = null;
3000
+ dependsOnSnapshot = /* @__PURE__ */ new Map();
3001
+ dependsOnTree = null;
3002
+ submitted = signal(false, ...ngDevMode ? [{ debugName: "submitted" }] : /* istanbul ignore next */ []);
3003
+ constructor() {
3004
+ this.route.paramMap.subscribe(() => this.rebuild());
3005
+ effect((onCleanup) => {
3006
+ const tree = this.formTree();
3007
+ if (!tree) return;
3008
+ const value = tree().value();
3009
+ if (this.status() !== "ready" || JSON.stringify(value) === this.loadedSnapshot) return;
3010
+ const timer = setTimeout(() => this.saveDraft(value), DRAFT_DEBOUNCE_MS);
3011
+ onCleanup(() => clearTimeout(timer));
3012
+ });
3013
+ effect(() => {
3014
+ const tree = this.formTree();
3015
+ const schema = this.schema();
3016
+ if (tree === null || !schema) {
3017
+ this.dependsOnSnapshot.clear();
3018
+ this.dependsOnTree = null;
3019
+ return;
3020
+ }
3021
+ if (this.dependsOnTree !== tree) {
3022
+ this.dependsOnTree = tree;
3023
+ this.dependsOnSnapshot.clear();
3024
+ }
3025
+ for (const link of collectDependsOn(schema.fields)) {
3026
+ let container = tree;
3027
+ for (const key of link.container) container = groupTree(container, key);
3028
+ const parentValue = JSON.stringify(childTree(container, link.parentKey)().value() ?? null);
3029
+ const previous = this.dependsOnSnapshot.get(link.id);
3030
+ if (previous !== void 0 && previous !== parentValue) childTree(container, link.childKey)().value.set(link.resetValue);
3031
+ this.dependsOnSnapshot.set(link.id, parentValue);
3032
+ }
3033
+ });
3034
+ }
3035
+ layoutMode = computed(() => {
3036
+ const mode = this.schema()?.layout?.mode ?? "sections";
3037
+ if (!isSupported("layout", mode)) throw new Error(`No existe un descriptor para el layout "${mode}".`);
3038
+ return mode;
3039
+ }, ...ngDevMode ? [{ debugName: "layoutMode" }] : /* istanbul ignore next */ []);
3040
+ layoutColumns = computed(() => this.schema()?.layout?.columns ?? 2, ...ngDevMode ? [{ debugName: "layoutColumns" }] : /* istanbul ignore next */ []);
3041
+ renderedFields = computed(() => (this.schema()?.fields ?? []).filter((field) => isSupported("field", field.type)), ...ngDevMode ? [{ debugName: "renderedFields" }] : /* istanbul ignore next */ []);
3042
+ summaryErrors = computed(() => {
3043
+ if (!this.submitted()) return [];
3044
+ const errors = this.collectErrors();
3045
+ if (this.layoutMode() !== "steps") return errors;
3046
+ const attempted = this.attemptedSteps();
3047
+ return errors.filter((entry) => !entry.section || attempted.has(entry.section));
3048
+ }, ...ngDevMode ? [{ debugName: "summaryErrors" }] : /* istanbul ignore next */ []);
3049
+ sectionViews = computed(() => {
3050
+ const layout = this.schema()?.layout;
3051
+ if (!layout) return [];
3052
+ const rendered = this.renderedFields();
3053
+ return (layout.sections ?? []).map((section) => ({
3054
+ id: section.id,
3055
+ label: section.label,
3056
+ fields: rendered.filter((field) => field.section === section.id)
3057
+ })).filter((view) => view.fields.length > 0);
3058
+ }, ...ngDevMode ? [{ debugName: "sectionViews" }] : /* istanbul ignore next */ []);
3059
+ unsectionedFields = computed(() => {
3060
+ const ids = new Set(this.sectionViews().map((view) => view.id));
3061
+ return this.renderedFields().filter((field) => field.section === void 0 || !ids.has(field.section));
3062
+ }, ...ngDevMode ? [{ debugName: "unsectionedFields" }] : /* istanbul ignore next */ []);
3063
+ visibleSections = computed(() => {
3064
+ const sections = this.sectionViews();
3065
+ if (this.layoutMode() !== "tabs" && this.layoutMode() !== "steps") return sections;
3066
+ const active = sections.find((section) => section.id === this.activeSection()) ?? sections[0];
3067
+ return active ? [active] : [];
3068
+ }, ...ngDevMode ? [{ debugName: "visibleSections" }] : /* istanbul ignore next */ []);
3069
+ dirty = computed(() => this.formTree()?.().dirty() ?? false, ...ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []);
3070
+ exists = computed(() => this.mode() !== "create", ...ngDevMode ? [{ debugName: "exists" }] : /* istanbul ignore next */ []);
3071
+ canSave = computed(() => {
3072
+ const schema = this.schema();
3073
+ if (!schema) return false;
3074
+ if (this.layoutMode() === "steps" && !this.isLastStep()) return false;
3075
+ const operation = this.mode() === "create" ? "create" : "update";
3076
+ return this.auth.canDoOperation(schema, operation);
3077
+ }, ...ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []);
3078
+ canDelete = computed(() => {
3079
+ const schema = this.schema();
3080
+ return this.mode() === "edit" && schema && this.auth.canDoOperation(schema, "remove");
3081
+ }, ...ngDevMode ? [{ debugName: "canDelete" }] : /* istanbul ignore next */ []);
3082
+ rebuild() {
3083
+ const schema = this.schema();
3084
+ this.serverErrors.set({ fields: {} });
3085
+ this.openSections.set({});
3086
+ this.activeSection.set(null);
3087
+ this.attemptedSteps.set(/* @__PURE__ */ new Set());
3088
+ this.submitted.set(false);
3089
+ if (!schema) {
3090
+ this.formTree.set(null);
3091
+ this.status.set("error");
3092
+ return;
3093
+ }
3094
+ const model = signal(seedModel(schema), ...ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []);
3095
+ const schemaFn = buildFormSchema(schema, this.api, this.recordKey() ?? void 0);
3096
+ this.formTree.set(form(model, schemaFn, { injector: this.injector }));
3097
+ this.activeSection.set(this.sectionViews()[0]?.id ?? null);
3098
+ const tree = this.formTree();
3099
+ this.loadedSnapshot = JSON.stringify(tree ? tree().value() : {});
3100
+ this.loadedRecord = null;
3101
+ this.draftOffer.set(null);
3102
+ if (this.mode() === "create") {
3103
+ this.status.set("ready");
3104
+ this.offerDraft(null);
3105
+ return;
3106
+ }
3107
+ this.status.set("loading");
3108
+ this.load();
3109
+ }
3110
+ async load() {
3111
+ const schema = this.schema();
3112
+ const tree = this.formTree();
3113
+ if (!schema || !tree) return;
3114
+ const epoch = ++this.loadEpoch;
3115
+ try {
3116
+ const record = await firstValueFrom(this.api.get(schema.endpoint, this.recordKey() ?? void 0));
3117
+ if (epoch !== this.loadEpoch) return;
3118
+ this.applyRecord(tree, schema, record ?? {});
3119
+ this.loadedRecord = record ?? {};
3120
+ this.errorCarga = void 0;
3121
+ this.status.set("ready");
3122
+ this.offerDraft(record ?? null);
3123
+ } catch (error) {
3124
+ if (epoch !== this.loadEpoch) return;
3125
+ this.errorCarga = error;
3126
+ this.status.set("error");
3127
+ }
3128
+ }
3129
+ applyRecord(tree, schema, record) {
3130
+ tree().value.set(mergeRecord(schema.fields, record));
3131
+ this.loadedSnapshot = JSON.stringify(tree().value());
3132
+ }
3133
+ draftStorageKey() {
3134
+ return `draft:${this.resourceId()}:${this.recordKey() ?? "nuevo"}`;
3135
+ }
3136
+ saveDraft(value) {
3137
+ try {
3138
+ localStorage.setItem(this.draftStorageKey(), JSON.stringify({
3139
+ savedAt: Date.now(),
3140
+ value
3141
+ }));
3142
+ } catch {}
3143
+ }
3144
+ offerDraft(serverRecord) {
3145
+ let raw = null;
3146
+ try {
3147
+ raw = localStorage.getItem(this.draftStorageKey());
3148
+ } catch {
3149
+ return;
3150
+ }
3151
+ if (!raw) return;
3152
+ let draft = null;
3153
+ try {
3154
+ draft = JSON.parse(raw);
3155
+ } catch {
3156
+ return;
3157
+ }
3158
+ if (!draft || typeof draft.savedAt !== "number" || !isRecord(draft.value)) {
3159
+ this.clearDraft();
3160
+ return;
3161
+ }
3162
+ const serverTs = readTimestamp(serverRecord);
3163
+ if (serverTs !== void 0 && serverTs >= draft.savedAt) {
3164
+ this.clearDraft();
3165
+ return;
3166
+ }
3167
+ this.draftOffer.set({
3168
+ savedAt: draft.savedAt,
3169
+ value: draft.value
3170
+ });
3171
+ }
3172
+ restoreDraft() {
3173
+ const offer = this.draftOffer();
3174
+ const tree = this.formTree();
3175
+ const schema = this.schema();
3176
+ if (!offer || !tree || !schema) return;
3177
+ this.applyRecord(tree, schema, offer.value);
3178
+ this.draftOffer.set(null);
3179
+ this.notifications.info("Borrador restaurado.");
3180
+ }
3181
+ discardDraft() {
3182
+ this.clearDraft();
3183
+ this.draftOffer.set(null);
3184
+ }
3185
+ clearDraft() {
3186
+ try {
3187
+ localStorage.removeItem(this.draftStorageKey());
3188
+ } catch {}
3189
+ }
3190
+ draftAge(savedAt) {
3191
+ const minutes = Math.max(1, Math.round((Date.now() - savedAt) / 6e4));
3192
+ if (minutes < 60) return `hace ${minutes} min`;
3193
+ const hours = Math.round(minutes / 60);
3194
+ return `hace ${hours} ${hours === 1 ? "hora" : "horas"}`;
3195
+ }
3196
+ restablecer() {
3197
+ const tree = this.formTree();
3198
+ const schema = this.schema();
3199
+ if (!tree || !schema) return;
3200
+ this.applyRecord(tree, schema, this.loadedRecord ?? seedModel(schema));
3201
+ this.serverErrors.set({ fields: {} });
3202
+ this.submitted.set(false);
3203
+ this.attemptedSteps.set(/* @__PURE__ */ new Set());
3204
+ this.draftOffer.set(null);
3205
+ this.clearDraft();
3206
+ this.notifications.info("Se restauraron los valores originales.");
3207
+ }
3208
+ retry() {
3209
+ this.load();
3210
+ }
3211
+ volverDeError() {
3212
+ this.nav.back();
3213
+ }
3214
+ async onSubmit(event) {
3215
+ event.preventDefault();
3216
+ const tree = this.formTree();
3217
+ if (!tree || this.saving()) return;
3218
+ this.serverErrors.set({ fields: {} });
3219
+ this.saving.set(true);
3220
+ try {
3221
+ if (!await submit(tree, {
3222
+ action: async () => {
3223
+ await this.persist(tree);
3224
+ return null;
3225
+ },
3226
+ onInvalid: () => this.revealErrors()
3227
+ }) || this.hasServerErrors()) this.revealErrors();
3228
+ } finally {
3229
+ this.saving.set(false);
3230
+ }
3231
+ }
3232
+ async persist(tree) {
3233
+ const schema = this.schema();
3234
+ if (!schema) return;
3235
+ const payload = buildPayload(schema, tree);
3236
+ try {
3237
+ if (this.mode() === "create") {
3238
+ const created = await firstValueFrom(this.api.create(schema.endpoint, payload));
3239
+ tree().reset();
3240
+ this.clearDraft();
3241
+ this.draftOffer.set(null);
3242
+ if (schema.postCreate?.mode === "modal") {
3243
+ this.createdWithSummary.set(created ?? payload);
3244
+ return;
3245
+ }
3246
+ this.notifications.success("Registro creado.");
3247
+ this.router.navigate(["/", schema.id]);
3248
+ return;
3249
+ }
3250
+ await firstValueFrom(this.api.update(schema.endpoint, this.mode() === "edit" ? this.recordKey() ?? void 0 : void 0, payload));
3251
+ this.notifications.success("Registro guardado.");
3252
+ tree().reset();
3253
+ this.clearDraft();
3254
+ this.draftOffer.set(null);
3255
+ this.loadedSnapshot = JSON.stringify(tree().value());
3256
+ } catch (err) {
3257
+ this.applyServerErrors(this.errorMapper.map(err));
3258
+ return;
3259
+ }
3260
+ try {
3261
+ const record = await firstValueFrom(this.api.get(schema.endpoint, this.recordKey() ?? void 0));
3262
+ this.applyRecord(tree, schema, record ?? {});
3263
+ tree().reset();
3264
+ } catch {
3265
+ this.notifications.error("No se pudo recargar el registro.");
3266
+ }
3267
+ }
3268
+ applyServerErrors(mapped) {
3269
+ const rendered = new Set(this.renderedFields().map((field) => field.key));
3270
+ const fields = {};
3271
+ const loose = [];
3272
+ if (mapped.general) loose.push(mapped.general);
3273
+ for (const [path, message] of Object.entries(mapped.fields)) {
3274
+ const root = path.split(/[.[]/)[0];
3275
+ if (rendered.has(root)) fields[path] = message;
3276
+ else loose.push(message);
3277
+ }
3278
+ this.serverErrors.set({
3279
+ fields,
3280
+ general: loose.length > 0 ? loose.join(" ") : void 0
3281
+ });
3282
+ }
3283
+ hasServerErrors() {
3284
+ const current = this.serverErrors();
3285
+ return current.general !== void 0 || Object.keys(current.fields).length > 0;
3286
+ }
3287
+ revealErrors() {
3288
+ this.submitted.set(true);
3289
+ const sections = this.sectionViews();
3290
+ if (this.layoutMode() === "sections") this.openSections.update((open) => {
3291
+ const next = { ...open };
3292
+ for (const section of sections) next[section.id] = true;
3293
+ return next;
3294
+ });
3295
+ const withProblem = sections.find((section) => this.sectionHasProblem(section));
3296
+ if (withProblem && this.layoutMode() === "tabs") this.activeSection.set(withProblem.id);
3297
+ if (this.layoutMode() === "steps") {
3298
+ const invalidSteps = sections.filter((section) => this.sectionHasProblem(section)).map((section) => section.id);
3299
+ this.attemptedSteps.update((attempted) => /* @__PURE__ */ new Set([...attempted, ...invalidSteps]));
3300
+ if (withProblem) this.activeSection.set(withProblem.id);
3301
+ }
3302
+ setTimeout(() => {
3303
+ if (this.summaryErrors().length >= 2) this.focusSummary();
3304
+ else this.scrollToFirstError();
3305
+ });
3306
+ }
3307
+ scrollToFirstError() {
3308
+ for (const wrap of document.querySelectorAll("[data-field]")) {
3309
+ if (!wrap.querySelector("p[role=\"alert\"]")) continue;
3310
+ wrap.scrollIntoView({
3311
+ behavior: "smooth",
3312
+ block: "center"
3313
+ });
3314
+ wrap.querySelector("input:not([type=\"hidden\"]), textarea, select, [contenteditable=\"true\"]")?.focus({ preventScroll: true });
3315
+ return;
3316
+ }
3317
+ }
3318
+ async remove() {
3319
+ const schema = this.schema();
3320
+ const key = this.recordKey();
3321
+ if (!schema || this.mode() !== "edit" || !key) return;
3322
+ const title = this.title();
3323
+ if (!await this.notifications.confirm({
3324
+ title: title ? `Eliminar «${title}»` : `Eliminar ${schema.label.toLowerCase()}`,
3325
+ message: "Esta acción no se puede deshacer.",
3326
+ confirmLabel: "Eliminar",
3327
+ cancelLabel: "Cancelar"
3328
+ })) return;
3329
+ try {
3330
+ await firstValueFrom(this.api.remove(schema.endpoint, key));
3331
+ this.notifications.success("Registro eliminado.");
3332
+ this.back();
3333
+ } catch (err) {
3334
+ this.notifications.error(this.errorMapper.messageOf(err));
3335
+ }
3336
+ }
3337
+ async canDeactivate() {
3338
+ if (!this.dirty()) return true;
3339
+ return this.notifications.confirm({
3340
+ title: "Cambios sin guardar",
3341
+ message: "Si sales ahora, se perderán los cambios realizados.",
3342
+ confirmLabel: "Salir sin guardar",
3343
+ cancelLabel: "Seguir editando"
3344
+ });
3345
+ }
3346
+ back() {
3347
+ this.router.navigate(["/", this.schema()?.id]);
3348
+ }
3349
+ onPostCreateClosed() {
3350
+ const schema = this.schema();
3351
+ this.createdWithSummary.set(null);
3352
+ if (schema) this.router.navigate(["/", schema.id]);
3353
+ }
3354
+ colSpan(field) {
3355
+ return colSpanClass(field.width);
3356
+ }
3357
+ tabId(sectionId) {
3358
+ return `section-tab-${sectionId}`;
3359
+ }
3360
+ onTablistKeydown(event) {
3361
+ const tabs = this.sectionViews();
3362
+ if (tabs.length < 2) return;
3363
+ const current = Math.max(tabs.findIndex((section) => section.id === this.activeSection()), 0);
3364
+ let next;
3365
+ switch (event.key) {
3366
+ case "ArrowRight":
3367
+ next = (current + 1) % tabs.length;
3368
+ break;
3369
+ case "ArrowLeft":
3370
+ next = (current - 1 + tabs.length) % tabs.length;
3371
+ break;
3372
+ case "Home":
3373
+ next = 0;
3374
+ break;
3375
+ case "End":
3376
+ next = tabs.length - 1;
3377
+ break;
3378
+ default: return;
3379
+ }
3380
+ event.preventDefault();
3381
+ this.selectSection(tabs[next].id);
3382
+ const active = this.activeSection() ?? tabs[current].id;
3383
+ document.getElementById(this.tabId(active))?.focus();
3384
+ }
3385
+ tabClass(section) {
3386
+ return section.id === this.activeSection() ? TAB_ACTIVE : TAB_INACTIVE;
3387
+ }
3388
+ isFirstStep() {
3389
+ return this.sectionViews().findIndex((section) => section.id === this.activeSection()) <= 0;
3390
+ }
3391
+ isLastStep() {
3392
+ const sections = this.sectionViews();
3393
+ const index = sections.findIndex((section) => section.id === this.activeSection());
3394
+ return sections.length === 0 || index >= sections.length - 1;
3395
+ }
3396
+ previousStep() {
3397
+ const sections = this.sectionViews();
3398
+ const current = sections.findIndex((section) => section.id === this.activeSection());
3399
+ if (current > 0) {
3400
+ this.activeSection.set(sections[current - 1].id);
3401
+ this.scrollToStep();
3402
+ }
3403
+ }
3404
+ nextStep() {
3405
+ const sections = this.sectionViews();
3406
+ const current = sections.findIndex((section) => section.id === this.activeSection());
3407
+ if (current < 0 || current >= sections.length - 1) return;
3408
+ if (current === 0 && this.unsectionedHasLocalErrors()) {
3409
+ this.revealUnsectionedErrors();
3410
+ return;
3411
+ }
3412
+ const section = sections[current];
3413
+ if (this.sectionHasLocalErrors(section.id)) {
3414
+ this.attemptedSteps.update((attempted) => /* @__PURE__ */ new Set([...attempted, section.id]));
3415
+ this.revealSectionErrors(section.id);
3416
+ return;
3417
+ }
3418
+ this.activeSection.set(sections[current + 1].id);
3419
+ this.scrollToStep();
3420
+ }
3421
+ unsectionedHasLocalErrors() {
3422
+ const tree = this.formTree();
3423
+ return tree !== null && this.unsectionedFields().some((field) => childTree(tree, field.key)().invalid());
3424
+ }
3425
+ revealUnsectionedErrors() {
3426
+ const tree = this.formTree();
3427
+ if (!tree) return;
3428
+ this.submitted.set(true);
3429
+ for (const field of this.unsectionedFields()) childTree(tree, field.key)().markAsTouched();
3430
+ setTimeout(() => this.scrollToFirstError());
3431
+ }
3432
+ scrollToStep() {
3433
+ setTimeout(() => document.getElementById("step-panel")?.scrollIntoView({
3434
+ behavior: "smooth",
3435
+ block: "start"
3436
+ }));
3437
+ }
3438
+ selectSection(id) {
3439
+ const current = this.activeSection();
3440
+ if (this.submitted() && current !== null && current !== id && this.sectionHasLocalErrors(current)) {
3441
+ this.revealSectionErrors(current);
3442
+ return;
3443
+ }
3444
+ this.activeSection.set(id);
3445
+ }
3446
+ sectionHasLocalErrors(sectionId) {
3447
+ const tree = this.formTree();
3448
+ const section = this.sectionViews().find((view) => view.id === sectionId);
3449
+ if (!tree || !section) return false;
3450
+ return section.fields.some((field) => childTree(tree, field.key)().invalid());
3451
+ }
3452
+ revealSectionErrors(sectionId) {
3453
+ const tree = this.formTree();
3454
+ const section = this.sectionViews().find((view) => view.id === sectionId);
3455
+ if (!tree || !section) return;
3456
+ this.submitted.set(true);
3457
+ this.attemptedSteps.update((attempted) => /* @__PURE__ */ new Set([...attempted, sectionId]));
3458
+ for (const field of section.fields) childTree(tree, field.key)().markAsTouched();
3459
+ setTimeout(() => this.scrollToFirstError());
3460
+ }
3461
+ isSectionOpen(id) {
3462
+ return this.openSections()[id] ?? true;
3463
+ }
3464
+ setSectionOpen(id, event) {
3465
+ const open = event.target.open;
3466
+ this.openSections.update((sections) => ({
3467
+ ...sections,
3468
+ [id]: open
3469
+ }));
3470
+ }
3471
+ goToSection(id) {
3472
+ this.openSections.update((open) => ({
3473
+ ...open,
3474
+ [id]: true
3475
+ }));
3476
+ setTimeout(() => {
3477
+ const section = document.getElementById(`section-${id}`);
3478
+ if (!section) return;
3479
+ section.scrollIntoView({
3480
+ behavior: "smooth",
3481
+ block: "start"
3482
+ });
3483
+ section.querySelector("summary")?.focus({ preventScroll: true });
3484
+ });
3485
+ }
3486
+ fieldHasProblem(field) {
3487
+ const root = field.key;
3488
+ if (Object.keys(this.serverErrors().fields).some((path) => path === root || path.startsWith(`${root}.`) || path.startsWith(`${root}[`))) return true;
3489
+ const tree = this.formTree();
3490
+ return tree !== null && childTree(tree, root)().invalid();
3491
+ }
3492
+ sectionHasProblem(section) {
3493
+ return section.fields.some((field) => this.fieldHasProblem(field));
3494
+ }
3495
+ sectionProgress(section) {
3496
+ if (!this.sectionHasProblem(section)) return "complete";
3497
+ if (this.layoutMode() === "steps" && !this.attemptedSteps().has(section.id)) return "pending";
3498
+ return this.submitted() ? "problem" : "pending";
3499
+ }
3500
+ collectErrors() {
3501
+ const tree = this.formTree();
3502
+ if (!tree) return [];
3503
+ const server = this.serverErrors().fields;
3504
+ const entries = [];
3505
+ const seen = /* @__PURE__ */ new Set();
3506
+ const walk = (fields, subtree, inheritedSection) => {
3507
+ for (const field of fields) {
3508
+ const section = field.section ?? inheritedSection;
3509
+ const state = childTree(subtree, field.key)();
3510
+ const serverMessage = this.serverMessageFor(field.key, server);
3511
+ if (field.type === "group") {
3512
+ const before = entries.length;
3513
+ walk(field.fields, groupTree(subtree, field.key), section);
3514
+ if (entries.length === before && (serverMessage || state.invalid())) {
3515
+ seen.add(state.name());
3516
+ entries.push({
3517
+ id: state.name(),
3518
+ label: field.label,
3519
+ message: serverMessage ?? "Revisa este campo",
3520
+ section
3521
+ });
3522
+ }
3523
+ continue;
3524
+ }
3525
+ if (!state.invalid() && !serverMessage) continue;
3526
+ const composite = field.type === "list" || field.type === "key-value" || field.type === "string-list";
3527
+ const id = state.name();
3528
+ if (seen.has(id)) continue;
3529
+ seen.add(id);
3530
+ entries.push({
3531
+ id,
3532
+ label: field.label,
3533
+ message: serverMessage ?? state.errors()[0]?.message ?? (composite ? "Hay elementos con errores" : "Revisa este campo"),
3534
+ section
3535
+ });
3536
+ }
3537
+ };
3538
+ walk(this.renderedFields(), tree);
3539
+ return entries;
3540
+ }
3541
+ serverMessageFor(root, server) {
3542
+ for (const [path, message] of Object.entries(server)) if (path === root || path.startsWith(`${root}.`) || path.startsWith(`${root}[`)) return message;
3543
+ }
3544
+ async goToError(entry, event) {
3545
+ event.preventDefault();
3546
+ if (entry.section) {
3547
+ if (this.layoutMode() === "tabs" || this.layoutMode() === "steps") this.activeSection.set(entry.section);
3548
+ else this.openSections.update((open) => ({
3549
+ ...open,
3550
+ [entry.section]: true
3551
+ }));
3552
+ }
3553
+ await this.appRef.whenStable();
3554
+ this.focusField(entry.id);
3555
+ }
3556
+ focusField(id) {
3557
+ const wrap = document.querySelector(`[data-field="${id}"]`);
3558
+ if (!wrap) return;
3559
+ wrap.scrollIntoView({
3560
+ behavior: "smooth",
3561
+ block: "center"
3562
+ });
3563
+ wrap.querySelector("input:not([type=\"hidden\"]), textarea, select, [contenteditable=\"true\"]")?.focus({ preventScroll: true });
3564
+ }
3565
+ focusSummary() {
3566
+ const summary = document.querySelector("[data-error-summary]");
3567
+ summary?.scrollIntoView({
3568
+ behavior: "smooth",
3569
+ block: "start"
3570
+ });
3571
+ summary?.focus({ preventScroll: true });
3572
+ }
3573
+ title() {
3574
+ const tree = this.formTree();
3575
+ const schema = this.schema();
3576
+ if (!tree || !schema) return "";
3577
+ const value = tree().value()[schema.titleField];
3578
+ return typeof value === "string" ? value : "";
3579
+ }
3580
+ recordKey() {
3581
+ return this.params().get("key");
3582
+ }
3583
+ drawerSchema(resource) {
3584
+ return getSchema(resource);
3585
+ }
3586
+ relationCreated(record, request) {
3587
+ const target = getSchema(request.resource);
3588
+ const key = target === void 0 ? void 0 : record[target.keyField];
3589
+ this.relationDrawer.clear();
3590
+ if (key === void 0 || key === null || key === "") return;
3591
+ request.onCreated(String(key));
3592
+ }
3593
+ static ɵfac = i0.ɵɵngDeclareFactory({
3594
+ minVersion: "12.0.0",
3595
+ version: "22.2.3",
3596
+ ngImport: i0,
3597
+ type: FormView,
3598
+ deps: [],
3599
+ target: i0.ɵɵFactoryTarget.Component
3600
+ });
3601
+ static ɵcmp = i0.ɵɵngDeclareComponent({
3602
+ minVersion: "17.0.0",
3603
+ version: "22.2.3",
3604
+ type: FormView,
3605
+ isStandalone: true,
3606
+ selector: "app-form-view",
3607
+ ngImport: i0,
3608
+ template: `
3609
+ @if (schema()) {
3610
+ <header class="mb-6 flex flex-wrap items-start justify-between gap-4">
3611
+ <div>
3612
+ <div class="flex flex-wrap items-center gap-3">
3613
+ <h1 class="font-display text-2xl font-semibold">{{ heading() }}</h1>
3614
+ @if (dirty()) {
3615
+ <span
3616
+ class="rounded-full bg-accent/15 px-2.5 py-0.5 text-xs font-medium text-accent-text"
3617
+ >
3618
+ Sin guardar
3619
+ </span>
3620
+ }
3621
+ </div>
3622
+ @if (mode() === 'single') {
3623
+ <p class="text-sm text-neutral">Configuración única</p>
3624
+ }
3625
+ </div>
3626
+ @if (mode() !== 'single') {
3627
+ <button app-button variant="outline" type="button" (click)="back()">
3628
+ Volver al listado
3629
+ </button>
3630
+ }
3631
+ </header>
3632
+
3633
+ @switch (status()) {
3634
+ @case ('loading') {
3635
+ <app-skeleton [lines]="8" [height]="48" />
3636
+ }
3637
+ @case ('error') {
3638
+ <app-empty-state
3639
+ title="No se pudo cargar el registro"
3640
+ message="Revisa tu conexión e intenta de nuevo."
3641
+ >
3642
+ <button app-button variant="outline" type="button" (click)="retry()">Reintentar</button>
3643
+ <button app-button variant="outline" type="button" (click)="volverDeError()">
3644
+ Volver
3645
+ </button>
3646
+ </app-empty-state>
3647
+ }
3648
+ @case ('ready') {
3649
+ @if (formTree(); as tree) {
3650
+ @if (serverErrors().general; as general) {
3651
+ <p
3652
+ class="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-4 py-3 text-sm text-danger"
3653
+ role="alert"
3654
+ >
3655
+ {{ general }}
3656
+ </p>
3657
+ }
3658
+
3659
+ <form novalidate [attr.aria-busy]="saving()" (submit)="onSubmit($event)">
3660
+ @if (draftOffer(); as offer) {
3661
+ <!-- §4.6: se OFRECE, nunca se restaura solo. role=status para
3662
+ anunciarlo sin arrebatarle el foco a quien está escribiendo. -->
3663
+ <div
3664
+ class="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-neutral/20 bg-surface px-4 py-3 text-sm"
3665
+ role="status"
3666
+ >
3667
+ <span class="text-neutral">
3668
+ Hay un borrador guardado {{ draftAge(offer.savedAt) }}.
3669
+ </span>
3670
+ <button app-button variant="outline" type="button" (click)="restoreDraft()">
3671
+ Restaurar
3672
+ </button>
3673
+ <button app-button variant="ghost" type="button" (click)="discardDraft()">
3674
+ Descartar
3675
+ </button>
3676
+ </div>
3677
+ }
3678
+
3679
+ @if (summaryErrors().length >= 2) {
3680
+ <!-- Con uno solo no se pinta: el foco ya está en el campo y
3681
+ el resumen sólo restaría. Con 2+ es la única forma de
3682
+ ver que hay varios (§4.11). tabindex -1 para poder
3683
+ recibir el foco y que role=alert lo anuncie. -->
3684
+ <div
3685
+ data-error-summary
3686
+ tabindex="-1"
3687
+ role="alert"
3688
+ aria-labelledby="error-summary-title"
3689
+ class="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-4 py-3 focus:outline-2 focus:outline-danger"
3690
+ >
3691
+ <p id="error-summary-title" class="text-sm font-semibold text-danger">
3692
+ Hay {{ summaryErrors().length }} errores que corregir
3693
+ </p>
3694
+ <ul class="mt-2 list-disc space-y-1 pl-5 text-sm">
3695
+ @for (entry of summaryErrors(); track entry.id) {
3696
+ <li>
3697
+ <a
3698
+ href="#"
3699
+ class="text-danger underline underline-offset-2 hover:text-danger/80"
3700
+ (click)="goToError(entry, $event)"
3701
+ >
3702
+ {{ entry.label }} — {{ entry.message }}
3703
+ </a>
3704
+ </li>
3705
+ }
3706
+ </ul>
3707
+ </div>
3708
+ }
3709
+
3710
+ @if (layoutMode() === 'grid') {
3711
+ <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
3712
+ @for (field of renderedFields(); track field.key) {
3713
+ <div [class]="colSpan(field)">
3714
+ <app-field-host
3715
+ [field]="field"
3716
+ [tree]="tree"
3717
+ [exists]="exists()"
3718
+ [serverErrors]="serverErrors().fields"
3719
+ [resource]="resourceId()"
3720
+ />
3721
+ </div>
3722
+ }
3723
+ </div>
3724
+ } @else if (unsectionedFields().length > 0) {
3725
+ <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
3726
+ @for (field of unsectionedFields(); track field.key) {
3727
+ <div [class]="colSpan(field)">
3728
+ <app-field-host
3729
+ [field]="field"
3730
+ [tree]="tree"
3731
+ [exists]="exists()"
3732
+ [serverErrors]="serverErrors().fields"
3733
+ [resource]="resourceId()"
3734
+ />
3735
+ </div>
3736
+ }
3737
+ </div>
3738
+ }
3739
+
3740
+ @if (layoutMode() === 'tabs') {
3741
+ <div
3742
+ role="tablist"
3743
+ class="mb-4 flex flex-wrap gap-1 border-b border-neutral/20"
3744
+ (keydown)="onTablistKeydown($event)"
3745
+ >
3746
+ @for (section of sectionViews(); track section.id) {
3747
+ <button
3748
+ type="button"
3749
+ role="tab"
3750
+ [id]="tabId(section.id)"
3751
+ [class]="tabClass(section)"
3752
+ [attr.aria-selected]="activeSection() === section.id"
3753
+ [attr.tabindex]="activeSection() === section.id ? 0 : -1"
3754
+ (click)="selectSection(section.id)"
3755
+ >
3756
+ {{ section.label }}
3757
+ <!-- Progreso (§4.3, decisión A): el ✓ siempre que esté
3758
+ completa; el ⚠ recién después del primer Guardar.
3759
+ El glifo va oculto para lectores y el estado se
3760
+ cuenta aparte en texto. -->
3761
+ @switch (sectionProgress(section)) {
3762
+ @case ('problem') {
3763
+ <span class="ml-1 text-danger" aria-hidden="true">⚠</span>
3764
+ <span class="sr-only"> — con errores</span>
3765
+ }
3766
+ @case ('complete') {
3767
+ <span class="ml-1 text-success" aria-hidden="true">✓</span>
3768
+ <span class="sr-only"> — completa</span>
3769
+ }
3770
+ }
3771
+ </button>
3772
+ }
3773
+ </div>
3774
+ }
3775
+
3776
+ @if (layoutMode() === 'steps') {
3777
+ <ol aria-label="Progreso del formulario" class="mb-4 flex flex-wrap gap-2">
3778
+ @for (section of sectionViews(); track section.id; let index = $index) {
3779
+ <li
3780
+ class="rounded-full border px-3 py-1 text-sm"
3781
+ [class.border-primary]="activeSection() === section.id"
3782
+ [class.font-semibold]="activeSection() === section.id"
3783
+ [attr.aria-current]="activeSection() === section.id ? 'step' : null"
3784
+ >
3785
+ {{ index + 1 }}. {{ section.label }}
3786
+ </li>
3787
+ }
3788
+ </ol>
3789
+ }
3790
+
3791
+ @if (layoutMode() === 'tabs' || layoutMode() === 'steps') {
3792
+ @for (section of visibleSections(); track section.id) {
3793
+ <div
3794
+ role="tabpanel"
3795
+ [attr.id]="layoutMode() === 'steps' ? 'step-panel' : null"
3796
+ class="grid grid-cols-1 gap-x-4 sm:grid-cols-12"
3797
+ >
3798
+ @for (field of section.fields; track field.key) {
3799
+ <div [class]="colSpan(field)">
3800
+ <app-field-host
3801
+ [field]="field"
3802
+ [tree]="tree"
3803
+ [exists]="exists()"
3804
+ [serverErrors]="serverErrors().fields"
3805
+ [resource]="resourceId()"
3806
+ />
3807
+ </div>
3808
+ }
3809
+ </div>
3810
+ }
3811
+ @if (layoutMode() === 'steps' && sectionViews().length > 1) {
3812
+ <div class="mt-4 flex justify-between gap-3">
3813
+ <button
3814
+ app-button
3815
+ variant="outline"
3816
+ type="button"
3817
+ [disabled]="isFirstStep()"
3818
+ (click)="previousStep()"
3819
+ >
3820
+ Anterior
3821
+ </button>
3822
+ @if (!isLastStep()) {
3823
+ <button app-button type="button" (click)="nextStep()">Continuar</button>
3824
+ }
3825
+ </div>
3826
+ }
3827
+ } @else if (layoutMode() === 'columns') {
3828
+ <div
3829
+ [class]="
3830
+ layoutColumns() === 3
3831
+ ? 'grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3'
3832
+ : 'grid grid-cols-1 gap-4 md:grid-cols-2'
3833
+ "
3834
+ >
3835
+ @for (section of sectionViews(); track section.id) {
3836
+ <section class="rounded-xl border border-neutral/20 bg-surface p-4">
3837
+ <h2 class="mb-3 text-section font-semibold">{{ section.label }}</h2>
3838
+ <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
3839
+ @for (field of section.fields; track field.key) {
3840
+ <div [class]="colSpan(field)">
3841
+ <app-field-host
3842
+ [field]="field"
3843
+ [tree]="tree"
3844
+ [exists]="exists()"
3845
+ [serverErrors]="serverErrors().fields"
3846
+ [resource]="resourceId()"
3847
+ />
3848
+ </div>
3849
+ }
3850
+ </div>
3851
+ </section>
3852
+ }
3853
+ </div>
3854
+ } @else if (layoutMode() === 'sections') {
3855
+ <!-- Índice interno (§5.3): en escritorio, índice sticky a la
3856
+ izquierda; en móvil, chips horizontales. En tabs las
3857
+ pestañas ya hacen de índice, así que esto no se dibuja. -->
3858
+ <div class="flex flex-col gap-4 lg:flex-row lg:gap-6">
3859
+ <nav
3860
+ aria-label="Secciones"
3861
+ class="sticky top-4 hidden shrink-0 self-start lg:block lg:w-44"
3862
+ >
3863
+ <p
3864
+ class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral"
3865
+ aria-hidden="true"
3866
+ >
3867
+ Secciones
3868
+ </p>
3869
+ <ul class="space-y-1 border-l border-neutral/20 pl-3">
3870
+ @for (section of visibleSections(); track section.id) {
3871
+ <li>
3872
+ <button
3873
+ type="button"
3874
+ class="w-full rounded px-2 py-1 text-left text-sm hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
3875
+ [class.font-medium]="sectionProgress(section) === 'problem'"
3876
+ (click)="goToSection(section.id)"
3877
+ >
3878
+ {{ section.label }}
3879
+ @switch (sectionProgress(section)) {
3880
+ @case ('problem') {
3881
+ <span class="ml-1 text-danger" aria-hidden="true">⚠</span>
3882
+ <span class="sr-only"> — con errores</span>
3883
+ }
3884
+ @case ('complete') {
3885
+ <span class="ml-1 text-success" aria-hidden="true">✓</span>
3886
+ <span class="sr-only"> — completa</span>
3887
+ }
3888
+ }
3889
+ </button>
3890
+ </li>
3891
+ }
3892
+ </ul>
3893
+ </nav>
3894
+
3895
+ <div class="min-w-0 flex-1" [class.lg:grid]="layoutMode() === 'columns'">
3896
+ <nav
3897
+ aria-label="Secciones"
3898
+ class="-mx-1 mb-4 flex gap-2 overflow-x-auto px-1 pb-1 lg:hidden"
3899
+ >
3900
+ @for (section of visibleSections(); track section.id) {
3901
+ <button
3902
+ type="button"
3903
+ class="shrink-0 rounded-full border border-neutral/30 px-3 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
3904
+ [class.border-danger]="sectionProgress(section) === 'problem'"
3905
+ [class.text-danger]="sectionProgress(section) === 'problem'"
3906
+ [class.font-medium]="sectionProgress(section) === 'problem'"
3907
+ (click)="goToSection(section.id)"
3908
+ >
3909
+ {{ section.label }}
3910
+ </button>
3911
+ }
3912
+ </nav>
3913
+
3914
+ @for (section of visibleSections(); track section.id) {
3915
+ <details
3916
+ id="section-{{ section.id }}"
3917
+ class="mb-4 rounded-xl border border-neutral/20 bg-surface scroll-mt-4"
3918
+ [open]="isSectionOpen(section.id)"
3919
+ (toggle)="setSectionOpen(section.id, $event)"
3920
+ >
3921
+ <summary
3922
+ tabindex="-1"
3923
+ class="flex cursor-pointer items-center justify-between gap-2 rounded-xl px-4 py-3 text-section font-semibold hover:bg-neutral/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
3924
+ >
3925
+ <span>{{ section.label }}</span>
3926
+ @if (sectionProgress(section) === 'problem') {
3927
+ <span class="text-xs font-medium text-danger">Revisar</span>
3928
+ }
3929
+ </summary>
3930
+ <div class="grid grid-cols-1 gap-x-4 px-4 pb-4 sm:grid-cols-12">
3931
+ @for (field of section.fields; track field.key) {
3932
+ <div [class]="colSpan(field)">
3933
+ <app-field-host
3934
+ [field]="field"
3935
+ [tree]="tree"
3936
+ [exists]="exists()"
3937
+ [serverErrors]="serverErrors().fields"
3938
+ [resource]="resourceId()"
3939
+ />
3940
+ </div>
3941
+ }
3942
+ </div>
3943
+ </details>
3944
+ }
3945
+ </div>
3946
+ </div>
3947
+ }
3948
+
3949
+ <!-- Sticky (§5.1): en products son 19 campos y Cancelar/Guardar
3950
+ desaparecían al final del scroll. bg-surface es opaco: sin
3951
+ él el contenido vería pasar por detrás. -->
3952
+ <div
3953
+ class="sticky bottom-0 z-10 mt-6 flex flex-wrap items-center justify-end gap-3 border-t border-neutral/20 bg-surface pt-4 pb-[calc(1rem+env(safe-area-inset-bottom))]"
3954
+ >
3955
+ @if (canDelete()) {
3956
+ <div class="mr-auto">
3957
+ <button app-button variant="danger" type="button" (click)="remove()">
3958
+ Eliminar
3959
+ </button>
3960
+ </div>
3961
+ }
3962
+ @if (mode() !== 'create') {
3963
+ <!-- §5: descartar lo escrito y volver a lo del servidor. -->
3964
+ <button app-button variant="ghost" type="button" (click)="restablecer()">
3965
+ Restablecer
3966
+ </button>
3967
+ }
3968
+ <button app-button variant="outline" type="button" (click)="back()">
3969
+ Cancelar
3970
+ </button>
3971
+ <button app-button type="submit" [loading]="saving()" [disabled]="!canSave()">
3972
+ {{ saving() ? 'Guardando…' : 'Guardar' }}
3973
+ </button>
3974
+ </div>
3975
+ </form>
3976
+ }
3977
+ }
3978
+ }
3979
+ } @else {
3980
+ <app-empty-state
3981
+ title="Recurso no encontrado"
3982
+ message="No existe un schema con ese id en el registry."
3983
+ />
3984
+ }
3985
+
3986
+ <!-- «Crear nuevo» desde el dropdown de una relación (plan 6.4). Se monta
3987
+ y desmonta con el pedido: el Drawer devuelve el foco al botón que lo
3988
+ abrió al destruirse. -->
3989
+ @if (relationDrawer.request(); as createRequest) {
3990
+ @if (drawerSchema(createRequest.resource); as target) {
3991
+ <app-drawer-form
3992
+ [open]="true"
3993
+ [schema]="target"
3994
+ mode="create"
3995
+ [title]="'Crear ' + target.label"
3996
+ (close)="relationDrawer.clear()"
3997
+ (saved)="relationCreated($event, createRequest)"
3998
+ />
3999
+ }
4000
+ }
4001
+
4002
+ @if (createdWithSummary(); as createdRec) {
4003
+ @if (schema()?.postCreate; as postCreateConfig) {
4004
+ <app-post-create-modal
4005
+ [schema]="schema()!"
4006
+ [config]="postCreateConfig"
4007
+ [record]="createdRec"
4008
+ (closed)="onPostCreateClosed()"
4009
+ />
4010
+ }
4011
+ }
4012
+ `,
4013
+ isInline: true,
4014
+ dependencies: [
4015
+ {
4016
+ kind: "component",
4017
+ type: Button,
4018
+ selector: "button[app-button]",
4019
+ inputs: [
4020
+ "variant",
4021
+ "size",
4022
+ "type",
4023
+ "disabled",
4024
+ "loading"
4025
+ ]
4026
+ },
4027
+ {
4028
+ kind: "component",
4029
+ type: DrawerForm,
4030
+ selector: "app-drawer-form",
4031
+ inputs: [
4032
+ "open",
4033
+ "schema",
4034
+ "fields",
4035
+ "mode",
4036
+ "recordKey",
4037
+ "title",
4038
+ "size"
4039
+ ],
4040
+ outputs: ["saved", "close"]
4041
+ },
4042
+ {
4043
+ kind: "component",
4044
+ type: EmptyState,
4045
+ selector: "app-empty-state",
4046
+ inputs: ["title", "message"]
4047
+ },
4048
+ {
4049
+ kind: "component",
4050
+ type: FieldHost,
4051
+ selector: "app-field-host",
4052
+ inputs: [
4053
+ "field",
4054
+ "tree",
4055
+ "exists",
4056
+ "serverErrors",
4057
+ "pathPrefix",
4058
+ "resource"
4059
+ ]
4060
+ },
4061
+ {
4062
+ kind: "component",
4063
+ type: PostCreateModal,
4064
+ selector: "app-post-create-modal",
4065
+ inputs: [
4066
+ "schema",
4067
+ "config",
4068
+ "record",
4069
+ "configValues"
4070
+ ],
4071
+ outputs: ["closed"]
4072
+ },
4073
+ {
4074
+ kind: "component",
4075
+ type: Skeleton,
4076
+ selector: "app-skeleton",
4077
+ inputs: [
4078
+ "lines",
4079
+ "height",
4080
+ "width"
4081
+ ]
4082
+ }
4083
+ ]
4084
+ });
4085
+ };
4086
+ i0.ɵɵngDeclareClassMetadata({
4087
+ minVersion: "12.0.0",
4088
+ version: "22.2.3",
4089
+ ngImport: i0,
4090
+ type: FormView,
4091
+ decorators: [{
4092
+ type: Component,
4093
+ args: [{
4094
+ selector: "app-form-view",
4095
+ imports: [
4096
+ Button,
4097
+ DrawerForm,
4098
+ EmptyState,
4099
+ FieldHost,
4100
+ PostCreateModal,
4101
+ Skeleton
4102
+ ],
4103
+ template: `
4104
+ @if (schema()) {
4105
+ <header class="mb-6 flex flex-wrap items-start justify-between gap-4">
4106
+ <div>
4107
+ <div class="flex flex-wrap items-center gap-3">
4108
+ <h1 class="font-display text-2xl font-semibold">{{ heading() }}</h1>
4109
+ @if (dirty()) {
4110
+ <span
4111
+ class="rounded-full bg-accent/15 px-2.5 py-0.5 text-xs font-medium text-accent-text"
4112
+ >
4113
+ Sin guardar
4114
+ </span>
4115
+ }
4116
+ </div>
4117
+ @if (mode() === 'single') {
4118
+ <p class="text-sm text-neutral">Configuración única</p>
4119
+ }
4120
+ </div>
4121
+ @if (mode() !== 'single') {
4122
+ <button app-button variant="outline" type="button" (click)="back()">
4123
+ Volver al listado
4124
+ </button>
4125
+ }
4126
+ </header>
4127
+
4128
+ @switch (status()) {
4129
+ @case ('loading') {
4130
+ <app-skeleton [lines]="8" [height]="48" />
4131
+ }
4132
+ @case ('error') {
4133
+ <app-empty-state
4134
+ title="No se pudo cargar el registro"
4135
+ message="Revisa tu conexión e intenta de nuevo."
4136
+ >
4137
+ <button app-button variant="outline" type="button" (click)="retry()">Reintentar</button>
4138
+ <button app-button variant="outline" type="button" (click)="volverDeError()">
4139
+ Volver
4140
+ </button>
4141
+ </app-empty-state>
4142
+ }
4143
+ @case ('ready') {
4144
+ @if (formTree(); as tree) {
4145
+ @if (serverErrors().general; as general) {
4146
+ <p
4147
+ class="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-4 py-3 text-sm text-danger"
4148
+ role="alert"
4149
+ >
4150
+ {{ general }}
4151
+ </p>
4152
+ }
4153
+
4154
+ <form novalidate [attr.aria-busy]="saving()" (submit)="onSubmit($event)">
4155
+ @if (draftOffer(); as offer) {
4156
+ <!-- §4.6: se OFRECE, nunca se restaura solo. role=status para
4157
+ anunciarlo sin arrebatarle el foco a quien está escribiendo. -->
4158
+ <div
4159
+ class="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-neutral/20 bg-surface px-4 py-3 text-sm"
4160
+ role="status"
4161
+ >
4162
+ <span class="text-neutral">
4163
+ Hay un borrador guardado {{ draftAge(offer.savedAt) }}.
4164
+ </span>
4165
+ <button app-button variant="outline" type="button" (click)="restoreDraft()">
4166
+ Restaurar
4167
+ </button>
4168
+ <button app-button variant="ghost" type="button" (click)="discardDraft()">
4169
+ Descartar
4170
+ </button>
4171
+ </div>
4172
+ }
4173
+
4174
+ @if (summaryErrors().length >= 2) {
4175
+ <!-- Con uno solo no se pinta: el foco ya está en el campo y
4176
+ el resumen sólo restaría. Con 2+ es la única forma de
4177
+ ver que hay varios (§4.11). tabindex -1 para poder
4178
+ recibir el foco y que role=alert lo anuncie. -->
4179
+ <div
4180
+ data-error-summary
4181
+ tabindex="-1"
4182
+ role="alert"
4183
+ aria-labelledby="error-summary-title"
4184
+ class="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-4 py-3 focus:outline-2 focus:outline-danger"
4185
+ >
4186
+ <p id="error-summary-title" class="text-sm font-semibold text-danger">
4187
+ Hay {{ summaryErrors().length }} errores que corregir
4188
+ </p>
4189
+ <ul class="mt-2 list-disc space-y-1 pl-5 text-sm">
4190
+ @for (entry of summaryErrors(); track entry.id) {
4191
+ <li>
4192
+ <a
4193
+ href="#"
4194
+ class="text-danger underline underline-offset-2 hover:text-danger/80"
4195
+ (click)="goToError(entry, $event)"
4196
+ >
4197
+ {{ entry.label }} — {{ entry.message }}
4198
+ </a>
4199
+ </li>
4200
+ }
4201
+ </ul>
4202
+ </div>
4203
+ }
4204
+
4205
+ @if (layoutMode() === 'grid') {
4206
+ <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
4207
+ @for (field of renderedFields(); track field.key) {
4208
+ <div [class]="colSpan(field)">
4209
+ <app-field-host
4210
+ [field]="field"
4211
+ [tree]="tree"
4212
+ [exists]="exists()"
4213
+ [serverErrors]="serverErrors().fields"
4214
+ [resource]="resourceId()"
4215
+ />
4216
+ </div>
4217
+ }
4218
+ </div>
4219
+ } @else if (unsectionedFields().length > 0) {
4220
+ <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
4221
+ @for (field of unsectionedFields(); track field.key) {
4222
+ <div [class]="colSpan(field)">
4223
+ <app-field-host
4224
+ [field]="field"
4225
+ [tree]="tree"
4226
+ [exists]="exists()"
4227
+ [serverErrors]="serverErrors().fields"
4228
+ [resource]="resourceId()"
4229
+ />
4230
+ </div>
4231
+ }
4232
+ </div>
4233
+ }
4234
+
4235
+ @if (layoutMode() === 'tabs') {
4236
+ <div
4237
+ role="tablist"
4238
+ class="mb-4 flex flex-wrap gap-1 border-b border-neutral/20"
4239
+ (keydown)="onTablistKeydown($event)"
4240
+ >
4241
+ @for (section of sectionViews(); track section.id) {
4242
+ <button
4243
+ type="button"
4244
+ role="tab"
4245
+ [id]="tabId(section.id)"
4246
+ [class]="tabClass(section)"
4247
+ [attr.aria-selected]="activeSection() === section.id"
4248
+ [attr.tabindex]="activeSection() === section.id ? 0 : -1"
4249
+ (click)="selectSection(section.id)"
4250
+ >
4251
+ {{ section.label }}
4252
+ <!-- Progreso (§4.3, decisión A): el ✓ siempre que esté
4253
+ completa; el ⚠ recién después del primer Guardar.
4254
+ El glifo va oculto para lectores y el estado se
4255
+ cuenta aparte en texto. -->
4256
+ @switch (sectionProgress(section)) {
4257
+ @case ('problem') {
4258
+ <span class="ml-1 text-danger" aria-hidden="true">⚠</span>
4259
+ <span class="sr-only"> — con errores</span>
4260
+ }
4261
+ @case ('complete') {
4262
+ <span class="ml-1 text-success" aria-hidden="true">✓</span>
4263
+ <span class="sr-only"> — completa</span>
4264
+ }
4265
+ }
4266
+ </button>
4267
+ }
4268
+ </div>
4269
+ }
4270
+
4271
+ @if (layoutMode() === 'steps') {
4272
+ <ol aria-label="Progreso del formulario" class="mb-4 flex flex-wrap gap-2">
4273
+ @for (section of sectionViews(); track section.id; let index = $index) {
4274
+ <li
4275
+ class="rounded-full border px-3 py-1 text-sm"
4276
+ [class.border-primary]="activeSection() === section.id"
4277
+ [class.font-semibold]="activeSection() === section.id"
4278
+ [attr.aria-current]="activeSection() === section.id ? 'step' : null"
4279
+ >
4280
+ {{ index + 1 }}. {{ section.label }}
4281
+ </li>
4282
+ }
4283
+ </ol>
4284
+ }
4285
+
4286
+ @if (layoutMode() === 'tabs' || layoutMode() === 'steps') {
4287
+ @for (section of visibleSections(); track section.id) {
4288
+ <div
4289
+ role="tabpanel"
4290
+ [attr.id]="layoutMode() === 'steps' ? 'step-panel' : null"
4291
+ class="grid grid-cols-1 gap-x-4 sm:grid-cols-12"
4292
+ >
4293
+ @for (field of section.fields; track field.key) {
4294
+ <div [class]="colSpan(field)">
4295
+ <app-field-host
4296
+ [field]="field"
4297
+ [tree]="tree"
4298
+ [exists]="exists()"
4299
+ [serverErrors]="serverErrors().fields"
4300
+ [resource]="resourceId()"
4301
+ />
4302
+ </div>
4303
+ }
4304
+ </div>
4305
+ }
4306
+ @if (layoutMode() === 'steps' && sectionViews().length > 1) {
4307
+ <div class="mt-4 flex justify-between gap-3">
4308
+ <button
4309
+ app-button
4310
+ variant="outline"
4311
+ type="button"
4312
+ [disabled]="isFirstStep()"
4313
+ (click)="previousStep()"
4314
+ >
4315
+ Anterior
4316
+ </button>
4317
+ @if (!isLastStep()) {
4318
+ <button app-button type="button" (click)="nextStep()">Continuar</button>
4319
+ }
4320
+ </div>
4321
+ }
4322
+ } @else if (layoutMode() === 'columns') {
4323
+ <div
4324
+ [class]="
4325
+ layoutColumns() === 3
4326
+ ? 'grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3'
4327
+ : 'grid grid-cols-1 gap-4 md:grid-cols-2'
4328
+ "
4329
+ >
4330
+ @for (section of sectionViews(); track section.id) {
4331
+ <section class="rounded-xl border border-neutral/20 bg-surface p-4">
4332
+ <h2 class="mb-3 text-section font-semibold">{{ section.label }}</h2>
4333
+ <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
4334
+ @for (field of section.fields; track field.key) {
4335
+ <div [class]="colSpan(field)">
4336
+ <app-field-host
4337
+ [field]="field"
4338
+ [tree]="tree"
4339
+ [exists]="exists()"
4340
+ [serverErrors]="serverErrors().fields"
4341
+ [resource]="resourceId()"
4342
+ />
4343
+ </div>
4344
+ }
4345
+ </div>
4346
+ </section>
4347
+ }
4348
+ </div>
4349
+ } @else if (layoutMode() === 'sections') {
4350
+ <!-- Índice interno (§5.3): en escritorio, índice sticky a la
4351
+ izquierda; en móvil, chips horizontales. En tabs las
4352
+ pestañas ya hacen de índice, así que esto no se dibuja. -->
4353
+ <div class="flex flex-col gap-4 lg:flex-row lg:gap-6">
4354
+ <nav
4355
+ aria-label="Secciones"
4356
+ class="sticky top-4 hidden shrink-0 self-start lg:block lg:w-44"
4357
+ >
4358
+ <p
4359
+ class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral"
4360
+ aria-hidden="true"
4361
+ >
4362
+ Secciones
4363
+ </p>
4364
+ <ul class="space-y-1 border-l border-neutral/20 pl-3">
4365
+ @for (section of visibleSections(); track section.id) {
4366
+ <li>
4367
+ <button
4368
+ type="button"
4369
+ class="w-full rounded px-2 py-1 text-left text-sm hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
4370
+ [class.font-medium]="sectionProgress(section) === 'problem'"
4371
+ (click)="goToSection(section.id)"
4372
+ >
4373
+ {{ section.label }}
4374
+ @switch (sectionProgress(section)) {
4375
+ @case ('problem') {
4376
+ <span class="ml-1 text-danger" aria-hidden="true">⚠</span>
4377
+ <span class="sr-only"> — con errores</span>
4378
+ }
4379
+ @case ('complete') {
4380
+ <span class="ml-1 text-success" aria-hidden="true">✓</span>
4381
+ <span class="sr-only"> — completa</span>
4382
+ }
4383
+ }
4384
+ </button>
4385
+ </li>
4386
+ }
4387
+ </ul>
4388
+ </nav>
4389
+
4390
+ <div class="min-w-0 flex-1" [class.lg:grid]="layoutMode() === 'columns'">
4391
+ <nav
4392
+ aria-label="Secciones"
4393
+ class="-mx-1 mb-4 flex gap-2 overflow-x-auto px-1 pb-1 lg:hidden"
4394
+ >
4395
+ @for (section of visibleSections(); track section.id) {
4396
+ <button
4397
+ type="button"
4398
+ class="shrink-0 rounded-full border border-neutral/30 px-3 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
4399
+ [class.border-danger]="sectionProgress(section) === 'problem'"
4400
+ [class.text-danger]="sectionProgress(section) === 'problem'"
4401
+ [class.font-medium]="sectionProgress(section) === 'problem'"
4402
+ (click)="goToSection(section.id)"
4403
+ >
4404
+ {{ section.label }}
4405
+ </button>
4406
+ }
4407
+ </nav>
4408
+
4409
+ @for (section of visibleSections(); track section.id) {
4410
+ <details
4411
+ id="section-{{ section.id }}"
4412
+ class="mb-4 rounded-xl border border-neutral/20 bg-surface scroll-mt-4"
4413
+ [open]="isSectionOpen(section.id)"
4414
+ (toggle)="setSectionOpen(section.id, $event)"
4415
+ >
4416
+ <summary
4417
+ tabindex="-1"
4418
+ class="flex cursor-pointer items-center justify-between gap-2 rounded-xl px-4 py-3 text-section font-semibold hover:bg-neutral/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
4419
+ >
4420
+ <span>{{ section.label }}</span>
4421
+ @if (sectionProgress(section) === 'problem') {
4422
+ <span class="text-xs font-medium text-danger">Revisar</span>
4423
+ }
4424
+ </summary>
4425
+ <div class="grid grid-cols-1 gap-x-4 px-4 pb-4 sm:grid-cols-12">
4426
+ @for (field of section.fields; track field.key) {
4427
+ <div [class]="colSpan(field)">
4428
+ <app-field-host
4429
+ [field]="field"
4430
+ [tree]="tree"
4431
+ [exists]="exists()"
4432
+ [serverErrors]="serverErrors().fields"
4433
+ [resource]="resourceId()"
4434
+ />
4435
+ </div>
4436
+ }
4437
+ </div>
4438
+ </details>
4439
+ }
4440
+ </div>
4441
+ </div>
4442
+ }
4443
+
4444
+ <!-- Sticky (§5.1): en products son 19 campos y Cancelar/Guardar
4445
+ desaparecían al final del scroll. bg-surface es opaco: sin
4446
+ él el contenido vería pasar por detrás. -->
4447
+ <div
4448
+ class="sticky bottom-0 z-10 mt-6 flex flex-wrap items-center justify-end gap-3 border-t border-neutral/20 bg-surface pt-4 pb-[calc(1rem+env(safe-area-inset-bottom))]"
4449
+ >
4450
+ @if (canDelete()) {
4451
+ <div class="mr-auto">
4452
+ <button app-button variant="danger" type="button" (click)="remove()">
4453
+ Eliminar
4454
+ </button>
4455
+ </div>
4456
+ }
4457
+ @if (mode() !== 'create') {
4458
+ <!-- §5: descartar lo escrito y volver a lo del servidor. -->
4459
+ <button app-button variant="ghost" type="button" (click)="restablecer()">
4460
+ Restablecer
4461
+ </button>
4462
+ }
4463
+ <button app-button variant="outline" type="button" (click)="back()">
4464
+ Cancelar
4465
+ </button>
4466
+ <button app-button type="submit" [loading]="saving()" [disabled]="!canSave()">
4467
+ {{ saving() ? 'Guardando…' : 'Guardar' }}
4468
+ </button>
4469
+ </div>
4470
+ </form>
4471
+ }
4472
+ }
4473
+ }
4474
+ } @else {
4475
+ <app-empty-state
4476
+ title="Recurso no encontrado"
4477
+ message="No existe un schema con ese id en el registry."
4478
+ />
4479
+ }
4480
+
4481
+ <!-- «Crear nuevo» desde el dropdown de una relación (plan 6.4). Se monta
4482
+ y desmonta con el pedido: el Drawer devuelve el foco al botón que lo
4483
+ abrió al destruirse. -->
4484
+ @if (relationDrawer.request(); as createRequest) {
4485
+ @if (drawerSchema(createRequest.resource); as target) {
4486
+ <app-drawer-form
4487
+ [open]="true"
4488
+ [schema]="target"
4489
+ mode="create"
4490
+ [title]="'Crear ' + target.label"
4491
+ (close)="relationDrawer.clear()"
4492
+ (saved)="relationCreated($event, createRequest)"
4493
+ />
4494
+ }
4495
+ }
4496
+
4497
+ @if (createdWithSummary(); as createdRec) {
4498
+ @if (schema()?.postCreate; as postCreateConfig) {
4499
+ <app-post-create-modal
4500
+ [schema]="schema()!"
4501
+ [config]="postCreateConfig"
4502
+ [record]="createdRec"
4503
+ (closed)="onPostCreateClosed()"
4504
+ />
4505
+ }
4506
+ }
4507
+ `
4508
+ }]
4509
+ }],
4510
+ ctorParameters: () => []
4511
+ });
4512
+ export { resolveShare as _, DrawerForm as a, buildFormSchema as c, actionTypes as d, actionsFor as f, isTriggerAction as g, isShareAction as h, CopyShare as i, FieldHost as l, registerAction as m, form_view_exports as n, buildPayload as o, getAction as p, Modal as r, seedModel as s, FormView as t, actionType as u, RenderHost as v };
4513
+
4514
+ //# sourceMappingURL=tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map