@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,301 @@
1
+ import { Component } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { ActivatedRoute } from '@angular/router';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { of } from 'rxjs';
6
+
7
+ import type { ResourceSchema, UiViewDescriptor, UiViewTabs } from '../../core/models/schema.model';
8
+ import { register, unregister } from '../../core/registry/render-registry';
9
+ import { ComposerHost } from './composer-host';
10
+
11
+ /** Hoja ligera para los tests (se registra y se desregistra por test). */
12
+ @Component({
13
+ standalone: true,
14
+ selector: 'app-hoja-h',
15
+ template: `<p data-hoja>{{ etiqueta }}</p>`,
16
+ })
17
+ class HojaH {
18
+ readonly etiqueta: string;
19
+ constructor(route: ActivatedRoute) {
20
+ let v: Record<string, unknown> = {};
21
+ route.data.subscribe((d) => (v = d as Record<string, unknown>));
22
+ this.etiqueta = String(v['uiView'] ?? '');
23
+ }
24
+ }
25
+
26
+ function tabs(children: UiViewTabs['config']['children'], initial?: string): UiViewTabs {
27
+ return { type: 'tabs', config: { children, ...(initial ? { initial } : {}) } };
28
+ }
29
+
30
+ function base(): ResourceSchema {
31
+ return {
32
+ id: 'demo',
33
+ label: 'Demo',
34
+ labelPlural: 'Demos',
35
+ kind: 'collection',
36
+ endpoint: {},
37
+ keyField: 'id',
38
+ titleField: 'name',
39
+ listColumns: [],
40
+ fields: [],
41
+ uiViews: {},
42
+ };
43
+ }
44
+
45
+ function esquemaCon(vista: UiViewDescriptor): ResourceSchema {
46
+ return { ...base(), uiViews: { panel: vista } };
47
+ }
48
+
49
+ type Fixture = ReturnType<TestBed['createComponent']>;
50
+
51
+ /**
52
+ * Dos ciclos estables: el primero resuelve el `load()` del slot activo, el
53
+ * segundo monta su componente; el tercero cubre un nivel de anidación.
54
+ */
55
+ async function flush(fixture: Fixture, cond?: () => boolean): Promise<Fixture> {
56
+ const ciclosMax = cond === undefined ? 4 : 30;
57
+ for (let ciclo = 0; ciclo < ciclosMax; ciclo += 1) {
58
+ if (cond?.()) break;
59
+ // Ceder la macrotarea: el `import()` dinámico de un descriptor no es
60
+ // microtask puro y `whenStable` en TestBed no lo espera. Con `cond`
61
+ // además se espera hasta que el DOM realmente avance (la suite completa
62
+ // satura el worker y el import tarda más ciclos).
63
+ await new Promise((resolve) => setTimeout(resolve, 0));
64
+ await fixture.whenStable();
65
+ fixture.detectChanges();
66
+ }
67
+ return fixture;
68
+ }
69
+
70
+ async function montar(schema: ResourceSchema, uiView = 'panel'): Promise<Fixture> {
71
+ const datos = { schema, resourceId: schema.id, uiView, viewType: 'tabs' };
72
+ TestBed.configureTestingModule({
73
+ providers: [{ provide: ActivatedRoute, useValue: { data: of(datos) } }],
74
+ });
75
+ const fixture = TestBed.createComponent(ComposerHost);
76
+ fixture.detectChanges();
77
+ await flush(fixture);
78
+ return fixture;
79
+ }
80
+
81
+ describe('ComposerHost', () => {
82
+ afterEach(() => unregister('view', 'hoja-h'));
83
+
84
+ it('renderiza los hijos en orden declarado con tablist accesible', async () => {
85
+ register('view', { kind: 'view', type: 'hoja-h', since: '9.9', load: async () => HojaH });
86
+ const fixture = await montar(
87
+ esquemaCon(
88
+ tabs([
89
+ { id: 'a', label: 'Uno', type: 'hoja-h' },
90
+ { id: 'b', label: 'Dos', type: 'hoja-h' },
91
+ { id: 'c', label: 'Tres', type: 'hoja-h' },
92
+ ]),
93
+ ),
94
+ );
95
+ const tabsDom = [...fixture.nativeElement.querySelectorAll('[role="tab"]')];
96
+ expect(tabsDom.map((t) => t.textContent?.trim())).toEqual(['Uno', 'Dos', 'Tres']);
97
+ expect(fixture.nativeElement.querySelector('[role="tablist"]')).toBeTruthy();
98
+ // el primer hijo está activo por defecto: sólo su panel está montado
99
+ expect(fixture.nativeElement.querySelectorAll('[role="tabpanel"]').length).toBe(1);
100
+ expect(tabsDom[0].getAttribute('aria-selected')).toBe('true');
101
+ expect(tabsDom[0].getAttribute('tabindex')).toBe('0');
102
+ expect(tabsDom[1].getAttribute('tabindex')).toBe('-1');
103
+ });
104
+
105
+ it('respeta `initial` para la pestaña activa al montar', async () => {
106
+ register('view', { kind: 'view', type: 'hoja-h', since: '9.9', load: async () => HojaH });
107
+ const fixture = await montar(
108
+ esquemaCon(
109
+ tabs(
110
+ [
111
+ { id: 'a', label: 'Uno', type: 'hoja-h' },
112
+ { id: 'b', label: 'Dos', type: 'hoja-h' },
113
+ ],
114
+ 'b',
115
+ ),
116
+ ),
117
+ );
118
+ const selected = [...fixture.nativeElement.querySelectorAll('[role="tab"]')].filter(
119
+ (t) => t.getAttribute('aria-selected') === 'true',
120
+ );
121
+ expect(selected.map((t) => t.textContent?.trim())).toEqual(['Dos']);
122
+ expect(fixture.nativeElement.querySelector('[role="tabpanel"]')?.textContent).toContain(
123
+ 'panel/b',
124
+ );
125
+ });
126
+
127
+ it('clic en pestaña activa el panel y destruye el no retenido', async () => {
128
+ register('view', { kind: 'view', type: 'hoja-h', since: '9.9', load: async () => HojaH });
129
+ const fixture = await montar(
130
+ esquemaCon(
131
+ tabs([
132
+ { id: 'a', label: 'Uno', type: 'hoja-h' },
133
+ { id: 'b', label: 'Dos', type: 'hoja-h' },
134
+ ]),
135
+ ),
136
+ );
137
+ const panels = () => fixture.nativeElement.querySelectorAll('[role="tabpanel"]');
138
+ expect(panels().length).toBe(1);
139
+ expect(panels()[0].textContent).toContain('panel/a');
140
+ [...fixture.nativeElement.querySelectorAll('[role="tab"]')][1].click();
141
+ await flush(fixture);
142
+ expect(panels().length).toBe(1);
143
+ expect(panels()[0].textContent).toContain('panel/b');
144
+ // no retenido: el panel de 'a' ya no existe en el DOM
145
+ expect(panels()[0].textContent).not.toContain('panel/a');
146
+ });
147
+
148
+ it('retain:true conserva el panel montado pero oculto tras cambiar de pestaña', async () => {
149
+ register('view', { kind: 'view', type: 'hoja-h', since: '9.9', load: async () => HojaH });
150
+ const fixture = await montar(
151
+ esquemaCon(
152
+ tabs([
153
+ { id: 'a', label: 'Uno', type: 'hoja-h', retain: true },
154
+ { id: 'b', label: 'Dos', type: 'hoja-h' },
155
+ ]),
156
+ ),
157
+ );
158
+ [...fixture.nativeElement.querySelectorAll('[role="tab"]')][1].click();
159
+ await flush(fixture);
160
+ const panels = [...fixture.nativeElement.querySelectorAll('[role="tabpanel"]')];
161
+ expect(panels.length).toBe(2);
162
+ const oculto = panels.find((p) => p.textContent?.includes('panel/a'));
163
+ expect(oculto).toBeTruthy();
164
+ expect(oculto!.classList.contains('hidden')).toBe(true);
165
+ const visible = panels.find((p) => p.textContent?.includes('panel/b'));
166
+ expect(visible!.classList.contains('hidden')).toBe(false);
167
+ });
168
+
169
+ it('flechas/Home/End mueven la selección y el tabindex (roving)', async () => {
170
+ register('view', { kind: 'view', type: 'hoja-h', since: '9.9', load: async () => HojaH });
171
+ const fixture = await montar(
172
+ esquemaCon(
173
+ tabs([
174
+ { id: 'a', label: 'Uno', type: 'hoja-h' },
175
+ { id: 'b', label: 'Dos', type: 'hoja-h' },
176
+ { id: 'c', label: 'Tres', type: 'hoja-h' },
177
+ ]),
178
+ ),
179
+ );
180
+ const tablist = fixture.nativeElement.querySelector('[role="tablist"]');
181
+ const tabsDom = [...tablist.querySelectorAll('[role="tab"]')];
182
+ const tecla = (key: string) => {
183
+ tablist.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
184
+ fixture.detectChanges();
185
+ };
186
+ tecla('ArrowRight');
187
+ expect(tabsDom[1].getAttribute('aria-selected')).toBe('true');
188
+ expect(tabsDom[1].getAttribute('tabindex')).toBe('0');
189
+ expect(tabsDom[0].getAttribute('tabindex')).toBe('-1');
190
+ tecla('End');
191
+ expect(tabsDom[2].getAttribute('aria-selected')).toBe('true');
192
+ tecla('Home');
193
+ expect(tabsDom[0].getAttribute('aria-selected')).toBe('true');
194
+ tecla('ArrowLeft'); // desde el primero ⇒ envuelve al último
195
+ expect(tabsDom[2].getAttribute('aria-selected')).toBe('true');
196
+ });
197
+
198
+ it('config inválida (no-tabs) ⇒ estado de error, sin montar hijos', async () => {
199
+ const fixture = await montar(esquemaCon({ type: 'list' }));
200
+ expect(fixture.nativeElement.textContent).toContain('inválida');
201
+ expect(fixture.nativeElement.querySelector('[role="tablist"]')).toBeFalsy();
202
+ });
203
+
204
+ it('ciclo de composición ⇒ error validado antes del render (D2)', async () => {
205
+ const conRef: UiViewTabs = {
206
+ type: 'tabs',
207
+ config: { children: [{ id: 'loop', label: 'Loop', ref: 'panel' }] },
208
+ };
209
+ const fixture = await montar(esquemaCon(conRef));
210
+ expect(fixture.nativeElement.textContent).toContain('ciclo');
211
+ expect(fixture.nativeElement.querySelector('[role="tablist"]')).toBeFalsy();
212
+ });
213
+
214
+ it('profundidad sobre el límite ⇒ error validado antes del render (D2)', async () => {
215
+ register('view', { kind: 'view', type: 'hoja-h', since: '9.9', load: async () => HojaH });
216
+ const schema: ResourceSchema = {
217
+ ...base(),
218
+ uiViews: {
219
+ a: tabs([{ id: 'n1', label: 'N1', ref: 'b' }]),
220
+ b: tabs([{ id: 'n2', label: 'N2', ref: 'c' }]),
221
+ c: tabs([{ id: 'n3', label: 'N3', ref: 'd' }]),
222
+ d: tabs([{ id: 'n4', label: 'N4', type: 'hoja-h' }]),
223
+ },
224
+ };
225
+ const fixture = await montar(schema, 'a');
226
+ expect(fixture.nativeElement.textContent).toContain('profundidad');
227
+ expect(fixture.nativeElement.querySelector('[role="tablist"]')).toBeFalsy();
228
+ });
229
+
230
+ it('visibleWhen sin record evalúa contra vacío: empty pasa, eq no', async () => {
231
+ register('view', { kind: 'view', type: 'hoja-h', since: '9.9', load: async () => HojaH });
232
+ const fixture = await montar(
233
+ esquemaCon(
234
+ tabs([
235
+ { id: 'a', label: 'Siempre', type: 'hoja-h' },
236
+ {
237
+ id: 'b',
238
+ label: 'Condicional',
239
+ type: 'hoja-h',
240
+ visibleWhen: [{ field: 'estado', operator: 'eq', value: 'activo' }],
241
+ },
242
+ {
243
+ id: 'c',
244
+ label: 'Sin filtro',
245
+ type: 'hoja-h',
246
+ visibleWhen: [{ field: 'estado', operator: 'empty' }],
247
+ },
248
+ ]),
249
+ ),
250
+ );
251
+ const labels = [...fixture.nativeElement.querySelectorAll('[role="tab"]')].map((t) =>
252
+ t.textContent?.trim(),
253
+ );
254
+ expect(labels).toEqual(['Siempre', 'Sin filtro']);
255
+ });
256
+
257
+ it('si la pestaña activa queda oculta, cae visiblemente a la primera visible', async () => {
258
+ register('view', { kind: 'view', type: 'hoja-h', since: '9.9', load: async () => HojaH });
259
+ const fixture = await montar(
260
+ esquemaCon(
261
+ tabs([
262
+ {
263
+ id: 'oculta',
264
+ label: 'Oculta',
265
+ type: 'hoja-h',
266
+ visibleWhen: [{ field: 'estado', operator: 'eq', value: 'activo' }],
267
+ },
268
+ { id: 'visible', label: 'Visible', type: 'hoja-h' },
269
+ ]),
270
+ ),
271
+ );
272
+ const tabsDom = [...fixture.nativeElement.querySelectorAll('[role="tab"]')];
273
+ expect(tabsDom.map((t) => t.textContent?.trim())).toEqual(['Visible']);
274
+ expect(tabsDom[0].getAttribute('aria-selected')).toBe('true');
275
+ expect(fixture.nativeElement.querySelector('[role="tabpanel"]')?.textContent).toContain(
276
+ 'panel/visible',
277
+ );
278
+ });
279
+
280
+ it('anida tabs inline: el hijo contenedor monta sus propios hijos', async () => {
281
+ register('view', { kind: 'view', type: 'hoja-h', since: '9.9', load: async () => HojaH });
282
+ const anidado: UiViewTabs = tabs([
283
+ {
284
+ id: 'hijo',
285
+ label: 'Hijo',
286
+ type: 'tabs',
287
+ config: { children: [{ id: 'nieto', label: 'Nieto', type: 'hoja-h' }] },
288
+ },
289
+ ]);
290
+ const fixture = await montar(esquemaCon(anidado));
291
+ await flush(fixture, () => fixture.nativeElement.querySelectorAll('[role="tab"]').length >= 2);
292
+ const labels = [...fixture.nativeElement.querySelectorAll('[role="tab"]')].map((t) =>
293
+ t.textContent?.trim(),
294
+ );
295
+ expect(labels).toEqual(['Hijo', 'Nieto']);
296
+ // el nieto vive bajo instanceId de cadena: panel/hijo (parentInstanceId)
297
+ expect(fixture.nativeElement.querySelector('[role="tabpanel"]')?.textContent).toContain(
298
+ 'panel/hijo',
299
+ );
300
+ });
301
+ });
@@ -0,0 +1,248 @@
1
+ import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
2
+ import { ActivatedRoute } from '@angular/router';
3
+ import { toSignal } from '@angular/core/rxjs-interop';
4
+
5
+ import type { ResourceSchema, UiViewTabs, ViewChildNode } from '../../core/models/schema.model';
6
+ import { validateComposition } from '../../core/registry/composition-validator';
7
+ import { getSchema } from '../../core/registry/schema-catalog';
8
+ import { EmptyState } from '../../shared/components/empty-state/empty-state';
9
+ import { evaluateCondition } from '../../shared/utils/condition-evaluator';
10
+ import { ViewSlot } from './view-slot';
11
+
12
+ /**
13
+ * R9.9 · slice 3 — Host genérico de composición (`uiView` tipo `tabs`).
14
+ *
15
+ * Un SOLO componente recursivo renderiza cualquier composición declarada:
16
+ * resuelve cada hijo por `(kind:'view', type)` dentro del `ViewSlot` (D2,
17
+ * sin `switch` de dominio y sin importar hojas), monta la validación del
18
+ * schema ANTES del primer render (fail-loud) y maneja el tablist accesible
19
+ * con roving tabindex (R9.9.5).
20
+ *
21
+ * Decisiones R9.9.5:
22
+ * - Retención: un hijo `retain:true` visitado queda MONTADO pero oculto
23
+ * (`hidden`); sin `retain` se destruye al cambiar de pestaña.
24
+ * - Pestaña activa LOCAL por componente: no hay estado compartido entre
25
+ * instancias de tabs anidadas (documentado en los docs de slice 5).
26
+ * - `visibleWhen` se evalúa sin record (el contenedor no es un formulario):
27
+ * contra registro vacío, TODAS las condiciones deben pasar (AND).
28
+ * Si la pestaña activa queda oculta, la selección cae visiblemente a la
29
+ * primera visible — nunca se pierde en silencio.
30
+ */
31
+ @Component({
32
+ selector: 'app-composer-host',
33
+ standalone: true,
34
+ imports: [ViewSlot, EmptyState],
35
+ changeDetection: ChangeDetectionStrategy.OnPush,
36
+ template: `
37
+ @if (error() !== null) {
38
+ <app-empty-state title="Composición no disponible" [message]="error()!" />
39
+ } @else if (visibles().length === 0) {
40
+ <app-empty-state
41
+ title="Sin secciones disponibles"
42
+ message="La vista no declara secciones visibles."
43
+ />
44
+ } @else {
45
+ <div
46
+ role="tablist"
47
+ class="flex flex-wrap gap-1 border-b border-neutral/20 px-1"
48
+ [attr.aria-label]="etiqueta()"
49
+ (keydown)="onKeydown($event)"
50
+ >
51
+ @for (h of visibles(); track h.id) {
52
+ <button
53
+ type="button"
54
+ role="tab"
55
+ [id]="'tab-' + h.id"
56
+ [attr.aria-selected]="h.id === activo()"
57
+ [attr.aria-controls]="montado(h.id) ? 'panel-' + h.id : null"
58
+ [attr.tabindex]="h.id === activo() ? 0 : -1"
59
+ [class]="claseTab(h.id)"
60
+ (click)="seleccionar(h.id, true)"
61
+ >
62
+ {{ h.label }}
63
+ </button>
64
+ }
65
+ </div>
66
+
67
+ @for (h of montados(); track h.id) {
68
+ <div
69
+ role="tabpanel"
70
+ [id]="'panel-' + h.id"
71
+ [attr.aria-labelledby]="'tab-' + h.id"
72
+ [class.hidden]="h.id !== activo()"
73
+ class="py-3 focus:outline-none"
74
+ tabindex="0"
75
+ >
76
+ <app-view-slot
77
+ [schema]="schema()!"
78
+ [node]="h"
79
+ [parentInstanceId]="uiViewName()"
80
+ [depth]="profundidad() + 1"
81
+ />
82
+ </div>
83
+ }
84
+ }
85
+ `,
86
+ })
87
+ export class ComposerHost {
88
+ private readonly route = inject(ActivatedRoute);
89
+ private readonly routeData = toSignal(this.route.data, { requireSync: true });
90
+
91
+ /** Schema: explícito en `data` (fixtures/ruta embebida) o vía `resourceId`. */
92
+ readonly schema = computed<ResourceSchema | undefined>(() => {
93
+ const data = this.routeData();
94
+ const explicit = data['schema'];
95
+ if (explicit !== undefined) return explicit as ResourceSchema;
96
+ const resourceId = data['resourceId'];
97
+ return typeof resourceId === 'string' ? getSchema(resourceId) : undefined;
98
+ });
99
+
100
+ /** instanceId de ESTE contenedor (raíz ⇒ nombre de la uiView de ruta). */
101
+ readonly uiViewName = computed(() => {
102
+ const v = this.routeData()['uiView'];
103
+ return typeof v === 'string' ? v : '';
104
+ });
105
+
106
+ /** Profundidad de ESTE contenedor (raíz ⇒ 0; embebido ⇒ payload.depth). */
107
+ readonly profundidad = computed(() => {
108
+ const d = this.routeData()['depth'];
109
+ return typeof d === 'number' ? d : 0;
110
+ });
111
+
112
+ /** Config `tabs` declarada para esta instancia. */
113
+ readonly vista = computed<UiViewTabs | undefined>(() => {
114
+ const schema = this.schema();
115
+ const nombre = this.uiViewName();
116
+ if (schema === undefined || nombre === '') return undefined;
117
+ const vista = schema.uiViews?.[nombre];
118
+ // `'config' in` descarta UiViewBase; el literal descarta calendar/kanban.
119
+ return vista !== undefined && 'config' in vista && vista.type === 'tabs' ? vista : undefined;
120
+ });
121
+
122
+ /** Error determinista de configuración (schema/vista ausente o no-tabs). */
123
+ private readonly errorConfig = computed<string | null>(() => {
124
+ const data = this.routeData();
125
+ const schema = this.schema();
126
+ const nombre = this.uiViewName();
127
+ if (schema === undefined) {
128
+ return `Schema no encontrado para '${String(data['resourceId'] ?? '')}'`;
129
+ }
130
+ if (nombre === '') return 'Ruta de composición sin uiView declarada';
131
+ const vista = schema.uiViews?.[nombre];
132
+ if (vista === undefined) return `Vista '${nombre}' no declarada en ${schema.id}`;
133
+ if (vista.type !== 'tabs') {
134
+ return `Configuración inválida: ${schema.id}.${nombre} declara '${vista.type}', no es una vista tabs`;
135
+ }
136
+ return null;
137
+ });
138
+
139
+ /** Error de validación D2 (se recalcula una sola vez, en el constructor). */
140
+ private readonly errorValidacion = signal<string | null>(null);
141
+
142
+ /** Error mostrado: validación del schema primero, luego configuración. */
143
+ readonly error = computed<string | null>(() => this.errorValidacion() ?? this.errorConfig());
144
+
145
+ /** Todos los hijos declarados (orden de declaración). */
146
+ private readonly hijos = computed<readonly ViewChildNode[]>(
147
+ () => this.vista()?.config.children ?? [],
148
+ );
149
+
150
+ /** Hijos visibles: TODAS sus condiciones `visibleWhen` pasan sin record. */
151
+ readonly visibles = computed<readonly ViewChildNode[]>(() =>
152
+ this.hijos().filter(
153
+ (h) =>
154
+ h.visibleWhen === undefined ||
155
+ h.visibleWhen.every((cond) => evaluateCondition(cond, {}, {})),
156
+ ),
157
+ );
158
+
159
+ /** Selección del usuario (intención; vacío ⇒ usar `initial`/primera). */
160
+ private readonly seleccion = signal('');
161
+
162
+ /**
163
+ * Pestaña activa EFECTIVA: derivada pura de la selección y de las
164
+ * visibles — si la selección dejó de existir (u oculta), cae a `initial`
165
+ * o a la primera visible de forma SINCRÓNICA y visible (R9.9.5: nunca
166
+ * perder la vista elegida en silencio). Estado LOCAL por instancia.
167
+ */
168
+ readonly activo = computed<string>(() => {
169
+ const vis = this.visibles();
170
+ if (vis.length === 0) return '';
171
+ const sel = this.seleccion();
172
+ if (vis.some((h) => h.id === sel)) return sel;
173
+ const inicial = this.vista()?.config.initial;
174
+ return inicial !== undefined && vis.some((h) => h.id === inicial) ? inicial : vis[0].id;
175
+ });
176
+
177
+ /** Ids ya seleccionados alguna vez (sustento de la retención). */
178
+ private readonly visitados = signal<ReadonlySet<string>>(new Set());
179
+
180
+ /** Paneles en DOM: activo + retenidos visitados que siguen visibles. */
181
+ readonly montados = computed<readonly ViewChildNode[]>(() => {
182
+ const visibles = new Set(this.visibles().map((h) => h.id));
183
+ const activo = this.activo();
184
+ const visitados = this.visitados();
185
+ return this.hijos().filter(
186
+ (h) => visibles.has(h.id) && (h.id === activo || (h.retain === true && visitados.has(h.id))),
187
+ );
188
+ });
189
+
190
+ constructor() {
191
+ // D2 — validar TODO el schema antes del primer render (fail-loud).
192
+ const schema = this.schema();
193
+ if (schema !== undefined) {
194
+ try {
195
+ validateComposition(schema);
196
+ } catch (e) {
197
+ this.errorValidacion.set(e instanceof Error ? e.message : String(e));
198
+ }
199
+ }
200
+ // La pestaña activa al montar cuenta como visitada: al cambiar de
201
+ // pestaña, un hijo `retain:true` que estaba activo conserva su panel.
202
+ const inicial = this.activo();
203
+ if (inicial !== '') this.visitados.set(new Set([inicial]));
204
+ }
205
+
206
+ /** Selecciona una pestaña y, opcionalmente, mueve el foco a su botón. */
207
+ seleccionar(id: string, conFoco: boolean): void {
208
+ this.seleccion.set(id);
209
+ this.visitados.update((v) => new Set(v).add(id));
210
+ if (conFoco) {
211
+ setTimeout(() => document.getElementById(`tab-${id}`)?.focus(), 0);
212
+ }
213
+ }
214
+
215
+ /** Navegación por teclado del tablist: flechas con wrap, Home/End. */
216
+ onKeydown(event: KeyboardEvent): void {
217
+ const vis = this.visibles();
218
+ if (vis.length === 0) return;
219
+ const i = vis.findIndex((h) => h.id === this.activo());
220
+ let next = -1;
221
+ if (event.key === 'ArrowRight') next = (i + 1) % vis.length;
222
+ else if (event.key === 'ArrowLeft') next = (i - 1 + vis.length) % vis.length;
223
+ else if (event.key === 'Home') next = 0;
224
+ else if (event.key === 'End') next = vis.length - 1;
225
+ if (next < 0) return;
226
+ event.preventDefault();
227
+ this.seleccionar(vis[next].id, true);
228
+ }
229
+
230
+ /** El panel de este hijo está montado (⇒ existe su `aria-controls`). */
231
+ montado(id: string): boolean {
232
+ return this.montados().some((h) => h.id === id);
233
+ }
234
+
235
+ /** Clases del botón de pestaña: activo resaltado con token `primary`. */
236
+ claseTab(id: string): string {
237
+ const base =
238
+ 'rounded-t-lg px-3 py-2 text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary';
239
+ return id === this.activo()
240
+ ? `${base} bg-primary/10 font-medium text-primary`
241
+ : `${base} text-neutral hover:text-primary`;
242
+ }
243
+
244
+ /** Etiqueta a11y del tablist (la del recurso). */
245
+ etiqueta(): string {
246
+ return this.schema()?.labelPlural ?? 'Secciones';
247
+ }
248
+ }
@@ -0,0 +1,54 @@
1
+ // @ts-ignore — `node:fs` no tiene tipos en este proyecto (sin @types/node);
2
+ // el runtime de tests corre en Node y el builtin resuelve igual.
3
+ import { readFileSync } from 'node:fs';
4
+ // @ts-ignore — idem para `node:url`.
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { describe, expect, it } from 'vitest';
8
+
9
+ /**
10
+ * R9.9 · slice 3 — Guardia arquitectónica sobre el compositor (D2).
11
+ *
12
+ * El host debe ser GENÉRICO: sin imports de hojas, sin strings de dominio y
13
+ * sin `switch` de tipos — cualquier página nueva declarada en un schema se
14
+ * compone sin tocar el compositor. Este test lee el fuente REAL desde disco
15
+ * (no el bundle) y falla si aparece un acoplamiento al dominio.
16
+ */
17
+ function fuente(nombre: string): string {
18
+ return readFileSync(fileURLToPath(new URL(nombre, import.meta.url)), 'utf8');
19
+ }
20
+
21
+ const COMPOSITOR = ['composer-host.ts', 'view-slot.ts', 'view-context.ts'];
22
+
23
+ describe('composer source-scan: el compositor no conoce el dominio', () => {
24
+ it('no importa hojas, páginas ni vistas de dominio', () => {
25
+ for (const nombre of COMPOSITOR) {
26
+ const codigo = fuente(nombre);
27
+ expect(codigo, `${nombre} importa una hoja/página de dominio`).not.toMatch(
28
+ /from ['"](\.\.\/)+(pages|views\/(calendar-view|kanban-view))/,
29
+ );
30
+ expect(codigo, `${nombre} referencia una hoja por import dinámico`).not.toMatch(
31
+ /import\(['"](\.\.\/)+(pages|views\/(calendar-view|kanban-view))/,
32
+ );
33
+ expect(codigo, `${nombre} menciona un tipo de dominio`).not.toMatch(
34
+ /'calendar'|'kanban'|'resource-page'|'form-view'|'detail-view'/,
35
+ );
36
+ }
37
+ });
38
+
39
+ it('no contiene un switch de tipos (dispatch exclusivamente por registro)', () => {
40
+ for (const nombre of COMPOSITOR) {
41
+ expect(fuente(nombre), `${nombre} contiene un switch`).not.toMatch(/@?switch\s*\(/);
42
+ }
43
+ });
44
+
45
+ it('resuelve hijos sólo vía get(kind, type) del registro, nunca resolve()', () => {
46
+ for (const nombre of ['view-slot.ts']) {
47
+ const codigo = fuente(nombre);
48
+ expect(codigo, `${nombre} usa resolve() (fallback silencioso prohibido)`).not.toMatch(
49
+ /\bresolve\(\s*'view'/,
50
+ );
51
+ expect(codigo, `${nombre} no usa get('view', type)`).toMatch(/get\(\s*'view'\s*,/);
52
+ }
53
+ });
54
+ });