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

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