@tinya-code/gas-admin-ui 0.2.0-alpha.15 → 0.2.0-alpha.18

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