@tinya-code/gas-admin-ui 0.2.0-alpha.13 → 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 (368) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +3 -15
  3. package/src/lib/actions/action-registry.spec.ts +52 -0
  4. package/src/lib/actions/action-registry.ts +126 -0
  5. package/src/lib/actions/action-service.spec.ts +215 -0
  6. package/src/lib/actions/action-service.ts +87 -0
  7. package/src/lib/actions/descriptors.spec.ts +155 -0
  8. package/src/lib/actions/descriptors.ts +154 -0
  9. package/src/lib/columns/column-cell.ts +33 -0
  10. package/src/lib/columns/column-defaults.spec.ts +99 -0
  11. package/src/lib/columns/column-defaults.ts +84 -0
  12. package/src/lib/columns/column-format.spec.ts +274 -0
  13. package/src/lib/columns/descriptors.ts +113 -0
  14. package/src/lib/columns/format-badge.ts +51 -0
  15. package/src/lib/columns/format-boolean.ts +58 -0
  16. package/src/lib/columns/format-currency.ts +30 -0
  17. package/src/lib/columns/format-date.ts +35 -0
  18. package/src/lib/columns/format-number.ts +32 -0
  19. package/src/lib/columns/format-text.ts +23 -0
  20. package/src/lib/columns/format-thumbnail.ts +63 -0
  21. package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
  22. package/src/lib/core/guards/auth.guard.spec.ts +88 -0
  23. package/src/lib/core/guards/auth.guard.ts +48 -0
  24. package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
  25. package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
  26. package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
  27. package/src/lib/core/models/api.model.ts +104 -0
  28. package/src/lib/core/models/schema-descriptions.ts +118 -0
  29. package/src/lib/core/models/schema.model.ts +1097 -0
  30. package/src/lib/core/registry/composition-validator.spec.ts +358 -0
  31. package/src/lib/core/registry/composition-validator.ts +270 -0
  32. package/src/lib/core/registry/field-validators.spec.ts +199 -0
  33. package/src/lib/core/registry/field-validators.ts +440 -0
  34. package/src/lib/core/registry/render-host.spec.ts +208 -0
  35. package/src/lib/core/registry/render-host.ts +646 -0
  36. package/src/lib/core/registry/render-registry.spec.ts +96 -0
  37. package/src/lib/core/registry/render-registry.ts +312 -0
  38. package/src/lib/core/registry/schema-catalog.ts +156 -0
  39. package/src/lib/core/services/api.service.spec.ts +1142 -0
  40. package/src/lib/core/services/api.service.ts +371 -0
  41. package/src/lib/core/services/auth.service.spec.ts +653 -0
  42. package/src/lib/core/services/auth.service.ts +503 -0
  43. package/src/lib/core/services/config.service.spec.ts +121 -0
  44. package/src/lib/core/services/config.service.ts +119 -0
  45. package/src/lib/core/services/data-source.service.spec.ts +179 -0
  46. package/src/lib/core/services/data-source.service.ts +142 -0
  47. package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
  48. package/src/lib/core/services/error-mapper.service.ts +49 -0
  49. package/src/lib/core/services/notification.service.ts +73 -0
  50. package/src/lib/core/services/relation-drawer.service.ts +41 -0
  51. package/src/lib/core/services/theme.service.spec.ts +106 -0
  52. package/src/lib/core/services/theme.service.ts +86 -0
  53. package/src/lib/core/services/transport.spec.ts +198 -0
  54. package/src/lib/core/services/transport.ts +164 -0
  55. package/src/lib/core/services/upload.service.ts +81 -0
  56. package/src/lib/core/testing/categories.schema.ts +125 -0
  57. package/src/lib/core/testing/orders.schema.ts +372 -0
  58. package/src/lib/core/testing/products.schema.ts +258 -0
  59. package/src/lib/core/testing/site.schema.ts +195 -0
  60. package/src/lib/core/testing/test-catalog.ts +21 -0
  61. package/src/lib/core/tokens.ts +112 -0
  62. package/src/lib/core/utils/clipboard.spec.ts +55 -0
  63. package/src/lib/core/utils/clipboard.ts +90 -0
  64. package/src/lib/core/utils/context-resolver.spec.ts +95 -0
  65. package/src/lib/core/utils/context-resolver.ts +101 -0
  66. package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
  67. package/src/lib/core/utils/response-unwrapper.ts +103 -0
  68. package/src/lib/core/utils/source-options.spec.ts +96 -0
  69. package/src/lib/core/utils/source-options.ts +54 -0
  70. package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
  71. package/src/lib/core/utils/template-interpolator.ts +56 -0
  72. package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
  73. package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
  74. package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
  75. package/src/lib/fields/boolean/boolean.ts +40 -0
  76. package/src/lib/fields/color/color.descriptor.ts +12 -0
  77. package/src/lib/fields/color/color.ts +34 -0
  78. package/src/lib/fields/currency/currency.descriptor.ts +12 -0
  79. package/src/lib/fields/currency/currency.ts +67 -0
  80. package/src/lib/fields/date/date.descriptor.ts +12 -0
  81. package/src/lib/fields/date/date.ts +36 -0
  82. package/src/lib/fields/descriptors.ts +122 -0
  83. package/src/lib/fields/display/display-badge.ts +65 -0
  84. package/src/lib/fields/display/display-date.ts +39 -0
  85. package/src/lib/fields/display/display-defaults.ts +84 -0
  86. package/src/lib/fields/display/display-file.ts +41 -0
  87. package/src/lib/fields/display/display-image.ts +54 -0
  88. package/src/lib/fields/display/display-list.ts +107 -0
  89. package/src/lib/fields/display/display-number.ts +41 -0
  90. package/src/lib/fields/display/display-text.ts +42 -0
  91. package/src/lib/fields/display/display.spec.ts +242 -0
  92. package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
  93. package/src/lib/fields/dropdown/dropdown.ts +45 -0
  94. package/src/lib/fields/email/email.descriptor.ts +12 -0
  95. package/src/lib/fields/email/email.ts +28 -0
  96. package/src/lib/fields/field-aria.ts +60 -0
  97. package/src/lib/fields/field-host/field-host.spec.ts +118 -0
  98. package/src/lib/fields/field-host/field-host.ts +47 -0
  99. package/src/lib/fields/field-node.ts +88 -0
  100. package/src/lib/fields/file/file.descriptor.ts +14 -0
  101. package/src/lib/fields/file/file.spec.ts +134 -0
  102. package/src/lib/fields/file/file.ts +281 -0
  103. package/src/lib/fields/group/group.descriptor.ts +21 -0
  104. package/src/lib/fields/group/group.ts +110 -0
  105. package/src/lib/fields/image/image.descriptor.ts +12 -0
  106. package/src/lib/fields/image/image.spec.ts +117 -0
  107. package/src/lib/fields/image/image.ts +281 -0
  108. package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
  109. package/src/lib/fields/key-value/key-value.ts +283 -0
  110. package/src/lib/fields/list/list.descriptor.ts +21 -0
  111. package/src/lib/fields/list/list.ts +376 -0
  112. package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
  113. package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
  114. package/src/lib/fields/multiselect/multiselect.ts +152 -0
  115. package/src/lib/fields/number/number.descriptor.ts +12 -0
  116. package/src/lib/fields/number/number.ts +40 -0
  117. package/src/lib/fields/phone/phone.descriptor.ts +12 -0
  118. package/src/lib/fields/phone/phone.ts +46 -0
  119. package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
  120. package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
  121. package/src/lib/fields/relation/relation.descriptor.ts +12 -0
  122. package/src/lib/fields/relation/relation.spec.ts +193 -0
  123. package/src/lib/fields/relation/relation.ts +224 -0
  124. package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
  125. package/src/lib/fields/rich-select/rich-select.ts +175 -0
  126. package/src/lib/fields/select/radio-mode.ts +25 -0
  127. package/src/lib/fields/select/select.descriptor.ts +12 -0
  128. package/src/lib/fields/select/select.spec.ts +162 -0
  129. package/src/lib/fields/select/select.ts +210 -0
  130. package/src/lib/fields/slug/slug.descriptor.ts +19 -0
  131. package/src/lib/fields/slug/slug.ts +67 -0
  132. package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
  133. package/src/lib/fields/string-list/string-list.ts +222 -0
  134. package/src/lib/fields/text/text.descriptor.ts +12 -0
  135. package/src/lib/fields/text/text.ts +35 -0
  136. package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
  137. package/src/lib/fields/textarea/textarea.spec.ts +117 -0
  138. package/src/lib/fields/textarea/textarea.ts +126 -0
  139. package/src/lib/fields/time/time.descriptor.ts +12 -0
  140. package/src/lib/fields/time/time.ts +28 -0
  141. package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
  142. package/src/lib/fields/url/url.descriptor.ts +12 -0
  143. package/src/lib/fields/url/url.ts +53 -0
  144. package/src/lib/gas-admin-ui.spec.ts +21 -0
  145. package/src/lib/gas-admin-ui.ts +9 -0
  146. package/src/lib/layouts/descriptors.ts +9 -0
  147. package/src/lib/pages/dashboard/dashboard.ts +167 -0
  148. package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
  149. package/src/lib/pages/detail-view/detail-view.ts +570 -0
  150. package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
  151. package/src/lib/pages/form-view/form-model.ts +88 -0
  152. package/src/lib/pages/form-view/form-schema.ts +255 -0
  153. package/src/lib/pages/form-view/form-view.ts +1622 -0
  154. package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
  155. package/src/lib/pages/list-view/list-view.ts +882 -0
  156. package/src/lib/pages/login/login.spec.ts +109 -0
  157. package/src/lib/pages/login/login.ts +227 -0
  158. package/src/lib/pages/resource-page/resource-page.ts +234 -0
  159. package/src/lib/shared/components/badge/badge.ts +35 -0
  160. package/src/lib/shared/components/button/button.ts +65 -0
  161. package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
  162. package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
  163. package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
  164. package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
  165. package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
  166. package/src/lib/shared/components/drawer/drawer.ts +172 -0
  167. package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
  168. package/src/lib/shared/components/modal/modal.spec.ts +149 -0
  169. package/src/lib/shared/components/modal/modal.ts +145 -0
  170. package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
  171. package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
  172. package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
  173. package/src/lib/shared/components/toast/toast.ts +58 -0
  174. package/src/lib/shared/utils/col-span.ts +25 -0
  175. package/src/lib/shared/utils/condition-evaluator.ts +53 -0
  176. package/src/lib/shared/utils/depends-on.ts +48 -0
  177. package/src/lib/shared/utils/dialog-focus.ts +28 -0
  178. package/src/lib/shared/utils/field-path.ts +49 -0
  179. package/src/lib/shared/utils/gap-sorting.ts +144 -0
  180. package/src/lib/shared/utils/seed.spec.ts +64 -0
  181. package/src/lib/shared/utils/seed.ts +149 -0
  182. package/src/lib/shared/utils/slugify.ts +13 -0
  183. package/src/lib/shared/utils/validators.ts +41 -0
  184. package/src/lib/shell/layout/layout.ts +63 -0
  185. package/src/lib/shell/shell.routes.spec.ts +59 -0
  186. package/src/lib/shell/shell.routes.ts +122 -0
  187. package/src/lib/shell/sidebar/sidebar.ts +139 -0
  188. package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
  189. package/src/lib/views/calendar-view/calendar-model.ts +426 -0
  190. package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
  191. package/src/lib/views/calendar-view/calendar-view.ts +568 -0
  192. package/src/lib/views/composer/composer-host.spec.ts +301 -0
  193. package/src/lib/views/composer/composer-host.ts +248 -0
  194. package/src/lib/views/composer/composer-imports.spec.ts +54 -0
  195. package/src/lib/views/composer/view-context.spec.ts +117 -0
  196. package/src/lib/views/composer/view-context.ts +109 -0
  197. package/src/lib/views/composer/view-slot.spec.ts +130 -0
  198. package/src/lib/views/composer/view-slot.ts +178 -0
  199. package/src/lib/views/descriptors.ts +46 -0
  200. package/src/lib/views/kanban-view/kanban-model.ts +173 -0
  201. package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
  202. package/src/lib/views/kanban-view/kanban-view.ts +327 -0
  203. package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
  204. package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
  205. package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
  206. package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
  207. package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
  208. package/src/lib/widgets/chart-line/chart-line.ts +219 -0
  209. package/src/lib/widgets/descriptors.ts +29 -0
  210. package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
  211. package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
  212. package/src/lib/widgets/metric-card/metric-card.ts +48 -0
  213. package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
  214. package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
  215. package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
  216. package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
  217. package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
  218. package/src/lib/widgets/record-list/record-list.ts +115 -0
  219. package/src/lib/widgets/series.spec.ts +42 -0
  220. package/src/lib/widgets/series.ts +56 -0
  221. package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
  222. package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
  223. package/src/lib/widgets/status-progress/status-progress.ts +104 -0
  224. package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
  225. package/src/lib/widgets/widget-host/widget-host.ts +99 -0
  226. package/src/public-api.ts +104 -0
  227. package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
  228. package/tsconfig.lib.json +13 -0
  229. package/tsconfig.lib.prod.json +11 -0
  230. package/tsconfig.spec.json +10 -0
  231. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
  232. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
  233. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
  234. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
  235. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
  236. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
  237. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
  238. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
  239. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
  240. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
  241. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
  242. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
  243. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
  244. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
  245. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
  246. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
  247. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
  248. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
  249. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
  250. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
  251. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
  252. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
  253. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
  254. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
  255. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
  256. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
  257. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
  258. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
  259. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
  260. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
  261. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
  262. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
  263. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
  264. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
  265. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
  266. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
  267. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
  268. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
  269. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
  270. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
  271. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
  272. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
  273. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
  274. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
  275. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
  276. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
  277. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
  278. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
  279. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
  280. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
  281. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
  282. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
  283. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
  284. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
  285. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
  286. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
  287. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
  288. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
  289. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
  290. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
  291. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
  292. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
  293. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
  294. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
  295. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
  296. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
  297. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
  298. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
  299. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
  300. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
  301. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
  302. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
  303. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
  304. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
  305. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
  306. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
  307. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
  308. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
  309. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
  310. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
  311. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
  312. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
  313. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
  314. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
  315. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
  316. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
  317. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
  318. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
  319. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
  320. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
  321. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
  322. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
  323. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
  324. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
  325. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
  326. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
  327. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
  328. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
  329. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
  330. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
  331. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
  332. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
  333. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
  334. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
  335. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
  336. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
  337. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
  338. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
  339. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
  340. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
  341. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
  342. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
  343. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
  344. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
  345. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
  346. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
  347. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
  348. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
  349. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
  350. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
  351. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
  352. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
  353. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
  354. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
  355. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
  356. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
  357. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
  358. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
  359. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
  360. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
  361. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
  362. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
  363. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
  364. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
  365. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
  366. package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
  367. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
  368. 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
+ }