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

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 (369) hide show
  1. package/README.md +222 -0
  2. package/ng-package.json +7 -0
  3. package/package.json +3 -15
  4. package/src/lib/actions/action-registry.spec.ts +52 -0
  5. package/src/lib/actions/action-registry.ts +126 -0
  6. package/src/lib/actions/action-service.spec.ts +215 -0
  7. package/src/lib/actions/action-service.ts +87 -0
  8. package/src/lib/actions/descriptors.spec.ts +155 -0
  9. package/src/lib/actions/descriptors.ts +154 -0
  10. package/src/lib/columns/column-cell.ts +33 -0
  11. package/src/lib/columns/column-defaults.spec.ts +99 -0
  12. package/src/lib/columns/column-defaults.ts +84 -0
  13. package/src/lib/columns/column-format.spec.ts +274 -0
  14. package/src/lib/columns/descriptors.ts +113 -0
  15. package/src/lib/columns/format-badge.ts +51 -0
  16. package/src/lib/columns/format-boolean.ts +58 -0
  17. package/src/lib/columns/format-currency.ts +30 -0
  18. package/src/lib/columns/format-date.ts +35 -0
  19. package/src/lib/columns/format-number.ts +32 -0
  20. package/src/lib/columns/format-text.ts +23 -0
  21. package/src/lib/columns/format-thumbnail.ts +63 -0
  22. package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
  23. package/src/lib/core/guards/auth.guard.spec.ts +88 -0
  24. package/src/lib/core/guards/auth.guard.ts +48 -0
  25. package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
  26. package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
  27. package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
  28. package/src/lib/core/models/api.model.ts +104 -0
  29. package/src/lib/core/models/schema-descriptions.ts +118 -0
  30. package/src/lib/core/models/schema.model.ts +1097 -0
  31. package/src/lib/core/registry/composition-validator.spec.ts +358 -0
  32. package/src/lib/core/registry/composition-validator.ts +270 -0
  33. package/src/lib/core/registry/field-validators.spec.ts +199 -0
  34. package/src/lib/core/registry/field-validators.ts +440 -0
  35. package/src/lib/core/registry/render-host.spec.ts +208 -0
  36. package/src/lib/core/registry/render-host.ts +646 -0
  37. package/src/lib/core/registry/render-registry.spec.ts +96 -0
  38. package/src/lib/core/registry/render-registry.ts +312 -0
  39. package/src/lib/core/registry/schema-catalog.ts +156 -0
  40. package/src/lib/core/services/api.service.spec.ts +1142 -0
  41. package/src/lib/core/services/api.service.ts +371 -0
  42. package/src/lib/core/services/auth.service.spec.ts +653 -0
  43. package/src/lib/core/services/auth.service.ts +503 -0
  44. package/src/lib/core/services/config.service.spec.ts +121 -0
  45. package/src/lib/core/services/config.service.ts +119 -0
  46. package/src/lib/core/services/data-source.service.spec.ts +179 -0
  47. package/src/lib/core/services/data-source.service.ts +142 -0
  48. package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
  49. package/src/lib/core/services/error-mapper.service.ts +49 -0
  50. package/src/lib/core/services/notification.service.ts +73 -0
  51. package/src/lib/core/services/relation-drawer.service.ts +41 -0
  52. package/src/lib/core/services/theme.service.spec.ts +106 -0
  53. package/src/lib/core/services/theme.service.ts +86 -0
  54. package/src/lib/core/services/transport.spec.ts +198 -0
  55. package/src/lib/core/services/transport.ts +164 -0
  56. package/src/lib/core/services/upload.service.ts +81 -0
  57. package/src/lib/core/testing/categories.schema.ts +125 -0
  58. package/src/lib/core/testing/orders.schema.ts +372 -0
  59. package/src/lib/core/testing/products.schema.ts +258 -0
  60. package/src/lib/core/testing/site.schema.ts +195 -0
  61. package/src/lib/core/testing/test-catalog.ts +21 -0
  62. package/src/lib/core/tokens.ts +112 -0
  63. package/src/lib/core/utils/clipboard.spec.ts +55 -0
  64. package/src/lib/core/utils/clipboard.ts +90 -0
  65. package/src/lib/core/utils/context-resolver.spec.ts +95 -0
  66. package/src/lib/core/utils/context-resolver.ts +101 -0
  67. package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
  68. package/src/lib/core/utils/response-unwrapper.ts +103 -0
  69. package/src/lib/core/utils/source-options.spec.ts +96 -0
  70. package/src/lib/core/utils/source-options.ts +54 -0
  71. package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
  72. package/src/lib/core/utils/template-interpolator.ts +56 -0
  73. package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
  74. package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
  75. package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
  76. package/src/lib/fields/boolean/boolean.ts +40 -0
  77. package/src/lib/fields/color/color.descriptor.ts +12 -0
  78. package/src/lib/fields/color/color.ts +34 -0
  79. package/src/lib/fields/currency/currency.descriptor.ts +12 -0
  80. package/src/lib/fields/currency/currency.ts +67 -0
  81. package/src/lib/fields/date/date.descriptor.ts +12 -0
  82. package/src/lib/fields/date/date.ts +36 -0
  83. package/src/lib/fields/descriptors.ts +122 -0
  84. package/src/lib/fields/display/display-badge.ts +65 -0
  85. package/src/lib/fields/display/display-date.ts +39 -0
  86. package/src/lib/fields/display/display-defaults.ts +84 -0
  87. package/src/lib/fields/display/display-file.ts +41 -0
  88. package/src/lib/fields/display/display-image.ts +54 -0
  89. package/src/lib/fields/display/display-list.ts +107 -0
  90. package/src/lib/fields/display/display-number.ts +41 -0
  91. package/src/lib/fields/display/display-text.ts +42 -0
  92. package/src/lib/fields/display/display.spec.ts +242 -0
  93. package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
  94. package/src/lib/fields/dropdown/dropdown.ts +45 -0
  95. package/src/lib/fields/email/email.descriptor.ts +12 -0
  96. package/src/lib/fields/email/email.ts +28 -0
  97. package/src/lib/fields/field-aria.ts +60 -0
  98. package/src/lib/fields/field-host/field-host.spec.ts +118 -0
  99. package/src/lib/fields/field-host/field-host.ts +47 -0
  100. package/src/lib/fields/field-node.ts +88 -0
  101. package/src/lib/fields/file/file.descriptor.ts +14 -0
  102. package/src/lib/fields/file/file.spec.ts +134 -0
  103. package/src/lib/fields/file/file.ts +281 -0
  104. package/src/lib/fields/group/group.descriptor.ts +21 -0
  105. package/src/lib/fields/group/group.ts +110 -0
  106. package/src/lib/fields/image/image.descriptor.ts +12 -0
  107. package/src/lib/fields/image/image.spec.ts +117 -0
  108. package/src/lib/fields/image/image.ts +281 -0
  109. package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
  110. package/src/lib/fields/key-value/key-value.ts +283 -0
  111. package/src/lib/fields/list/list.descriptor.ts +21 -0
  112. package/src/lib/fields/list/list.ts +376 -0
  113. package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
  114. package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
  115. package/src/lib/fields/multiselect/multiselect.ts +152 -0
  116. package/src/lib/fields/number/number.descriptor.ts +12 -0
  117. package/src/lib/fields/number/number.ts +40 -0
  118. package/src/lib/fields/phone/phone.descriptor.ts +12 -0
  119. package/src/lib/fields/phone/phone.ts +46 -0
  120. package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
  121. package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
  122. package/src/lib/fields/relation/relation.descriptor.ts +12 -0
  123. package/src/lib/fields/relation/relation.spec.ts +193 -0
  124. package/src/lib/fields/relation/relation.ts +224 -0
  125. package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
  126. package/src/lib/fields/rich-select/rich-select.ts +175 -0
  127. package/src/lib/fields/select/radio-mode.ts +25 -0
  128. package/src/lib/fields/select/select.descriptor.ts +12 -0
  129. package/src/lib/fields/select/select.spec.ts +162 -0
  130. package/src/lib/fields/select/select.ts +210 -0
  131. package/src/lib/fields/slug/slug.descriptor.ts +19 -0
  132. package/src/lib/fields/slug/slug.ts +67 -0
  133. package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
  134. package/src/lib/fields/string-list/string-list.ts +222 -0
  135. package/src/lib/fields/text/text.descriptor.ts +12 -0
  136. package/src/lib/fields/text/text.ts +35 -0
  137. package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
  138. package/src/lib/fields/textarea/textarea.spec.ts +117 -0
  139. package/src/lib/fields/textarea/textarea.ts +126 -0
  140. package/src/lib/fields/time/time.descriptor.ts +12 -0
  141. package/src/lib/fields/time/time.ts +28 -0
  142. package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
  143. package/src/lib/fields/url/url.descriptor.ts +12 -0
  144. package/src/lib/fields/url/url.ts +53 -0
  145. package/src/lib/gas-admin-ui.spec.ts +21 -0
  146. package/src/lib/gas-admin-ui.ts +9 -0
  147. package/src/lib/layouts/descriptors.ts +9 -0
  148. package/src/lib/pages/dashboard/dashboard.ts +167 -0
  149. package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
  150. package/src/lib/pages/detail-view/detail-view.ts +570 -0
  151. package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
  152. package/src/lib/pages/form-view/form-model.ts +88 -0
  153. package/src/lib/pages/form-view/form-schema.ts +255 -0
  154. package/src/lib/pages/form-view/form-view.ts +1622 -0
  155. package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
  156. package/src/lib/pages/list-view/list-view.ts +882 -0
  157. package/src/lib/pages/login/login.spec.ts +109 -0
  158. package/src/lib/pages/login/login.ts +227 -0
  159. package/src/lib/pages/resource-page/resource-page.ts +234 -0
  160. package/src/lib/shared/components/badge/badge.ts +35 -0
  161. package/src/lib/shared/components/button/button.ts +65 -0
  162. package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
  163. package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
  164. package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
  165. package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
  166. package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
  167. package/src/lib/shared/components/drawer/drawer.ts +172 -0
  168. package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
  169. package/src/lib/shared/components/modal/modal.spec.ts +149 -0
  170. package/src/lib/shared/components/modal/modal.ts +145 -0
  171. package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
  172. package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
  173. package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
  174. package/src/lib/shared/components/toast/toast.ts +58 -0
  175. package/src/lib/shared/utils/col-span.ts +25 -0
  176. package/src/lib/shared/utils/condition-evaluator.ts +53 -0
  177. package/src/lib/shared/utils/depends-on.ts +48 -0
  178. package/src/lib/shared/utils/dialog-focus.ts +28 -0
  179. package/src/lib/shared/utils/field-path.ts +49 -0
  180. package/src/lib/shared/utils/gap-sorting.ts +144 -0
  181. package/src/lib/shared/utils/seed.spec.ts +64 -0
  182. package/src/lib/shared/utils/seed.ts +149 -0
  183. package/src/lib/shared/utils/slugify.ts +13 -0
  184. package/src/lib/shared/utils/validators.ts +41 -0
  185. package/src/lib/shell/layout/layout.ts +63 -0
  186. package/src/lib/shell/shell.routes.spec.ts +59 -0
  187. package/src/lib/shell/shell.routes.ts +122 -0
  188. package/src/lib/shell/sidebar/sidebar.ts +139 -0
  189. package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
  190. package/src/lib/views/calendar-view/calendar-model.ts +426 -0
  191. package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
  192. package/src/lib/views/calendar-view/calendar-view.ts +568 -0
  193. package/src/lib/views/composer/composer-host.spec.ts +301 -0
  194. package/src/lib/views/composer/composer-host.ts +248 -0
  195. package/src/lib/views/composer/composer-imports.spec.ts +54 -0
  196. package/src/lib/views/composer/view-context.spec.ts +117 -0
  197. package/src/lib/views/composer/view-context.ts +109 -0
  198. package/src/lib/views/composer/view-slot.spec.ts +130 -0
  199. package/src/lib/views/composer/view-slot.ts +178 -0
  200. package/src/lib/views/descriptors.ts +46 -0
  201. package/src/lib/views/kanban-view/kanban-model.ts +173 -0
  202. package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
  203. package/src/lib/views/kanban-view/kanban-view.ts +327 -0
  204. package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
  205. package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
  206. package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
  207. package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
  208. package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
  209. package/src/lib/widgets/chart-line/chart-line.ts +219 -0
  210. package/src/lib/widgets/descriptors.ts +29 -0
  211. package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
  212. package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
  213. package/src/lib/widgets/metric-card/metric-card.ts +48 -0
  214. package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
  215. package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
  216. package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
  217. package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
  218. package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
  219. package/src/lib/widgets/record-list/record-list.ts +115 -0
  220. package/src/lib/widgets/series.spec.ts +42 -0
  221. package/src/lib/widgets/series.ts +56 -0
  222. package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
  223. package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
  224. package/src/lib/widgets/status-progress/status-progress.ts +104 -0
  225. package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
  226. package/src/lib/widgets/widget-host/widget-host.ts +99 -0
  227. package/src/public-api.ts +104 -0
  228. package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
  229. package/tsconfig.lib.json +13 -0
  230. package/tsconfig.lib.prod.json +11 -0
  231. package/tsconfig.spec.json +10 -0
  232. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
  233. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
  234. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
  235. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
  236. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
  237. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
  238. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
  239. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
  240. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
  241. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
  242. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
  243. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
  244. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
  245. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
  246. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
  247. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
  248. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
  249. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
  250. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
  251. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
  252. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
  253. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
  254. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
  255. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
  256. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
  257. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
  258. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
  259. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
  260. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
  261. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
  262. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
  263. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
  264. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
  265. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
  266. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
  267. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
  268. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
  269. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
  270. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
  271. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
  272. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
  273. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
  274. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
  275. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
  276. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
  277. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
  278. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
  279. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
  280. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
  281. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
  282. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
  283. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
  284. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
  285. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
  286. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
  287. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
  288. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
  289. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
  290. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
  291. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
  292. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
  293. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
  294. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
  295. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
  296. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
  297. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
  298. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
  299. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
  300. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
  301. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
  302. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
  303. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
  304. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
  305. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
  306. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
  307. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
  308. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
  309. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
  310. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
  311. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
  312. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
  313. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
  314. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
  315. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
  316. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
  317. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
  318. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
  319. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
  320. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
  321. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
  322. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
  323. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
  324. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
  325. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
  326. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
  327. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
  328. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
  329. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
  330. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
  331. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
  332. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
  333. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
  334. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
  335. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
  336. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
  337. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
  338. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
  339. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
  340. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
  341. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
  342. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
  343. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
  344. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
  345. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
  346. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
  347. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
  348. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
  349. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
  350. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
  351. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
  352. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
  353. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
  354. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
  355. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
  356. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
  357. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
  358. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
  359. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
  360. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
  361. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
  362. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
  363. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
  364. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
  365. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
  366. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
  367. package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
  368. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
  369. package/types/tinya-code-gas-admin-ui.d.ts +0 -3632
