@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,327 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ inject,
7
+ signal,
8
+ } from '@angular/core';
9
+ import { toSignal } from '@angular/core/rxjs-interop';
10
+ import { ActivatedRoute } from '@angular/router';
11
+
12
+ import type {
13
+ KanbanViewConfig,
14
+ ResourceSchema,
15
+ SelectionBinding,
16
+ } from '../../core/models/schema.model';
17
+ import { ErrorMapperService } from '../../core/services/error-mapper.service';
18
+ import { DataSourceService, type SourceRow } from '../../core/services/data-source.service';
19
+ import type { SelectionEmitter } from '../composer/view-context';
20
+ import { getSchema } from '../../core/registry/schema-catalog';
21
+ import { Button } from '../../shared/components/button/button';
22
+ import { EmptyState } from '../../shared/components/empty-state/empty-state';
23
+ import { Skeleton } from '../../shared/components/skeleton/skeleton';
24
+ import {
25
+ cardSubtitle,
26
+ cardTitle,
27
+ mapKanbanRows,
28
+ type KanbanColumnBucket,
29
+ type KanbanMapping,
30
+ } from './kanban-model';
31
+
32
+ /**
33
+ * Narrowing helper: `UiViewBase` keeps `type: string` open, so comparing
34
+ * `view.type === 'kanban'` alone does not remove the base member from the
35
+ * union. Requiring `config` in the value is what actually proves the view is
36
+ * a `UiViewKanban` at runtime.
37
+ */
38
+ function kanbanConfigOf(view: unknown): KanbanViewConfig | undefined {
39
+ if (!view || typeof view !== 'object') return undefined;
40
+ const candidate = view as { type?: unknown; config?: unknown };
41
+ return candidate.type === 'kanban' && candidate.config !== undefined
42
+ ? (candidate.config as KanbanViewConfig)
43
+ : undefined;
44
+ }
45
+
46
+ type Status = 'loading' | 'ready' | 'error';
47
+
48
+ /** Label of the bucket for rows without a group value (Spanish UI copy). */
49
+ const UNKNOWN_LABEL = 'Sin valor';
50
+
51
+ /**
52
+ * Synthetic `value` of that bucket: a real group value can never collide
53
+ * with it (a declared `value` is non-empty and the grouping never produces
54
+ * this literal), so `@for` tracking stays unique.
55
+ */
56
+ const UNKNOWN_VALUE = '__sin_valor__';
57
+
58
+ /**
59
+ * Generic kanban view (R9.8.4): rows of the declared
60
+ * `KanbanViewConfig.source` (or the resource's own listing) grouped by
61
+ * `groupField` into the declared columns, client-side.
62
+ *
63
+ * Data flow: ONE fetcher (`DataSourceService.rows`) — there is no dynamic
64
+ * context here, so `ctx` is `undefined`; the 30 s cache of the service is
65
+ * what keeps repeated visits free.
66
+ *
67
+ * Read-only by design (R9.8.1): no drag-and-drop, no move buttons, no
68
+ * mutation path at all. Colour never carries meaning alone — every count is
69
+ * text — and an empty column is a visible «Sin tarjetas», never a gap.
70
+ */
71
+ @Component({
72
+ selector: 'app-kanban-view',
73
+ changeDetection: ChangeDetectionStrategy.OnPush,
74
+ imports: [Button, EmptyState, Skeleton],
75
+ template: `
76
+ @if (config(); as cfg) {
77
+ <section class="space-y-4" aria-label="Tablero" [attr.aria-busy]="status() === 'loading'">
78
+ <header class="flex flex-wrap items-center justify-between gap-3">
79
+ <h2 class="font-display text-xl font-semibold">Tablero</h2>
80
+ <p class="text-sm text-neutral">
81
+ Agrupado por <code>{{ cfg.groupField }}</code>
82
+ </p>
83
+ </header>
84
+
85
+ @switch (status()) {
86
+ @case ('loading') {
87
+ <app-skeleton [lines]="6" [height]="32" />
88
+ }
89
+ @case ('error') {
90
+ <app-empty-state title="No se pudo cargar el tablero" [message]="errorMessage()">
91
+ <button app-button variant="outline" type="button" (click)="retry()">
92
+ Reintentar
93
+ </button>
94
+ </app-empty-state>
95
+ }
96
+ @case ('ready') {
97
+ <div class="flex gap-3 overflow-x-auto pb-2">
98
+ @for (column of columns(); track column.value) {
99
+ <section
100
+ class="flex w-64 shrink-0 flex-col gap-3 rounded-xl border border-neutral/20 bg-surface p-3"
101
+ [attr.data-column]="column.value"
102
+ [attr.aria-label]="columnAria(column)"
103
+ >
104
+ <header class="flex items-center justify-between gap-2">
105
+ <h3 class="text-sm font-semibold">{{ column.label }}</h3>
106
+ <span class="rounded-full bg-neutral/10 px-2 py-0.5 text-xs text-neutral">
107
+ {{ column.rows.length }}<span class="sr-only"> tarjetas</span>
108
+ </span>
109
+ </header>
110
+ @if (column.rows.length === 0) {
111
+ <p class="text-sm text-neutral">Sin tarjetas</p>
112
+ } @else {
113
+ <ul class="flex flex-col gap-2">
114
+ @for (row of column.rows; track $index) {
115
+ <li
116
+ class="rounded-lg border border-neutral/15 p-2"
117
+ [attr.tabindex]="seleccionable() ? '0' : null"
118
+ [attr.role]="seleccionable() ? 'button' : null"
119
+ [class.cursor-pointer]="seleccionable()"
120
+ [class.ring-2]="esSeleccionada(row)"
121
+ [attr.data-selectable]="seleccionable() ? 'true' : null"
122
+ (click)="seleccionar(row)"
123
+ (keydown.enter)="activar($event, row)"
124
+ (keydown.space)="activar($event, row)"
125
+ >
126
+ <p class="text-sm font-medium">{{ cardTitleOf(row) }}</p>
127
+ @if (cardSubtitleOf(row); as subtitle) {
128
+ <p class="mt-0.5 text-xs text-neutral">{{ subtitle }}</p>
129
+ }
130
+ </li>
131
+ }
132
+ </ul>
133
+ }
134
+ </section>
135
+ }
136
+ </div>
137
+ }
138
+ }
139
+ </section>
140
+ } @else {
141
+ <app-empty-state
142
+ title="Configuración inválida"
143
+ message="Esta vista no declara una configuración de kanban válida."
144
+ />
145
+ }
146
+ `,
147
+ })
148
+ export class KanbanView {
149
+ private readonly route = inject(ActivatedRoute);
150
+ // The shell mounts views via `loadComponent` (no input bindings), so the
151
+ // schema is resolved from route data exactly like CalendarView does.
152
+ private readonly routeData = toSignal(this.route.data, { requireSync: true });
153
+ private readonly dataSource = inject(DataSourceService);
154
+ private readonly errorMapper = inject(ErrorMapperService);
155
+
156
+ /**
157
+ * Resource schema for this view. The shell mounts views via
158
+ * `loadComponent` (no input bindings), so the schema is resolved from
159
+ * route data: either an explicit `schema` object (fixtures), or a
160
+ * `resourceId` looked up in the registry (the production route shape).
161
+ */
162
+ readonly schema = computed<ResourceSchema | undefined>(() => {
163
+ const data = this.routeData();
164
+ const explicit = data['schema'];
165
+ if (explicit !== undefined) return explicit as ResourceSchema;
166
+ const resourceId = data['resourceId'];
167
+ return typeof resourceId === 'string' ? getSchema(resourceId) : undefined;
168
+ });
169
+
170
+ /**
171
+ * Config resolved from `schema.uiViews[<route uiView>]`; `undefined` when
172
+ * the route does not name a kanban view with a usable config — the
173
+ * template then renders the invalid-config empty state and NOTHING is
174
+ * fetched (the effect below bails out on `undefined`).
175
+ */
176
+ readonly config = computed<KanbanViewConfig | undefined>(() => {
177
+ const schema = this.schema();
178
+ const viewName = this.routeData()['uiView'];
179
+ if (schema === undefined || typeof viewName !== 'string') return undefined;
180
+ const cfg = kanbanConfigOf(schema.uiViews?.[viewName]);
181
+ if (cfg === undefined) return undefined;
182
+ // Usable = the two references the board cannot render without: a
183
+ // non-empty `groupField` and at least one column with a non-empty value.
184
+ const usable =
185
+ typeof cfg.groupField === 'string' &&
186
+ cfg.groupField !== '' &&
187
+ Array.isArray(cfg.columns) &&
188
+ cfg.columns.length > 0 &&
189
+ cfg.columns.every((column) => typeof column?.value === 'string' && column.value !== '');
190
+ return usable ? cfg : undefined;
191
+ });
192
+
193
+ readonly status = signal<Status>('loading');
194
+ readonly rows = signal<SourceRow[]>([]);
195
+ readonly errorMessage = signal('');
196
+ /** Clave seleccionada localmente cuando el binding declara `mode:'inline'`. */
197
+ readonly seleccionada = signal<string | null>(null);
198
+
199
+ /** Guards against a slow response overwriting a newer load's rows. */
200
+ private loadEpoch = 0;
201
+
202
+ constructor() {
203
+ effect(() => {
204
+ const cfg = this.config();
205
+ if (cfg === undefined) return;
206
+ void this.load(cfg);
207
+ });
208
+ }
209
+
210
+ /** Rows grouped onto columns + the visible `unknown` bucket (pure model). */
211
+ readonly mapping = computed<KanbanMapping>(() => {
212
+ const cfg = this.config();
213
+ const schema = this.schema();
214
+ if (cfg === undefined || schema === undefined) return { columns: [], unknown: [] };
215
+ return mapKanbanRows(this.rows(), cfg);
216
+ });
217
+
218
+ /**
219
+ * Columns to paint: the model's buckets plus the `unknown` bucket at the
220
+ * END — but only when it actually has rows, otherwise the board would show
221
+ * an empty column nobody declared.
222
+ */
223
+ readonly columns = computed<readonly KanbanColumnBucket[]>(() => {
224
+ const { columns, unknown } = this.mapping();
225
+ if (unknown.length === 0) return columns;
226
+ return [...columns, { value: UNKNOWN_VALUE, label: UNKNOWN_LABEL, rows: unknown }];
227
+ });
228
+
229
+ retry(): void {
230
+ const cfg = this.config();
231
+ if (cfg !== undefined) void this.load(cfg);
232
+ }
233
+
234
+ /** Clave estable de la fila (mismo campo que el resto del motor). */
235
+ private keyOf(row: SourceRow): string {
236
+ const schema = this.schema();
237
+ const key = schema ? row[schema.keyField] : undefined;
238
+ return key === undefined || key === null ? '' : String(key);
239
+ }
240
+
241
+ /** Binding `select` declarado por la vista (R9.9.4 · decision table). */
242
+ private binding(): SelectionBinding | undefined {
243
+ return this.routeData()['select'] as SelectionBinding | undefined;
244
+ }
245
+
246
+ /**
247
+ * Clic de tarjeta. Sin declaración ⇒ inerte (el tablero no navegaba);
248
+ * con declaración ⇒ sólo el contrato declarado corre: `route` emite al
249
+ * dispatcher, `inline` selecciona en local, `none` es no-op explícito.
250
+ */
251
+ /**
252
+ * R9.8.7 — la misma selección que el clic, operable con teclado (Enter y
253
+ * Space sobre la tarjeta focusable). Sólo tiene sentido con `select`
254
+ * declarado: sin binding la tarjeta no es focusable y esto nunca corre.
255
+ */
256
+ activar(event: Event, row: SourceRow): void {
257
+ if (!this.seleccionable()) return;
258
+ event.preventDefault(); // Space no debe desplazar la página
259
+ this.seleccionar(row);
260
+ }
261
+
262
+ seleccionar(row: SourceRow): void {
263
+ const binding = this.binding();
264
+ if (binding === undefined) return;
265
+ if (binding.mode === 'inline') {
266
+ this.seleccionada.set(this.keyOf(row));
267
+ return;
268
+ }
269
+ if (binding.mode === 'none') return;
270
+ const schema = this.schema();
271
+ const emit = this.routeData()['emit'] as SelectionEmitter | undefined;
272
+ emit?.({
273
+ resource: schema?.id ?? String(this.routeData()['resourceId'] ?? ''),
274
+ key: this.keyOf(row),
275
+ action: binding.on,
276
+ });
277
+ }
278
+
279
+ /** El DOM declara clicable sólo si hay contrato que responder. */
280
+ seleccionable(): boolean {
281
+ const binding = this.binding();
282
+ return binding !== undefined && binding.mode !== 'none';
283
+ }
284
+
285
+ esSeleccionada(row: SourceRow): boolean {
286
+ return this.seleccionable() && this.seleccionada() === this.keyOf(row);
287
+ }
288
+
289
+ /** «Por hacer: 2 tarjetas» — the count is text, never colour alone. */
290
+ columnAria(column: KanbanColumnBucket): string {
291
+ const count = column.rows.length;
292
+ return `${column.label}: ${count} ${count === 1 ? 'tarjeta' : 'tarjetas'}`;
293
+ }
294
+
295
+ cardTitleOf(row: SourceRow): string {
296
+ const cfg = this.config();
297
+ const schema = this.schema();
298
+ if (cfg === undefined || schema === undefined) return '';
299
+ return cardTitle(row, cfg, schema);
300
+ }
301
+
302
+ cardSubtitleOf(row: SourceRow): string {
303
+ const cfg = this.config();
304
+ const schema = this.schema();
305
+ if (cfg === undefined || schema === undefined) return '';
306
+ return cardSubtitle(row, cfg, schema);
307
+ }
308
+
309
+ private async load(cfg: KanbanViewConfig): Promise<void> {
310
+ const schema = this.schema();
311
+ if (schema === undefined) return;
312
+ const epoch = ++this.loadEpoch;
313
+ this.status.set('loading');
314
+ const source = cfg.source ?? { resource: schema.id };
315
+ try {
316
+ const rows = await this.dataSource.rows(source, undefined);
317
+ if (epoch !== this.loadEpoch) return;
318
+ this.rows.set(rows);
319
+ this.errorMessage.set('');
320
+ this.status.set('ready');
321
+ } catch (error) {
322
+ if (epoch !== this.loadEpoch) return;
323
+ this.errorMessage.set(this.errorMapper.map(error).general ?? 'No se pudo cargar el tablero.');
324
+ this.status.set('error');
325
+ }
326
+ }
327
+ }
@@ -0,0 +1,17 @@
1
+ import type { BarChartWidget } from '../../core/models/schema.model';
2
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
3
+
4
+ import { toSeries } from '../series';
5
+
6
+ /** Descriptor del widget `bar-chart` (R3.6). */
7
+ export const descriptor: RenderDescriptor = {
8
+ kind: 'widget',
9
+ type: 'bar-chart',
10
+ since: '1.0',
11
+ load: () => import('./bar-chart').then((module) => module.BarChart),
12
+ surfaces: ['display'],
13
+ inputs: (config, extras) => {
14
+ const cfg = config as BarChartWidget;
15
+ return { label: cfg.label, data: toSeries(extras.data), unit: cfg.unit ?? '' };
16
+ },
17
+ };
@@ -0,0 +1,66 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { BarChart, type BarDatum } from './bar-chart';
4
+
5
+ const DATA: BarDatum[] = [
6
+ { label: 'Lun', value: 3 },
7
+ { label: 'Mar', value: 5 },
8
+ { label: 'Mié', value: 1 },
9
+ ];
10
+
11
+ describe('BarChart', () => {
12
+ let fixture: ComponentFixture<BarChart>;
13
+
14
+ const root = (): HTMLElement => fixture.nativeElement as HTMLElement;
15
+ const bars = (): HTMLElement[] => [...root().querySelectorAll<HTMLElement>('[role="img"] > div')];
16
+
17
+ beforeEach(async () => {
18
+ await TestBed.configureTestingModule({ imports: [BarChart] }).compileComponents();
19
+ fixture = TestBed.createComponent(BarChart);
20
+ fixture.componentRef.setInput('label', 'Entregas');
21
+ });
22
+
23
+ it('renderiza N barras con el resumen en aria-label', () => {
24
+ fixture.componentRef.setInput('data', DATA);
25
+ fixture.detectChanges();
26
+
27
+ expect(bars().length).toBe(3);
28
+ expect(root().textContent).toContain('Entregas');
29
+ expect(root().textContent).toContain('Lun');
30
+
31
+ const chart = root().querySelector('[role="img"]');
32
+ expect(chart?.getAttribute('aria-label')).toContain('3 categorías');
33
+ expect(chart?.getAttribute('aria-label')).toContain('Mayor: Mar con 5');
34
+ });
35
+
36
+ it('data vacía ⇒ «Sin datos» y sin grilla accesible', () => {
37
+ fixture.componentRef.setInput('data', []);
38
+ fixture.detectChanges();
39
+
40
+ expect(root().querySelector('[data-testid="empty"]')?.textContent).toContain('Sin datos');
41
+ expect(root().querySelector('[role="img"]')).toBeNull();
42
+ expect(bars().length).toBe(0);
43
+ });
44
+
45
+ it('el unit se añade a las cifras del resumen', () => {
46
+ fixture.componentRef.setInput('data', DATA);
47
+ fixture.componentRef.setInput('unit', ' u.');
48
+ fixture.detectChanges();
49
+
50
+ expect(root().querySelector('[role="img"]')?.getAttribute('aria-label')).toContain('con 5 u.');
51
+ });
52
+
53
+ it('con todos los valores en 0 no divide entre cero', () => {
54
+ fixture.componentRef.setInput('data', [
55
+ { label: 'A', value: 0 },
56
+ { label: 'B', value: 0 },
57
+ ]);
58
+ fixture.detectChanges();
59
+
60
+ expect(bars().length).toBe(2);
61
+ const heights = bars().map(
62
+ (bar) => bar.querySelector<HTMLElement>('div[style*="height"]')?.style.height,
63
+ );
64
+ expect(heights).toEqual(['0%', '0%']);
65
+ });
66
+ });
@@ -0,0 +1,76 @@
1
+ import { Component, computed, input } from '@angular/core';
2
+
3
+ export interface BarDatum {
4
+ label: string;
5
+ value: number;
6
+ }
7
+
8
+ /**
9
+ * Barras por categoría, sin librería: se dibujan con CSS y un `@for` de
10
+ * Angular (~1 K gz frente a los 61,4 K de Chart.js — viabilidad B2.1).
11
+ *
12
+ * Accesibilidad (plan 3.2): la grilla es `role="img"` con un `aria-label`
13
+ * que resume los datos, de modo que un lector de pantalla no recorra cada
14
+ * barra suelta sino que escuche «8 categorías. Mayor: …».
15
+ */
16
+ @Component({
17
+ selector: 'app-bar-chart',
18
+ template: `
19
+ <div class="rounded-lg border border-neutral/20 bg-surface p-4">
20
+ <p class="mb-3 text-sm text-neutral">{{ label() }}</p>
21
+
22
+ @if (data().length === 0) {
23
+ <p class="text-sm text-neutral/60" data-testid="empty">Sin datos.</p>
24
+ } @else {
25
+ <div role="img" [attr.aria-label]="summary()" class="flex h-32 items-end gap-2">
26
+ @for (row of data(); track row.label) {
27
+ <div class="flex min-w-0 flex-1 flex-col items-center justify-end gap-1">
28
+ <span class="text-xs tabular-nums text-neutral"> {{ row.value }}{{ unit() }} </span>
29
+ <div class="w-full rounded-t bg-primary" [style.height.%]="height(row.value)"></div>
30
+ <span class="w-full truncate text-center text-xs text-neutral">{{ row.label }}</span>
31
+ </div>
32
+ }
33
+ </div>
34
+ }
35
+ </div>
36
+ `,
37
+ })
38
+ export class BarChart {
39
+ /** Título del gráfico. */
40
+ readonly label = input.required<string>();
41
+ /** Series a dibujar; vacío ⇒ estado «Sin datos». */
42
+ readonly data = input<BarDatum[]>([]);
43
+ /** Sufijo de las cifras del eje («unidades», «S/»). */
44
+ readonly unit = input('');
45
+
46
+ private readonly max = computed(() => Math.max(0, ...this.data().map((row) => row.value)));
47
+
48
+ /**
49
+ * Altura relativa de una barra. `max = 0` ⇒ 0 % (nada de división por
50
+ * cero); un valor de 0 se queda en 0, y los demás bajan a un 2 % mínimo
51
+ * para que la barra siga siendo visible.
52
+ */
53
+ protected height(value: number): number {
54
+ if (value === 0) {
55
+ return 0;
56
+ }
57
+ const max = this.max();
58
+ if (max === 0) {
59
+ return 0;
60
+ }
61
+ return Math.max(2, (value / max) * 100);
62
+ }
63
+
64
+ protected readonly summary = computed(() => {
65
+ const rows = this.data();
66
+ if (rows.length === 0) {
67
+ return 'Sin datos.';
68
+ }
69
+ const top = rows.reduce((best, row) => (row.value > best.value ? row : best), rows[0]);
70
+ const unit = this.unit();
71
+ return (
72
+ `${rows.length} categorías. Mayor: ${top.label} con ${top.value}${unit}. ` +
73
+ `Menor: ${rows.reduce((low, row) => (row.value < low.value ? row : low), rows[0]).label}.`
74
+ );
75
+ });
76
+ }
@@ -0,0 +1,17 @@
1
+ import type { ChartLineWidget } from '../../core/models/schema.model';
2
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
3
+
4
+ import { toSeries } from '../series';
5
+
6
+ /** Descriptor del widget `chart-line` (R3.6). */
7
+ export const descriptor: RenderDescriptor = {
8
+ kind: 'widget',
9
+ type: 'chart-line',
10
+ since: '1.0',
11
+ load: () => import('./chart-line').then((module) => module.ChartLine),
12
+ surfaces: ['display'],
13
+ inputs: (config, extras) => {
14
+ const cfg = config as ChartLineWidget;
15
+ return { label: cfg.label, data: toSeries(extras.data), unit: cfg.unit ?? '' };
16
+ },
17
+ };
@@ -0,0 +1,113 @@
1
+ import {
2
+ DeferBlockBehavior,
3
+ DeferBlockState,
4
+ ComponentFixture,
5
+ TestBed,
6
+ } from '@angular/core/testing';
7
+
8
+ import { ChartLine } from './chart-line';
9
+
10
+ /**
11
+ * jsdom no implementa el lienzo 2D: `getContext('2d')` devuelve `null` y
12
+ * Chart.js revienta al primer trazo. Se stubbea por `beforeAll` para poder
13
+ * montar el componente de verdad y se restaura en `afterAll`.
14
+ */
15
+ const canvasStub = { width: 300, height: 150 } as HTMLCanvasElement;
16
+
17
+ const ctxStub = new Proxy({} as unknown as CanvasRenderingContext2D, {
18
+ get(_target, prop) {
19
+ if (prop === 'canvas') {
20
+ return canvasStub;
21
+ }
22
+ if (prop === 'measureText') {
23
+ return () => ({ width: 10 });
24
+ }
25
+ if (prop === 'createLinearGradient') {
26
+ return () => ({ addColorStop: () => undefined });
27
+ }
28
+ return () => undefined;
29
+ },
30
+ set() {
31
+ return true;
32
+ },
33
+ });
34
+
35
+ describe('ChartLine', () => {
36
+ let fixture: ComponentFixture<ChartLine>;
37
+
38
+ const root = (): HTMLElement => fixture.nativeElement as HTMLElement;
39
+ const originalGetContext = HTMLCanvasElement.prototype.getContext;
40
+
41
+ /**
42
+ * `on viewport` necesita `IntersectionObserver`, que jsdom no implementa:
43
+ * con `Manual` el bloque no evalúa su condición y lo pintamos a mano.
44
+ */
45
+ const pintarDefer = async (estado: DeferBlockState): Promise<void> => {
46
+ const blocks = await fixture.getDeferBlocks();
47
+ await blocks[0].render(estado);
48
+ fixture.detectChanges();
49
+ };
50
+
51
+ beforeAll(() => {
52
+ HTMLCanvasElement.prototype.getContext = (() =>
53
+ ctxStub) as unknown as typeof originalGetContext;
54
+ });
55
+
56
+ afterAll(() => {
57
+ HTMLCanvasElement.prototype.getContext = originalGetContext;
58
+ });
59
+
60
+ beforeEach(async () => {
61
+ await TestBed.configureTestingModule({
62
+ imports: [ChartLine],
63
+ deferBlockBehavior: DeferBlockBehavior.Manual,
64
+ }).compileComponents();
65
+ fixture = TestBed.createComponent(ChartLine);
66
+ });
67
+
68
+ it('sin datos muestra el estado vacío y no toca el lienzo', async () => {
69
+ fixture.componentRef.setInput('label', 'Entregas por día');
70
+ fixture.componentRef.setInput('data', []);
71
+ fixture.detectChanges();
72
+
73
+ expect(root().querySelector('[data-testid="empty"]')?.textContent?.trim()).toBe('Sin datos.');
74
+ expect(root().querySelector('canvas')).toBeNull();
75
+ // Sin datos no hay bloque que diferir: sólo existe el estado vacío.
76
+ expect(await fixture.getDeferBlocks()).toHaveLength(0);
77
+ });
78
+
79
+ it('en placeholder dibuja la miniatura SVG y no monta el lienzo', async () => {
80
+ fixture.componentRef.setInput('label', 'Entregas por día');
81
+ fixture.componentRef.setInput('data', [
82
+ { label: 'Lun', value: 2 },
83
+ { label: 'Mar', value: 5 },
84
+ ]);
85
+ fixture.detectChanges();
86
+
87
+ await pintarDefer(DeferBlockState.Placeholder);
88
+
89
+ expect(root().querySelector('canvas')).toBeNull();
90
+ expect(root().querySelector('[role="img"]')).toBeNull();
91
+ expect(root().querySelector('polyline')?.getAttribute('points')?.split(' ')).toHaveLength(2);
92
+ expect(root().textContent).toContain('Entregas por día');
93
+ });
94
+
95
+ it('completado el defer dibuja el lienzo y resume la serie en el aria-label', async () => {
96
+ fixture.componentRef.setInput('label', 'Entregas por día');
97
+ fixture.componentRef.setInput('unit', ' u.');
98
+ fixture.componentRef.setInput('data', [
99
+ { label: 'Lun', value: 2 },
100
+ { label: 'Mar', value: 5 },
101
+ ]);
102
+ fixture.detectChanges();
103
+
104
+ await pintarDefer(DeferBlockState.Complete);
105
+
106
+ const chart = root().querySelector('[role="img"]');
107
+ expect(root().querySelector('canvas')).not.toBeNull();
108
+ expect(chart?.getAttribute('aria-label')).toBe(
109
+ 'Serie de 2 puntos, de Lun (2 u.) a Mar (5 u.): sube.',
110
+ );
111
+ expect(root().textContent).toContain('Entregas por día');
112
+ });
113
+ });