@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,358 @@
1
+ import { setSchemaCatalog } from './schema-catalog';
2
+ import { testCatalog } from '../testing/test-catalog';
3
+
4
+ setSchemaCatalog(testCatalog);
5
+
6
+ import { describe, expect, it } from 'vitest';
7
+
8
+ import type {
9
+ DataSource,
10
+ NavigationTarget,
11
+ ResourceSchema,
12
+ SelectionBinding,
13
+ UiViewTabs,
14
+ ViewChildNode,
15
+ } from '../models/schema.model';
16
+ import {
17
+ MAX_DEPTH,
18
+ MAX_NODES,
19
+ SchemaCompositionError,
20
+ validateComposition,
21
+ } from './composition-validator';
22
+
23
+ /** Schema sintético mínimo para ejercitar el validador (D2 · R9.9.2). */
24
+ function esquemaCon(uiViews: NonNullable<ResourceSchema['uiViews']>): ResourceSchema {
25
+ return {
26
+ id: 'orders',
27
+ label: 'Pedidos',
28
+ labelPlural: 'Pedidos',
29
+ kind: 'collection',
30
+ endpoint: {},
31
+ keyField: 'id',
32
+ titleField: 'title',
33
+ listColumns: [],
34
+ fields: [],
35
+ uiViews,
36
+ };
37
+ }
38
+
39
+ function tabs(children: ViewChildNode[]): UiViewTabs {
40
+ return { type: 'tabs', path: 'panel', config: { children } };
41
+ }
42
+
43
+ function hijo(id: string, type = 'list'): ViewChildNode {
44
+ return { id, label: `Hijo ${id}`, type };
45
+ }
46
+
47
+ /** Exige un `SchemaCompositionError` cuyo mensaje nombra todos los fragmentos. */
48
+ function expectaError(fn: () => void, ...fragmentos: string[]): void {
49
+ let error: unknown;
50
+ try {
51
+ fn();
52
+ } catch (caught) {
53
+ error = caught;
54
+ }
55
+ expect(error, 'se esperaba un SchemaCompositionError').toBeInstanceOf(SchemaCompositionError);
56
+ for (const fragmento of fragmentos) {
57
+ expect((error as Error).message).toContain(fragmento);
58
+ }
59
+ }
60
+
61
+ describe('composition-validator', () => {
62
+ it('acepta una composición heterogénea válida', () => {
63
+ const schema = esquemaCon({
64
+ panel: tabs([
65
+ {
66
+ id: 'cal',
67
+ label: 'Calendario',
68
+ type: 'calendar',
69
+ config: { startField: 'pickup_date', titleField: 'customer_name' },
70
+ },
71
+ {
72
+ id: 'board',
73
+ label: 'Tablero',
74
+ type: 'kanban',
75
+ config: { groupField: 'status', columns: [] },
76
+ },
77
+ { id: 'rows', label: 'Filas', type: 'list' },
78
+ ]),
79
+ list: { type: 'list' },
80
+ });
81
+
82
+ expect(() => validateComposition(schema)).not.toThrow();
83
+ });
84
+
85
+ it('deja pasar un schema sin composición (uiViews base)', () => {
86
+ const schema = esquemaCon({
87
+ list: { type: 'list' },
88
+ create: { type: 'form', path: 'new' },
89
+ detail: { type: 'detail', path: ':key' },
90
+ });
91
+
92
+ expect(() => validateComposition(schema)).not.toThrow();
93
+ });
94
+
95
+ it('falla con id duplicado nombrando contenedor e id', () => {
96
+ const schema = esquemaCon({ panel: tabs([hijo('cal'), hijo('cal')]) });
97
+
98
+ expectaError(() => validateComposition(schema), 'orders.panel', 'cal', 'duplicado');
99
+ });
100
+
101
+ it('falla con un hijo sin id nombrando el contenedor', () => {
102
+ const sinId = { label: 'Sin id', type: 'list' } as ViewChildNode;
103
+ const schema = esquemaCon({ panel: tabs([sinId]) });
104
+
105
+ expectaError(() => validateComposition(schema), 'orders.panel', 'sin id');
106
+ });
107
+
108
+ it('falla con tipo de hijo sin descriptor nombrando recurso, contenedor e hijo (get, no resolve)', () => {
109
+ const schema = esquemaCon({ panel: tabs([hijo('cal', 'heatmap')]) });
110
+
111
+ expectaError(() => validateComposition(schema), 'orders', 'panel', 'cal', 'heatmap');
112
+ });
113
+
114
+ // ── Ciclos y refs (R9.9.2 · los `ref` hacen posible el ciclo) ──────────────
115
+
116
+ it('detecta un ciclo directo por ref nombrando hijo y contenedor', () => {
117
+ const schema = esquemaCon({
118
+ panel: tabs([{ id: 'self', label: 'Autorreferencia', ref: 'panel' }]),
119
+ });
120
+
121
+ expectaError(() => validateComposition(schema), 'orders.panel', 'self', 'ciclo');
122
+ });
123
+
124
+ it('detecta un ciclo transitivo por ref', () => {
125
+ const schema = esquemaCon({
126
+ a: tabs([{ id: 'ab', label: 'AB', ref: 'b' }]),
127
+ b: tabs([{ id: 'ba', label: 'BA', ref: 'a' }]),
128
+ });
129
+
130
+ expectaError(() => validateComposition(schema), 'ba', 'ciclo');
131
+ });
132
+
133
+ it('falla con un ref roto nombrando el hijo y el destino inexistente', () => {
134
+ const schema = esquemaCon({
135
+ panel: tabs([{ id: 'rows', label: 'Filas', ref: 'no-existe' }]),
136
+ });
137
+
138
+ expectaError(() => validateComposition(schema), 'orders.panel', 'rows', 'no-existe');
139
+ });
140
+
141
+ // ── Topes configurables (D2: MAX_DEPTH / MAX_NODES) ────────────────────────
142
+
143
+ it('acepta un anidamiento justo en el límite de profundidad', () => {
144
+ // a → b → c: los hijos de `c` quedan en profundidad MAX_DEPTH.
145
+ const schema = esquemaCon({
146
+ a: tabs([{ id: 'n1', label: 'N1', ref: 'b' }]),
147
+ b: tabs([{ id: 'n2', label: 'N2', ref: 'c' }]),
148
+ c: tabs([{ id: 'n3', label: 'N3', type: 'list' }]),
149
+ });
150
+
151
+ expect(() => validateComposition(schema)).not.toThrow();
152
+ });
153
+
154
+ it('falla con profundidad sobre el límite nombrando el hijo y la profundidad observada', () => {
155
+ const schema = esquemaCon({
156
+ a: tabs([{ id: 'n1', label: 'N1', ref: 'b' }]),
157
+ b: tabs([{ id: 'n2', label: 'N2', ref: 'c' }]),
158
+ c: tabs([{ id: 'n3', label: 'N3', ref: 'd' }]),
159
+ d: tabs([{ id: 'n4', label: 'N4', type: 'list' }]),
160
+ });
161
+
162
+ expectaError(() => validateComposition(schema), 'n4', '4', String(MAX_DEPTH));
163
+ });
164
+
165
+ it('valida la profundidad ANIDADA dentro de un contenedor inline (no sólo vía ref)', () => {
166
+ // panel → n1 → n2 → n3 (todos tabs inline) ⇒ los hijos de n3 están en
167
+ // profundidad 4 y deben reventar ANTES del render (D2).
168
+ const schema = esquemaCon({
169
+ panel: tabs([
170
+ {
171
+ id: 'n1',
172
+ label: 'N1',
173
+ type: 'tabs',
174
+ config: {
175
+ children: [
176
+ {
177
+ id: 'n2',
178
+ label: 'N2',
179
+ type: 'tabs',
180
+ config: {
181
+ children: [
182
+ {
183
+ id: 'n3',
184
+ label: 'N3',
185
+ type: 'tabs',
186
+ config: { children: [hijo('n4')] },
187
+ },
188
+ ],
189
+ },
190
+ },
191
+ ],
192
+ },
193
+ },
194
+ ]),
195
+ });
196
+
197
+ expectaError(() => validateComposition(schema), 'n4', 'profundidad', String(MAX_DEPTH));
198
+ });
199
+
200
+ it('detecta un ciclo que atraviesa un contenedor inline (D2)', () => {
201
+ const schema = esquemaCon({
202
+ panel: tabs([
203
+ {
204
+ id: 'hijo',
205
+ label: 'Hijo',
206
+ type: 'tabs',
207
+ config: { children: [{ id: 'loop', label: 'Loop', ref: 'panel' }] },
208
+ },
209
+ ]),
210
+ });
211
+
212
+ expectaError(() => validateComposition(schema), 'ciclo', 'panel');
213
+ });
214
+
215
+ it('acepta una composición con exactamente MAX_NODES nodos', () => {
216
+ const hijos = Array.from({ length: MAX_NODES }, (_, i) => hijo(`n${i}`));
217
+ const schema = esquemaCon({ panel: tabs(hijos) });
218
+
219
+ expect(() => validateComposition(schema)).not.toThrow();
220
+ });
221
+
222
+ it('falla al superar MAX_NODES nombrando el tope', () => {
223
+ const hijos = Array.from({ length: MAX_NODES + 1 }, (_, i) => hijo(`n${i}`));
224
+ const schema = esquemaCon({ panel: tabs(hijos) });
225
+
226
+ expectaError(() => validateComposition(schema), 'límite de', String(MAX_NODES));
227
+ });
228
+
229
+ // ── Destino de navegación (spec view-navigation: URL cruda rechazada) ──────
230
+
231
+ it('rechaza una URL cruda como destino nombrando recurso, contenedor e hijo', () => {
232
+ const schema = esquemaCon({
233
+ panel: tabs([
234
+ {
235
+ id: 'rows',
236
+ label: 'Filas',
237
+ type: 'list',
238
+ select: {
239
+ on: 'open',
240
+ mode: 'route',
241
+ target: 'https://evil.com/x' as unknown as NavigationTarget,
242
+ },
243
+ },
244
+ ]),
245
+ });
246
+
247
+ expectaError(
248
+ () => validateComposition(schema),
249
+ 'orders.panel',
250
+ 'rows',
251
+ 'URL cruda',
252
+ 'https://evil.com/x',
253
+ );
254
+ });
255
+
256
+ it('rechaza una URL cruda declarada en una uiView raíz', () => {
257
+ const schema = esquemaCon({
258
+ list: {
259
+ type: 'list',
260
+ select: {
261
+ on: 'open',
262
+ mode: 'route',
263
+ target: '/dashboard' as unknown as NavigationTarget,
264
+ },
265
+ },
266
+ });
267
+
268
+ expectaError(() => validateComposition(schema), 'orders.list', 'URL cruda', '/dashboard');
269
+ });
270
+
271
+ function esquemaConSelect(target: NavigationTarget): ResourceSchema {
272
+ return esquemaCon({
273
+ list: { type: 'list', select: { on: 'open', mode: 'route', target } },
274
+ });
275
+ }
276
+
277
+ it('valida que el destino nombre un recurso y una uiView declarados', () => {
278
+ expectaError(
279
+ () => validateComposition(esquemaConSelect({ resource: 'fantasma', view: 'detail' })),
280
+ 'orders.list',
281
+ 'fantasma',
282
+ );
283
+ expectaError(
284
+ () => validateComposition(esquemaConSelect({ resource: 'orders', view: 'no-existe' })),
285
+ 'orders.list',
286
+ 'no-existe',
287
+ );
288
+ });
289
+
290
+ it('valida los params declarados contra la ruta del destino', () => {
291
+ // Param que la ruta del destino no tiene.
292
+ expectaError(
293
+ () =>
294
+ validateComposition(
295
+ esquemaConSelect({ resource: 'orders', view: 'detail', params: ['nope'] }),
296
+ ),
297
+ 'nope',
298
+ );
299
+ // Falta declarar el param que la ruta `orders/:key` exige.
300
+ expectaError(
301
+ () => validateComposition(esquemaConSelect({ resource: 'orders', view: 'detail' })),
302
+ 'key',
303
+ );
304
+ });
305
+
306
+ it('acepta un destino declarado con resource + view + params válidos', () => {
307
+ const schema = esquemaConSelect({
308
+ resource: 'orders',
309
+ view: 'detail',
310
+ params: ['key'],
311
+ });
312
+
313
+ expect(() => validateComposition(schema)).not.toThrow();
314
+ });
315
+
316
+ // ── Fuente de filas incompatible (R9.9.2 · "incompatible sources") ─────────
317
+
318
+ function esquemaConSource(source: DataSource): ResourceSchema {
319
+ return esquemaCon({
320
+ panel: tabs([
321
+ {
322
+ id: 'cal',
323
+ label: 'Calendario',
324
+ type: 'calendar',
325
+ config: { startField: 'pickup_date', titleField: 'customer_name', source },
326
+ },
327
+ ]),
328
+ });
329
+ }
330
+
331
+ it('falla con una source sobre un recurso no declarado', () => {
332
+ expectaError(
333
+ () => validateComposition(esquemaConSource({ resource: 'fantasma' })),
334
+ 'orders.panel',
335
+ 'cal',
336
+ 'fantasma',
337
+ );
338
+ });
339
+
340
+ it('falla con una source sobre una vista no declarada del recurso', () => {
341
+ expectaError(
342
+ () => validateComposition(esquemaConSource({ resource: 'orders', view: 'vista-fantasma' })),
343
+ 'vista-fantasma',
344
+ );
345
+ });
346
+
347
+ it('acepta una source con recurso declarado y sin vista', () => {
348
+ expect(() => validateComposition(esquemaConSource({ resource: 'orders' }))).not.toThrow();
349
+ });
350
+
351
+ it('un binding mode none/inline no exige target', () => {
352
+ const schema = esquemaCon({
353
+ list: { type: 'list', select: { on: 'open', mode: 'inline' } satisfies SelectionBinding },
354
+ });
355
+
356
+ expect(() => validateComposition(schema)).not.toThrow();
357
+ });
358
+ });
@@ -0,0 +1,270 @@
1
+ import { getSchema } from './schema-catalog';
2
+ import type {
3
+ DataSource,
4
+ NavigationTarget,
5
+ ResourceSchema,
6
+ SelectionBinding,
7
+ UiViewDescriptor,
8
+ ViewChildNode,
9
+ } from '../models/schema.model';
10
+ import { get } from './render-registry';
11
+
12
+ /**
13
+ * Validador de composición de vistas (R9.9.2 · D2/D3).
14
+ *
15
+ * Una composición (un `uiView` contenedor con `config.children`, p. ej.
16
+ * `tabs`) se valida ANTES del render con esta única función pura y síncrona:
17
+ *
18
+ * - identidad de hijos (`id` presente y único entre hermanos);
19
+ * - tipos resueltos con `get('view', type)` — **nunca** `resolve()`, cuyo
20
+ * `fallback` silencioso dejaría un hijo inválido pintándose de todos modos;
21
+ * - `ref` rotos y ciclos (directos o transitivos) que los `ref` permiten;
22
+ * - profundidad y cantidad de nodos con topes configurables;
23
+ * - destinos de navegación (`resource` + `view` + params) y fuentes de
24
+ * filas — una URL cruda nunca es un destino válido;
25
+ *
26
+ * El llamante es D3: `resourceRoutes()` en `shell.routes.ts` (fail-loud al
27
+ * arrancar) y el guardián de montaje del composer. Cualquier violación lanza
28
+ * `SchemaCompositionError` con un mensaje legible que nombra recurso,
29
+ * contenedor e hijo: nada se oculta ni se sustituye en silencio.
30
+ *
31
+ * El tipo de una `uiView` RAÍZ no se chequea acá: eso ya lo hace
32
+ * `resourceRoutes()` con su error genérico de descriptor inexistente. Este
33
+ * validador cubre lo que ese recorrido no ve: la estructura de los hijos.
34
+ */
35
+
36
+ /** Profundidad máxima de anidamiento de contenedores (hijos raíz = 1). */
37
+ export const MAX_DEPTH = 3;
38
+
39
+ /** Cantidad máxima de nodos-hijo declarados en la composición de un schema. */
40
+ export const MAX_NODES = 50;
41
+
42
+ /** Violación del contrato de composición, detectada antes del render. */
43
+ export class SchemaCompositionError extends Error {
44
+ constructor(message: string) {
45
+ super(message);
46
+ this.name = 'SchemaCompositionError';
47
+ }
48
+ }
49
+
50
+ /** Estado del recorrido: cuenta de nodos y camino actual (detección de ciclos). */
51
+ interface Estado {
52
+ readonly esquema: ResourceSchema;
53
+ nodos: number;
54
+ readonly contados: Set<ViewChildNode>;
55
+ readonly camino: Set<string>;
56
+ }
57
+
58
+ /** `config` de la vista cuando la hay (`tabs` es el único contenedor hoy). */
59
+ function configDe(vista: UiViewDescriptor | ViewChildNode): unknown {
60
+ return 'config' in vista ? vista.config : undefined;
61
+ }
62
+
63
+ /** Hijos declarados si la vista es un contenedor; `undefined` si no lo es. */
64
+ function hijosDe(vista: UiViewDescriptor | ViewChildNode): ViewChildNode[] | undefined {
65
+ const hijos = (configDe(vista) as { children?: unknown } | undefined)?.children;
66
+ return Array.isArray(hijos) ? (hijos as ViewChildNode[]) : undefined;
67
+ }
68
+
69
+ /**
70
+ * Valida TODA la composición de un schema. Lanza `SchemaCompositionError`
71
+ * en la primera violación; no lanzar significa «apto para renderizar».
72
+ */
73
+ export function validateComposition(schema: ResourceSchema): void {
74
+ const estado: Estado = { esquema: schema, nodos: 0, contados: new Set(), camino: new Set() };
75
+ for (const [nombre, vista] of Object.entries(schema.uiViews ?? {})) {
76
+ estado.camino.add(nombre);
77
+ validarContenido(nombre, vista, 1, estado);
78
+ estado.camino.delete(nombre);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Valida una `uiView` ya resuelta (raíz o referenciada por `ref`): sus
84
+ * bindings, su fuente de filas y —si es contenedor— sus hijos.
85
+ * `profundidadHijos` es la profundidad que tendrán sus hijos directos.
86
+ */
87
+ function validarContenido(
88
+ nombre: string,
89
+ vista: UiViewDescriptor,
90
+ profundidadHijos: number,
91
+ estado: Estado,
92
+ ): void {
93
+ const donde = `${estado.esquema.id}.${nombre}`;
94
+ validarSelect(donde, (vista as { select?: SelectionBinding }).select);
95
+ validarSource(donde, (configDe(vista) as { source?: DataSource } | undefined)?.source);
96
+ const hijos = hijosDe(vista);
97
+ if (!hijos) return;
98
+ validarHijos(nombre, hijos, profundidadHijos, estado);
99
+ }
100
+
101
+ /** Valida la lista de hijos de un contenedor: identidad única entre hermanos. */
102
+ function validarHijos(
103
+ contenedor: string,
104
+ hijos: readonly ViewChildNode[],
105
+ profundidad: number,
106
+ estado: Estado,
107
+ ): void {
108
+ const ids = new Set<string>();
109
+ for (const hijo of hijos) {
110
+ const id = (hijo as { id?: unknown }).id;
111
+ if (typeof id !== 'string' || id === '') {
112
+ throw new SchemaCompositionError(`${estado.esquema.id}.${contenedor}: hijo sin id`);
113
+ }
114
+ if (ids.has(id)) {
115
+ throw new SchemaCompositionError(`${estado.esquema.id}.${contenedor}: id duplicado '${id}'`);
116
+ }
117
+ ids.add(id);
118
+ validarHijo(hijo, contenedor, id, profundidad, estado);
119
+ }
120
+ }
121
+
122
+ /** Valida un hijo: topes de nodos/profundidad, ciclo, ref y tipo inline. */
123
+ function validarHijo(
124
+ hijo: ViewChildNode,
125
+ contenedor: string,
126
+ id: string,
127
+ profundidad: number,
128
+ estado: Estado,
129
+ ): void {
130
+ const donde = `${estado.esquema.id}.${contenedor} → child '${id}'`;
131
+
132
+ if (!estado.contados.has(hijo)) {
133
+ estado.contados.add(hijo);
134
+ estado.nodos += 1;
135
+ if (estado.nodos > MAX_NODES) {
136
+ throw new SchemaCompositionError(
137
+ `${estado.esquema.id}: la composición supera el límite de ${MAX_NODES} nodos (observados ${estado.nodos})`,
138
+ );
139
+ }
140
+ }
141
+ if (profundidad > MAX_DEPTH) {
142
+ throw new SchemaCompositionError(
143
+ `${donde}: profundidad ${profundidad} supera el máximo ${MAX_DEPTH}`,
144
+ );
145
+ }
146
+
147
+ if ('ref' in hijo) {
148
+ validarRef(donde, (hijo as { ref?: unknown }).ref as string, profundidad, estado);
149
+ return;
150
+ }
151
+
152
+ const tipo = (hijo as { type?: unknown }).type as string;
153
+ if (!get('view', tipo)) {
154
+ throw new SchemaCompositionError(`${donde}: tipo desconocido '${tipo}'`);
155
+ }
156
+ validarSelect(donde, (hijo as { select?: SelectionBinding }).select);
157
+ validarSource(donde, (configDe(hijo) as { source?: DataSource } | undefined)?.source);
158
+ /**
159
+ * Contenedor inline: sus propios hijos también se validan. Sin esto, una
160
+ * composición anidada dentro de otro `tabs` inline se renderiza sin haber
161
+ * pasado por topes de profundidad/nodos ni por la detección de ciclos de
162
+ * sus `ref` (D2: invalid branch NUNCA llega al render). La profundidad
163
+ * CONTINUA desde este hijo — no se reinicia — y el camino de ciclo se
164
+ * mantiene intacto, porque un `ref` sólo puede volver a nombres que ya
165
+ * están en él.
166
+ */
167
+ const internos = hijosDe(hijo);
168
+ if (internos !== undefined) {
169
+ validarHijos(id, internos, profundidad + 1, estado);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Resuelve un hijo `ref` contra `schema.uiViews`: el destino debe existir y
175
+ * no formar parte del camino actual (ciclo directo o transitivo). El
176
+ * contenido del destino se valida en profundidad CONTINUANDO la del ref —
177
+ * un `ref` anida de verdad (R9.9.2).
178
+ */
179
+ function validarRef(donde: string, target: string, profundidad: number, estado: Estado): void {
180
+ if (estado.camino.has(target)) {
181
+ throw new SchemaCompositionError(
182
+ `${donde}: ciclo detectado — '${target}' ya está en el camino`,
183
+ );
184
+ }
185
+ const vista = estado.esquema.uiViews?.[target];
186
+ if (!vista) {
187
+ throw new SchemaCompositionError(
188
+ `${donde}: ref '${target}' no existe en ${estado.esquema.id}.uiViews`,
189
+ );
190
+ }
191
+ estado.camino.add(target);
192
+ validarContenido(target, vista, profundidad + 1, estado);
193
+ estado.camino.delete(target);
194
+ }
195
+
196
+ /**
197
+ * Valida el binding de selección de una `uiView` o de un hijo (R9.9.4).
198
+ * Sólo `mode: 'route'` exige destino declarado; `none`/`inline` son locales.
199
+ */
200
+ function validarSelect(donde: string, select: SelectionBinding | undefined): void {
201
+ if (select === undefined || select.mode !== 'route') return;
202
+ const target = (select as { target?: unknown }).target;
203
+ if (target === undefined || target === null) {
204
+ throw new SchemaCompositionError(`${donde}: select mode 'route' sin target`);
205
+ }
206
+ if (typeof target === 'string') {
207
+ throw new SchemaCompositionError(
208
+ `${donde}: destino con URL cruda '${target}' — declará resource + view`,
209
+ );
210
+ }
211
+ validarTarget(donde, target as NavigationTarget);
212
+ }
213
+
214
+ /**
215
+ * El destino sólo puede ser un recurso + `uiView` declarados, con los params
216
+ * declarados EXACTAMENTE igual que los params de la ruta del destino: ni
217
+ * params ajenos a la ruta ni params de la ruta sin declarar.
218
+ */
219
+ function validarTarget(donde: string, target: NavigationTarget): void {
220
+ const { resource, view, params } = target;
221
+ if (typeof resource !== 'string' || resource === '' || typeof view !== 'string' || view === '') {
222
+ throw new SchemaCompositionError(`${donde}: destino sin resource + view`);
223
+ }
224
+ const destino = getSchema(resource);
225
+ if (!destino) {
226
+ throw new SchemaCompositionError(`${donde}: destino '${resource}' no existe`);
227
+ }
228
+ const vista = destino.uiViews?.[view];
229
+ if (!vista) {
230
+ throw new SchemaCompositionError(
231
+ `${donde}: destino ${resource}.${view} no tiene uiView declarada`,
232
+ );
233
+ }
234
+ const paramsRuta = (vista.path ?? '')
235
+ .split('/')
236
+ .filter((segmento) => segmento.startsWith(':'))
237
+ .map((segmento) => segmento.slice(1));
238
+ const declarados = params ?? [];
239
+ for (const param of declarados) {
240
+ if (!paramsRuta.includes(param)) {
241
+ throw new SchemaCompositionError(
242
+ `${donde}: param '${param}' no existe en la ruta ${resource}.${view}`,
243
+ );
244
+ }
245
+ }
246
+ for (const param of paramsRuta) {
247
+ if (!declarados.includes(param)) {
248
+ throw new SchemaCompositionError(
249
+ `${donde}: falta declarar el param '${param}' de la ruta ${resource}.${view}`,
250
+ );
251
+ }
252
+ }
253
+ }
254
+
255
+ /** Valida la fuente de filas (`config.source`) declarada por una vista (R9.8). */
256
+ function validarSource(donde: string, source: DataSource | undefined): void {
257
+ if (source === undefined) return;
258
+ if (typeof source.resource !== 'string' || source.resource === '') {
259
+ throw new SchemaCompositionError(`${donde}: source sin resource`);
260
+ }
261
+ const origen = getSchema(source.resource);
262
+ if (!origen) {
263
+ throw new SchemaCompositionError(`${donde}: source '${source.resource}' no existe`);
264
+ }
265
+ if (source.view !== undefined && origen.views?.[source.view] === undefined) {
266
+ throw new SchemaCompositionError(
267
+ `${donde}: la vista '${source.view}' no existe en ${source.resource}.views`,
268
+ );
269
+ }
270
+ }