@@ -0,0 +1,281 @@
1
+ import { HttpEventType } from '@angular/common/http';
2
+ import {
3
+ Component,
4
+ computed,
5
+ DestroyRef,
6
+ ElementRef,
7
+ inject,
8
+ input,
9
+ model,
10
+ output,
11
+ signal,
12
+ viewChild,
13
+ } from '@angular/core';
14
+ import type { FormValueControl } from '@angular/forms/signals';
15
+
16
+ import type { ImageField } from '../../core/models/schema.model';
17
+ import { ApiError } from '../../core/models/api.model';
18
+ import { NotificationService } from '../../core/services/notification.service';
19
+ import { Button } from '../../shared/components/button/button';
20
+ import { UploadService } from '../../core/services/upload.service';
21
+
22
+ /**
23
+ * Carga de imagen (base.md §7): el valor es SOLO la URL pública.
24
+ *
25
+ * Control propio `FormValueControl<string>`: el nodo llega con `[formField]`
26
+ * desde el despachador y el `id` lo pone el host sobre este elemento (mismo
27
+ * patrón que `multiselect`), para que el `<label for>` del campo exista.
28
+ *
29
+ * Estados: vacío (zona punteada + arrastrar-soltar), subiendo (progreso; el
30
+ * valor no cambia hasta la respuesta), con imagen (preview + Reemplazar /
31
+ * Quitar / Ver en grande, y drop sobre el preview para reemplazar) y error
32
+ * (conserva la imagen anterior).
33
+ */
34
+ @Component({
35
+ selector: 'app-field-image',
36
+ imports: [Button],
37
+ template: ` @if (error(); as message) {
38
+ <p
39
+ class="mb-2 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-xs text-danger"
40
+ role="alert"
41
+ >
42
+ {{ message }}
43
+ </p>
44
+ }
45
+ @if (uploading()) {
46
+ <div class="rounded-xl border border-neutral/20 bg-surface p-3" data-testid="image-uploading">
47
+ <div class="h-1.5 w-full overflow-hidden rounded-full bg-neutral/20">
48
+ <div
49
+ class="h-full rounded-full bg-primary transition-all"
50
+ [style.width.%]="progress()"
51
+ ></div>
52
+ </div>
53
+ <p class="mt-1 text-xs text-neutral">Subiendo… {{ progress() }}%</p>
54
+ </div>
55
+ } @else if (value()) {
56
+ <div
57
+ class="rounded-xl border border-neutral/20 bg-surface p-3"
58
+ data-testid="image-preview"
59
+ (dragover)="$event.preventDefault()"
60
+ (drop)="onDrop($event)"
61
+ >
62
+ <img
63
+ [src]="value()"
64
+ [alt]="''"
65
+ [class]="previewClass()"
66
+ [style.aspect-ratio]="aspectRatio()"
67
+ />
68
+ <div class="mt-2 flex flex-wrap items-center gap-2">
69
+ <button
70
+ type="button"
71
+ app-button
72
+ variant="outline"
73
+ [disabled]="disabled()"
74
+ (click)="pickFile()"
75
+ >
76
+ Reemplazar
77
+ </button>
78
+ @if (!field().required) {
79
+ <button
80
+ type="button"
81
+ app-button
82
+ variant="outline"
83
+ [disabled]="disabled()"
84
+ (click)="clear()"
85
+ >
86
+ Quitar
87
+ </button>
88
+ }
89
+ <a
90
+ class="text-sm font-medium text-primary hover:underline"
91
+ [href]="value()"
92
+ target="_blank"
93
+ rel="noopener noreferrer"
94
+ >
95
+ Ver en grande
96
+ </a>
97
+ </div>
98
+ </div>
99
+ } @else {
100
+ <div
101
+ class="rounded-xl border-2 border-dashed border-neutral/30 p-6 text-center"
102
+ data-testid="image-dropzone"
103
+ (dragover)="$event.preventDefault()"
104
+ (drop)="onDrop($event)"
105
+ >
106
+ <p class="text-sm text-neutral">Arrastrá una imagen acá</p>
107
+ <button
108
+ type="button"
109
+ app-button
110
+ variant="outline"
111
+ class="mt-2"
112
+ [disabled]="disabled()"
113
+ (click)="pickFile()"
114
+ >
115
+ Subir imagen
116
+ </button>
117
+ </div>
118
+ }
119
+
120
+ <input
121
+ #fileInput
122
+ type="file"
123
+ class="sr-only"
124
+ [accept]="acceptAttr()"
125
+ [disabled]="disabled()"
126
+ (change)="onFileSelected($event)"
127
+ />`,
128
+ })
129
+ export class FieldImage implements FormValueControl<string> {
130
+ readonly field = input.required<ImageField>();
131
+ /** Recurso dueño de la imagen (firma de subida: carpeta de Cloudinary, §11.2). */
132
+ readonly resource = input('');
133
+
134
+ /** Sincronizado por el directive `FormField` del host. */
135
+ readonly value = model.required<string>();
136
+ readonly disabled = input(false);
137
+ /** Marca el campo como tocado (upload/clear/error), como el `blur` nativo. */
138
+ readonly touch = output<void>();
139
+
140
+ private readonly upload = inject(UploadService);
141
+ private readonly notifications = inject(NotificationService);
142
+ private readonly destroyRef = inject(DestroyRef);
143
+
144
+ protected readonly uploading = signal(false);
145
+ protected readonly progress = signal(0);
146
+ protected readonly error = signal<string | null>(null);
147
+
148
+ /** Literal en el código: Tailwind v4 genera las clases al escanearlo. */
149
+ protected readonly previewClass = computed(
150
+ () => `${this.field().previewSize ?? 'w-48'} rounded-lg object-cover`,
151
+ );
152
+ protected readonly aspectRatio = computed(() => this.field().aspectRatio ?? null);
153
+ protected readonly acceptAttr = computed(() => this.field().accept ?? 'image/*');
154
+
155
+ /**
156
+ * Template ref `#fileInput`: Angular NO auto-asigna las template refs a
157
+ * propiedades de clase (el campo `fileInput` quedaba `undefined` y
158
+ * `pickFile()` no hacía nada — el selector no se abría). Igual que en
159
+ * `modal.ts`: el tipo se expresa como genérico (`ElementRef<T>`).
160
+ */
161
+ private readonly fileInput = viewChild.required<ElementRef<HTMLInputElement>>('fileInput');
162
+ private uploadSub?: { unsubscribe(): void };
163
+
164
+ protected pickFile(): void {
165
+ this.fileInput().nativeElement.click();
166
+ }
167
+
168
+ protected clear(): void {
169
+ this.value.set('');
170
+ this.error.set(null);
171
+ this.touch.emit();
172
+ }
173
+
174
+ protected onFileSelected(event: Event): void {
175
+ const input = event.target as HTMLInputElement;
176
+ const file = input.files?.[0];
177
+ // Permite volver a elegir el mismo archivo tras un error.
178
+ input.value = '';
179
+ if (file) {
180
+ this.startUpload(file);
181
+ }
182
+ }
183
+
184
+ protected onDrop(event: Event): void {
185
+ event.preventDefault();
186
+ const file = (event as DragEvent).dataTransfer?.files?.[0];
187
+ if (file) {
188
+ this.startUpload(file);
189
+ }
190
+ }
191
+
192
+ /** Valida en cliente (accept + maxSizeMB) y sube con progreso. */
193
+ private startUpload(file: File): void {
194
+ if (this.disabled()) {
195
+ return;
196
+ }
197
+ const problem = this.validate(file);
198
+ if (problem !== null) {
199
+ this.error.set(problem);
200
+ this.touch.emit();
201
+ return;
202
+ }
203
+ this.error.set(null);
204
+ this.uploading.set(true);
205
+ this.progress.set(0);
206
+ this.touch.emit();
207
+
208
+ this.uploadSub?.unsubscribe();
209
+ // El nombre del campo viaja siempre: si declara `formats`, el backend
210
+ // firma ESA lista; sin formatos firma la lista global (R9.5.3).
211
+ this.uploadSub = this.upload
212
+ .uploadWithProgress(file, this.resource(), this.field().key)
213
+ .subscribe({
214
+ next: (event) => {
215
+ if (event.type === HttpEventType.UploadProgress) {
216
+ if (event.total) {
217
+ this.progress.set(Math.round((event.loaded / event.total) * 100));
218
+ }
219
+ return;
220
+ }
221
+ if (event.type === HttpEventType.Response) {
222
+ const url = event.body ?? '';
223
+ this.uploading.set(false);
224
+ if (url === '') {
225
+ this.error.set('No se pudo subir la imagen.');
226
+ return;
227
+ }
228
+ this.value.set(url);
229
+ this.notifications.success('Imagen subida.');
230
+ }
231
+ },
232
+ error: (err: unknown) => {
233
+ this.uploading.set(false);
234
+ // El backend explica el motivo (firma inválida, 429 rate-limit…).
235
+ this.error.set(
236
+ err instanceof ApiError
237
+ ? err.message
238
+ : 'No se pudo subir la imagen. Revisa la conexión e inténtalo de nuevo.',
239
+ );
240
+ },
241
+ });
242
+ this.destroyRef.onDestroy(() => this.uploadSub?.unsubscribe());
243
+ }
244
+
245
+ private validate(file: File): string | null {
246
+ if (!file.type.startsWith('image/')) {
247
+ return 'El archivo debe ser una imagen.';
248
+ }
249
+ const accept = this.field().accept;
250
+ if (accept !== undefined && !acceptMatches(accept, file)) {
251
+ return 'Formato no permitido para este campo.';
252
+ }
253
+ const maxSizeMB = this.field().maxSizeMB;
254
+ if (maxSizeMB !== undefined && file.size > maxSizeMB * 1024 * 1024) {
255
+ return `La imagen supera ${maxSizeMB} MB.`;
256
+ }
257
+ return null;
258
+ }
259
+ }
260
+
261
+ /** `accept` de `<input type=file>`: lista, `image/*` o extensión `.png`. */
262
+ function acceptMatches(accept: string, file: File): boolean {
263
+ for (const raw of accept.split(',')) {
264
+ const entry = raw.trim().toLowerCase();
265
+ if (entry === '') {
266
+ continue;
267
+ }
268
+ if (entry.startsWith('.')) {
269
+ if (file.name.toLowerCase().endsWith(entry)) {
270
+ return true;
271
+ }
272
+ } else if (entry.endsWith('/*')) {
273
+ if (file.type.startsWith(entry.slice(0, -1))) {
274
+ return true;
275
+ }
276
+ } else if (file.type === entry) {
277
+ return true;
278
+ }
279
+ }
280
+ return false;
281
+ }
@@ -0,0 +1,11 @@
1
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
2
+
3
+ /** Descriptor del campo `key-value` (R3.3). */
4
+ export const descriptor: RenderDescriptor = {
5
+ kind: 'field',
6
+ type: 'key-value',
7
+ since: '1.0',
8
+ load: () => import('./key-value').then((module) => module.FieldKeyValue),
9
+ seed: 'array',
10
+ surfaces: ['form', 'display'],
11
+ };
@@ -0,0 +1,283 @@
1
+ import { Component, computed, inject, input } from '@angular/core';
2
+ import { FormField } from '@angular/forms/signals';
3
+ import {
4
+ CdkDrag,
5
+ CdkDragDrop,
6
+ CdkDragHandle,
7
+ CdkDropList,
8
+ moveItemInArray,
9
+ } from '@angular/cdk/drag-drop';
10
+
11
+ import { LucideArrowDown, LucideArrowUp, LucideGripVertical, LucideTrash2 } from '@lucide/angular';
12
+ import type { KeyValueField, KeyValueItem } from '../../core/models/schema.model';
13
+ import { NotificationService } from '../../core/services/notification.service';
14
+ import { Button } from '../../shared/components/button/button';
15
+ import {
16
+ appendPosition,
17
+ computeMove,
18
+ GAP,
19
+ type PositionedItem,
20
+ } from '../../shared/utils/gap-sorting';
21
+ import { FieldAria } from '../field-aria';
22
+ import { childTree, itemTree, listTree, type RootTree } from '../field-node';
23
+
24
+ /**
25
+ * Pares clave→valor (base.md §9 «ficha técnica»). Hoja: tabla con sus
26
+ * propios `<input [formField]>` (nodos `key`/`value` del ítem) y no toca
27
+ * FieldHost. `position` se calcula con gap-sorting igual que en `list`.
28
+ */
29
+ @Component({
30
+ selector: 'app-field-key-value',
31
+ imports: [
32
+ FieldAria,
33
+ Button,
34
+ CdkDrag,
35
+ CdkDragHandle,
36
+ CdkDropList,
37
+ FormField,
38
+ LucideArrowDown,
39
+ LucideArrowUp,
40
+ LucideGripVertical,
41
+ LucideTrash2,
42
+ ],
43
+ template: `
44
+ @if (items().length === 0) {
45
+ <p
46
+ class="rounded-lg border border-dashed border-neutral/30 px-4 py-6 text-center text-sm text-neutral"
47
+ >
48
+ {{ field().emptyText ?? 'Sin elementos.' }}
49
+ </p>
50
+ } @else {
51
+ <div class="overflow-x-auto rounded-xl border border-neutral/20 bg-surface">
52
+ <table class="w-full text-sm">
53
+ <thead>
54
+ <tr class="border-b border-neutral/20 text-left text-xs font-medium text-neutral">
55
+ <th class="w-10 px-3 py-2" scope="col"><span class="sr-only">Orden</span></th>
56
+ <th class="px-3 py-2" scope="col">Clave</th>
57
+ <th class="px-3 py-2" scope="col">Valor</th>
58
+ <th class="w-28 px-3 py-2" scope="col"><span class="sr-only">Acciones</span></th>
59
+ </tr>
60
+ </thead>
61
+ <tbody
62
+ cdkDropList
63
+ [cdkDropListData]="items()"
64
+ [cdkDropListDisabled]="!sortable()"
65
+ (cdkDropListDropped)="onDrop($event)"
66
+ >
67
+ @for (item of items(); track $index; let i = $index) {
68
+ <tr
69
+ class="border-b border-neutral/10 last:border-b-0"
70
+ cdkDrag
71
+ [cdkDragDisabled]="!sortable()"
72
+ >
73
+ <td class="px-3 py-2 align-middle">
74
+ @if (sortable()) {
75
+ <button
76
+ type="button"
77
+ app-button
78
+ variant="ghost"
79
+ size="icon"
80
+ class="cursor-grab"
81
+ cdkDragHandle
82
+ [attr.aria-label]="'Reordenar fila ' + (i + 1)"
83
+ title="Arrastrar para reordenar"
84
+ >
85
+ <svg lucideGripVertical size="16" />
86
+ </button>
87
+ }
88
+ </td>
89
+ <td class="px-3 py-2">
90
+ <input
91
+ class="field-input"
92
+ type="text"
93
+ placeholder="Clave"
94
+ [attr.aria-label]="'Clave, fila ' + (i + 1)"
95
+ [id]="idPrefix() + '-key-' + i"
96
+ [formField]="fieldNode(i, 'key')"
97
+ />
98
+ </td>
99
+ <td class="px-3 py-2">
100
+ <input
101
+ class="field-input"
102
+ type="text"
103
+ placeholder="Valor"
104
+ [attr.aria-label]="'Valor, fila ' + (i + 1)"
105
+ [id]="idPrefix() + '-value-' + i"
106
+ [formField]="fieldNode(i, 'value')"
107
+ />
108
+ </td>
109
+ <td class="px-3 py-2">
110
+ <span class="flex items-center justify-end gap-2 lg:gap-1">
111
+ <button
112
+ type="button"
113
+ app-button
114
+ variant="ghost"
115
+ size="icon"
116
+ [disabled]="i === 0"
117
+ [attr.aria-label]="'Subir fila ' + (i + 1)"
118
+ (click)="move(i, -1)"
119
+ >
120
+ <svg lucideArrowUp size="16" />
121
+ </button>
122
+ <button
123
+ type="button"
124
+ app-button
125
+ variant="ghost"
126
+ size="icon"
127
+ [disabled]="i === items().length - 1"
128
+ [attr.aria-label]="'Bajar fila ' + (i + 1)"
129
+ (click)="move(i, 1)"
130
+ >
131
+ <svg lucideArrowDown size="16" />
132
+ </button>
133
+ <button
134
+ type="button"
135
+ app-button
136
+ variant="ghost"
137
+ size="icon"
138
+ [disabled]="!canRemove()"
139
+ [attr.aria-label]="'Eliminar fila ' + (i + 1)"
140
+ (click)="remove(i)"
141
+ >
142
+ <svg lucideTrash2 size="16" />
143
+ </button>
144
+ </span>
145
+ </td>
146
+ </tr>
147
+ }
148
+ </tbody>
149
+ </table>
150
+ </div>
151
+ }
152
+
153
+ <div class="mt-3">
154
+ <button app-button variant="outline" type="button" [disabled]="!canAdd()" (click)="add()">
155
+ + {{ field().addLabel ?? 'Agregar especificación' }}
156
+ </button>
157
+ </div>
158
+ `,
159
+ })
160
+ export class FieldKeyValue {
161
+ readonly field = input.required<KeyValueField>();
162
+ readonly tree = input.required<RootTree>();
163
+
164
+ private readonly notifications = inject(NotificationService);
165
+
166
+ private readonly itemsNode = computed(() =>
167
+ listTree<KeyValueItem>(this.tree(), this.field().key),
168
+ );
169
+ private readonly state = computed(() => this.itemsNode()());
170
+ /** Prefijo estable del campo para los id de cada celda (§8). */
171
+ protected readonly idPrefix = computed(() => this.state().name());
172
+ protected readonly items = computed(() => this.state().value() ?? []);
173
+
174
+ protected readonly sortable = computed(() => this.field().sortable ?? true);
175
+
176
+ /** Nodo de `key`/`value` del ítem; se llama fresco en cada render (NG01904). */
177
+ protected fieldNode(
178
+ index: number,
179
+ column: 'key' | 'value',
180
+ ): ReturnType<typeof childTree<string>> {
181
+ return childTree<string>(itemTree(this.itemsNode(), index), column);
182
+ }
183
+
184
+ protected canAdd(): boolean {
185
+ const max = this.field().max;
186
+ return max === undefined || this.items().length < max;
187
+ }
188
+
189
+ protected canRemove(): boolean {
190
+ return this.items().length > (this.field().min ?? 0);
191
+ }
192
+
193
+ protected add(): void {
194
+ if (!this.canAdd()) {
195
+ return;
196
+ }
197
+ const positionKey = this.field().positionField;
198
+ const row: KeyValueItem & Record<string, unknown> = { key: '', value: '' };
199
+ if (positionKey !== undefined) {
200
+ row[positionKey] = appendPosition(this.positioned());
201
+ }
202
+ this.updateItems((next) => {
203
+ next.push(row);
204
+ });
205
+ }
206
+
207
+ protected move(index: number, direction: -1 | 1): void {
208
+ const target = index + direction;
209
+ if (target < 0 || target >= this.items().length) {
210
+ return;
211
+ }
212
+ this.applyMove(index, target);
213
+ }
214
+
215
+ protected onDrop(event: CdkDragDrop<KeyValueItem[]>): void {
216
+ if (event.previousIndex === event.currentIndex) {
217
+ return;
218
+ }
219
+ this.applyMove(event.previousIndex, event.currentIndex);
220
+ }
221
+
222
+ protected async remove(index: number): Promise<void> {
223
+ if (!this.canRemove()) {
224
+ return;
225
+ }
226
+ const row = this.items()[index];
227
+ if (row !== undefined && (row.key.trim() !== '' || row.value.trim() !== '')) {
228
+ const accepted = await this.notifications.confirm({
229
+ title: `Eliminar fila ${index + 1}`,
230
+ message: 'Esta acción no se puede deshacer.',
231
+ confirmLabel: 'Eliminar',
232
+ cancelLabel: 'Cancelar',
233
+ });
234
+ if (!accepted) {
235
+ return;
236
+ }
237
+ }
238
+ this.updateItems((next) => {
239
+ next.splice(index, 1);
240
+ });
241
+ }
242
+
243
+ /** Mueve y recalcula `position` (base.md §8): cambios antes del splice. */
244
+ private applyMove(from: number, to: number): void {
245
+ const positionKey = this.field().positionField;
246
+ this.updateItems((next) => {
247
+ if (positionKey !== undefined) {
248
+ const result = computeMove(this.positioned(), String(from), to);
249
+ for (const change of result.changes) {
250
+ const row = next[Number(change.key)] as
251
+ (KeyValueItem & Record<string, unknown>) | undefined;
252
+ if (row !== undefined) {
253
+ row[positionKey] = change.position;
254
+ }
255
+ }
256
+ }
257
+ moveItemInArray(next, from, to);
258
+ });
259
+ }
260
+
261
+ private positioned(): PositionedItem[] {
262
+ const positionKey = this.field().positionField;
263
+ return this.items().map((item, index) => {
264
+ const raw =
265
+ positionKey !== undefined
266
+ ? (item as unknown as Record<string, unknown>)[positionKey]
267
+ : undefined;
268
+ // Fallback: un registro viejo sin `position` se numera como rebalanceo.
269
+ return {
270
+ key: String(index),
271
+ position: typeof raw === 'number' ? raw : (index + 1) * GAP,
272
+ };
273
+ });
274
+ }
275
+
276
+ private updateItems(mutator: (next: KeyValueItem[]) => void): void {
277
+ this.state().controlValue.update((current) => {
278
+ const next = [...current];
279
+ mutator(next);
280
+ return next;
281
+ });
282
+ }
283
+ }
@@ -0,0 +1,21 @@
1
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
2
+
3
+ /** Descriptor del campo `list` (R3.3). */
4
+ export const descriptor: RenderDescriptor = {
5
+ kind: 'field',
6
+ type: 'list',
7
+ since: '1.0',
8
+ load: () => import('./list').then((module) => module.FieldList),
9
+ seed: 'array',
10
+ surfaces: ['form', 'display'],
11
+ // Compuesto: además de su árbol dibuja el template de cada hijo y hereda
12
+ // el `exists` del nivel para no reactivar el borrado de filas (R3.6).
13
+ inputs: (config, extras) => ({
14
+ field: config,
15
+ tree: extras.ctx.tree,
16
+ childTemplate: extras.childTemplate,
17
+ serverErrors: extras.ctx.serverErrors,
18
+ pathPrefix: extras.ctx.pathPrefix,
19
+ exists: extras.ctx.exists,
20
+ }),
21
+ };