@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,109 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { provideHttpClient } from '@angular/common/http';
3
+ import { provideHttpClientTesting } from '@angular/common/http/testing';
4
+ import { provideRouter, Router } from '@angular/router';
5
+ import { of } from 'rxjs';
6
+ import { vi } from 'vitest';
7
+
8
+ import type { AuthProvider } from '../../core/models/schema.model';
9
+ import { setSchemaCatalog } from '../../core/registry/schema-catalog';
10
+ import { AuthService } from '../../core/services/auth.service';
11
+ import { API_URL } from '../../core/tokens';
12
+ import { Login } from './login';
13
+
14
+ describe('Login component', () => {
15
+ let fixture: ComponentFixture<Login>;
16
+ let component: Login;
17
+ let auth: AuthService;
18
+ let router: Router;
19
+
20
+ function setup(providers: AuthProvider[], hasFirebase = false) {
21
+ TestBed.resetTestingModule();
22
+ setSchemaCatalog([], {
23
+ providers,
24
+ loginFields: [
25
+ { key: 'email', type: 'email', required: true },
26
+ { key: 'password', type: 'password', required: true },
27
+ ],
28
+ redirectAfterLogin: '/dashboard',
29
+ rememberMe: false,
30
+ ...(hasFirebase ? { firebase: { exchangeEndpoint: '/auth/firebase' } } : {}),
31
+ });
32
+
33
+ TestBed.configureTestingModule({
34
+ imports: [Login],
35
+ providers: [
36
+ provideHttpClient(),
37
+ provideHttpClientTesting(),
38
+ provideRouter([]),
39
+ { provide: API_URL, useValue: '/api' },
40
+ ],
41
+ });
42
+
43
+ auth = TestBed.inject(AuthService);
44
+ router = TestBed.inject(Router);
45
+ vi.spyOn(router, 'navigateByUrl').mockResolvedValue(true);
46
+ fixture = TestBed.createComponent(Login);
47
+ component = fixture.componentInstance;
48
+ fixture.detectChanges();
49
+ }
50
+
51
+ afterEach(() => {
52
+ TestBed.resetTestingModule();
53
+ });
54
+
55
+ it('providers = [password], sin firebase: no muestra botón de Google y submit() llama a auth.login()', () => {
56
+ setup(['password'], false);
57
+ const compiled = fixture.nativeElement as HTMLElement;
58
+ expect(compiled.querySelector('#btn-login-google')).toBeNull();
59
+
60
+ const loginSpy = vi.spyOn(auth, 'login').mockReturnValue(
61
+ of({ id: 'u_1', email: 'test@example.com', role: 'admin' }),
62
+ );
63
+
64
+ component.setValue('email', 'test@example.com');
65
+ component.setValue('password', 'secret');
66
+
67
+ const form = compiled.querySelector('form')!;
68
+ form.dispatchEvent(new Event('submit'));
69
+
70
+ expect(loginSpy).toHaveBeenCalledWith('test@example.com', 'secret');
71
+ expect(router.navigateByUrl).toHaveBeenCalledWith('/dashboard');
72
+ });
73
+
74
+ it('providers = [google, password]: muestra botón de Google en el DOM', () => {
75
+ setup(['google', 'password'], false);
76
+ const compiled = fixture.nativeElement as HTMLElement;
77
+ expect(compiled.querySelector('#btn-login-google')).not.toBeNull();
78
+ expect(compiled.querySelector('#btn-login-google')?.textContent).toContain('Continuar con Google');
79
+ });
80
+
81
+ it('loginWithGoogle() llama a auth.loginWithGoogle() y navega al tener éxito', () => {
82
+ setup(['google', 'password'], false);
83
+ const googleSpy = vi.spyOn(auth, 'loginWithGoogle').mockReturnValue(
84
+ of({ id: 'u_google', email: 'google@test.com', role: 'admin' }),
85
+ );
86
+
87
+ component.loginWithGoogle();
88
+
89
+ expect(googleSpy).toHaveBeenCalled();
90
+ expect(router.navigateByUrl).toHaveBeenCalledWith('/dashboard');
91
+ });
92
+
93
+ it('providers = [password] con firebase: submit() delega a loginWithFirebasePassword()', () => {
94
+ setup(['password'], true);
95
+ const fbPwSpy = vi.spyOn(auth, 'loginWithFirebasePassword').mockReturnValue(
96
+ of({ id: 'u_pw', email: 'pw@test.com', role: 'admin' }),
97
+ );
98
+
99
+ component.setValue('email', 'pw@test.com');
100
+ component.setValue('password', 'secret');
101
+
102
+ const compiled = fixture.nativeElement as HTMLElement;
103
+ const form = compiled.querySelector('form')!;
104
+ form.dispatchEvent(new Event('submit'));
105
+
106
+ expect(fbPwSpy).toHaveBeenCalledWith('pw@test.com', 'secret');
107
+ expect(router.navigateByUrl).toHaveBeenCalledWith('/dashboard');
108
+ });
109
+ });
@@ -0,0 +1,227 @@
1
+ import { Component, inject, signal } from '@angular/core';
2
+ import { Router } from '@angular/router';
3
+
4
+ import { ApiError } from '../../core/models/api.model';
5
+ import { AuthService, LOGIN_PATH } from '../../core/services/auth.service';
6
+ import { Button } from '../../shared/components/button/button';
7
+ import { getAuth } from '../../core/registry/schema-catalog';
8
+
9
+ /** Copy de las etiquetas — vive acá, no en el schema: una sola pantalla. */
10
+ const LABELS: Record<string, string> = {
11
+ email: 'Correo electrónico',
12
+ password: 'Contraseña',
13
+ };
14
+
15
+ const FIELD_BASE =
16
+ 'mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary';
17
+
18
+ /**
19
+ * Login (auth.md §5.1).
20
+ *
21
+ * **Pintada a partir de `authSchema.loginFields`**: si mañana cambian los
22
+ * campos, éste componente no se toca — sólo se mueven claves y etiquetas.
23
+ * No usa `FieldHost` porque el tipo `password` todavía no existe en
24
+ * `FieldSchema` (es pieza nueva de Fase 3, R3.3); el `@switch` de abajo es
25
+ * exactamente el reemplazo que R3.x hará por el registro de render.
26
+ *
27
+ * **No hay casilla «recordarme»**: D10-3 negó `localStorage` (§9.3) y el
28
+ * token muere con la pestaña. Poner un checkbox sería prometer que la
29
+ * sesión sobrevive a cerrar el navegador.
30
+ */
31
+ @Component({
32
+ selector: 'app-login',
33
+ imports: [Button],
34
+ template: `
35
+ <main class="flex min-h-[100svh] items-center justify-center bg-surface px-4">
36
+ <div
37
+ class="w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm"
38
+ >
39
+ <h1 class="font-display text-lg font-semibold">Admin</h1>
40
+ <p class="mt-1 text-sm text-neutral">Inicia sesión para continuar</p>
41
+
42
+ @if (providers.includes('google')) {
43
+ <button
44
+ type="button"
45
+ id="btn-login-google"
46
+ app-button
47
+ variant="outline"
48
+ [loading]="providerLoading() === 'google'"
49
+ (click)="loginWithGoogle()"
50
+ class="mt-6 w-full"
51
+ >
52
+ Continuar con Google
53
+ </button>
54
+ <div class="my-4 flex items-center gap-3">
55
+ <hr class="flex-1 border-neutral/20" />
56
+ <span class="text-xs text-neutral/50">o</span>
57
+ <hr class="flex-1 border-neutral/20" />
58
+ </div>
59
+ }
60
+
61
+ <form
62
+ [class]="providers.includes('google') ? 'space-y-4' : 'mt-6 space-y-4'"
63
+ (submit)="submit($event)"
64
+ novalidate
65
+ >
66
+ @for (field of schema.loginFields; track field.key) {
67
+ <div>
68
+ <label [attr.for]="field.key" class="block text-sm font-medium text-neutral">
69
+ {{ label(field.key) }}
70
+ </label>
71
+
72
+ @switch (field.type) {
73
+ @case ('email') {
74
+ <input
75
+ [attr.id]="field.key"
76
+ [attr.name]="field.key"
77
+ type="email"
78
+ autocomplete="email"
79
+ inputmode="email"
80
+ [attr.required]="field.required ? '' : null"
81
+ [value]="value(field.key)"
82
+ (input)="setValue(field.key, $any($event.target).value)"
83
+ class="${FIELD_BASE}"
84
+ />
85
+ }
86
+ @default {
87
+ <input
88
+ [attr.id]="field.key"
89
+ [attr.name]="field.key"
90
+ type="password"
91
+ autocomplete="current-password"
92
+ [attr.required]="field.required ? '' : null"
93
+ [value]="value(field.key)"
94
+ (input)="setValue(field.key, $any($event.target).value)"
95
+ class="${FIELD_BASE}"
96
+ />
97
+ }
98
+ }
99
+ </div>
100
+ }
101
+
102
+ @if (error(); as message) {
103
+ <p role="alert" class="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">
104
+ {{ message }}
105
+ </p>
106
+ }
107
+
108
+ <button app-button type="submit" [loading]="loading()" class="w-full">
109
+ Iniciar sesión
110
+ </button>
111
+ </form>
112
+ </div>
113
+ </main>
114
+ `,
115
+ })
116
+ export class Login {
117
+ private readonly auth = inject(AuthService);
118
+ private readonly router = inject(Router);
119
+
120
+ // Se resuelve en runtime: el catálogo lo fija `provideGasAdmin` en el bootstrap.
121
+ readonly schema = getAuth()!;
122
+ readonly providers = this.schema.providers;
123
+ readonly error = signal<string | null>(null);
124
+ readonly loading = signal(false);
125
+ readonly providerLoading = signal<string | null>(null);
126
+
127
+ /** Valores por clave: el set lo pinta el `@for` del schema. */
128
+ private readonly values = signal<Record<string, string>>({});
129
+
130
+ label(key: string): string {
131
+ return LABELS[key] ?? key;
132
+ }
133
+
134
+ value(key: string): string {
135
+ return this.values()[key] ?? '';
136
+ }
137
+
138
+ setValue(key: string, next: string): void {
139
+ this.values.update((current) => ({ ...current, [key]: next }));
140
+ }
141
+
142
+ loginWithGoogle(): void {
143
+ if (this.providerLoading()) return;
144
+ this.providerLoading.set('google');
145
+ this.error.set(null);
146
+ this.auth.loginWithGoogle().subscribe({
147
+ next: () => {
148
+ this.providerLoading.set(null);
149
+ this.goAfterLogin();
150
+ },
151
+ error: (err: unknown) => {
152
+ this.providerLoading.set(null);
153
+ this.error.set(
154
+ err instanceof ApiError && err.message !== ''
155
+ ? err.message
156
+ : err instanceof Error && err.message !== ''
157
+ ? err.message
158
+ : 'No se pudo iniciar sesión con Google.',
159
+ );
160
+ },
161
+ });
162
+ }
163
+
164
+ submit(event: Event): void {
165
+ event.preventDefault();
166
+ if (this.loading()) return;
167
+
168
+ const email = this.value('email').trim();
169
+ const password = this.value('password');
170
+ if (email === '' || password === '') {
171
+ this.error.set('Completa el correo y la contraseña.');
172
+ return;
173
+ }
174
+
175
+ if (this.schema.firebase && this.schema.providers.includes('password')) {
176
+ this.loading.set(true);
177
+ this.error.set(null);
178
+ this.auth.loginWithFirebasePassword(email, password).subscribe({
179
+ next: () => {
180
+ this.loading.set(false);
181
+ this.goAfterLogin();
182
+ },
183
+ error: (err: unknown) => {
184
+ this.loading.set(false);
185
+ this.error.set(
186
+ err instanceof ApiError && err.message !== ''
187
+ ? err.message
188
+ : err instanceof Error && err.message !== ''
189
+ ? err.message
190
+ : 'No se pudo iniciar sesión.',
191
+ );
192
+ },
193
+ });
194
+ return;
195
+ }
196
+
197
+ this.loading.set(true);
198
+ this.error.set(null);
199
+ this.auth.login(email, password).subscribe({
200
+ next: () => {
201
+ this.loading.set(false);
202
+ this.goAfterLogin();
203
+ },
204
+ error: (err: unknown) => {
205
+ this.loading.set(false);
206
+ // El mensaje del backend ya viene en español y ya describe el
207
+ // caso («credenciales inválidas»): reformatearlo acá sólo
208
+ // duplicaría la verdad y se perdería si cambia el texto del back.
209
+ this.error.set(
210
+ err instanceof ApiError && err.message !== ''
211
+ ? err.message
212
+ : 'No se pudo iniciar sesión.',
213
+ );
214
+ },
215
+ });
216
+ }
217
+
218
+ private goAfterLogin(): void {
219
+ const previous = this.auth.consumeReturnUrl();
220
+ const target =
221
+ previous !== null && previous !== '' && !previous.startsWith(LOGIN_PATH)
222
+ ? previous
223
+ : this.schema.redirectAfterLogin;
224
+ void this.router.navigateByUrl(target).catch(() => undefined);
225
+ }
226
+ }
227
+
@@ -0,0 +1,234 @@
1
+ import { Component, computed, inject, signal } from '@angular/core';
2
+ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
3
+ import { ActivatedRoute, Router } from '@angular/router';
4
+ import { distinctUntilChanged, map, of } from 'rxjs';
5
+
6
+ import type { ResourceSchema, SelectionBinding } from '../../core/models/schema.model';
7
+ import { AuthService } from '../../core/services/auth.service';
8
+ import type { SelectionEmitter } from '../../views/composer/view-context';
9
+ import { getSchema } from '../../core/registry/schema-catalog';
10
+ import { EmptyState } from '../../shared/components/empty-state/empty-state';
11
+ import { Dashboard } from '../dashboard/dashboard';
12
+ import { DetailView } from '../detail-view/detail-view';
13
+ import { FormView } from '../form-view/form-view';
14
+ import { ListView } from '../list-view/list-view';
15
+
16
+ type View = 'list' | 'detail';
17
+
18
+ /**
19
+ * Página del recurso: resuelve el `id` de la ruta contra el registry y
20
+ * despacha según `kind` — colección → list view, singleton → form view,
21
+ * dashboard → panel de sólo lectura — aplicando `permissions` (estado
22
+ * «sin permisos»).
23
+ *
24
+ * Cuando `schema.detail.enabled === true`, intercepta el click de fila del
25
+ * `ListView` y muestra el `DetailView` en lugar de navegar al formulario.
26
+ * «Editar» desde el `DetailView` sí navega al formulario de edición.
27
+ */
28
+ @Component({
29
+ selector: 'app-resource-page',
30
+ imports: [Dashboard, DetailView, EmptyState, FormView, ListView],
31
+ template: `
32
+ @if (schema(); as schema) {
33
+ @if (hasNoAccess(schema)) {
34
+ <app-empty-state
35
+ title="Sin permisos"
36
+ message="No tiene permisos para gestionar este recurso."
37
+ />
38
+ } @else {
39
+ @switch (schema.kind) {
40
+ @case ('collection') {
41
+ @switch (currentView()) {
42
+ @case ('detail') {
43
+ @if (selectedRecord(); as rec) {
44
+ <app-detail-view
45
+ [schema]="schema"
46
+ [record]="rec"
47
+ (back)="switchToList()"
48
+ (edit)="goEdit($event)"
49
+ (delete)="switchToList()"
50
+ />
51
+ }
52
+ }
53
+ @default {
54
+ <header class="mb-6">
55
+ <h1 class="font-display text-2xl font-semibold">
56
+ {{ schema.labelPlural }}
57
+ </h1>
58
+ <p class="text-sm text-neutral">Colección · {{ schema.fields.length }} campos</p>
59
+ </header>
60
+ <app-list-view [schema]="schema" (viewRecord)="switchToDetail($event)" />
61
+ }
62
+ }
63
+ }
64
+ @case ('dashboard') {
65
+ <app-dashboard [schema]="schema" />
66
+ }
67
+ @default {
68
+ <!-- singleton: formulario directo -->
69
+ <app-form-view />
70
+ }
71
+ }
72
+ }
73
+ } @else {
74
+ <app-empty-state
75
+ title="Recurso no encontrado"
76
+ message="No existe un schema con ese id en el registry."
77
+ />
78
+ }
79
+ `,
80
+ })
81
+ export class ResourcePage {
82
+ private readonly route = inject(ActivatedRoute);
83
+ private readonly router = inject(Router);
84
+ private readonly auth = inject(AuthService);
85
+ // paramMap emite síncrono (ConvertToParamMap) → requireSync evita `| undefined`.
86
+ private readonly params = toSignal(this.route.paramMap, { requireSync: true });
87
+ private readonly routeData = toSignal(this.route.data ?? of({}), { requireSync: true });
88
+
89
+ readonly schema = computed(() => {
90
+ const resourceId = this.routeData()['resourceId'] ?? this.params().get('id');
91
+ return getSchema(typeof resourceId === 'string' ? resourceId : '');
92
+ });
93
+
94
+ /** Vista activa: 'list' (por defecto) o 'detail' (al hacer clic en una fila con detail.enabled). */
95
+ readonly currentView = signal<View>('list');
96
+ /** Registro seleccionado para la vista de detalle. */
97
+ readonly selectedRecord = signal<Record<string, unknown> | null>(null);
98
+
99
+ constructor() {
100
+ // La ficha es ENLAZABLE: `/:id/:key` (p. ej. el «Ver Ficha» del
101
+ // dashboard) entra directo al detalle. Al navegar — desde una fila o con
102
+ // el botón atrás — la URL es la única fuente de verdad de la vista.
103
+ this.route.paramMap
104
+ .pipe(
105
+ map((params) => params.get('key')),
106
+ distinctUntilChanged(),
107
+ takeUntilDestroyed(),
108
+ )
109
+ .subscribe((key) => this.openFromRoute(key));
110
+ this.route.data
111
+ ?.pipe(takeUntilDestroyed())
112
+ .subscribe(() => this.openFromRoute(this.params().get('key')));
113
+ }
114
+
115
+ private openFromRoute(key: string | null): void {
116
+ const schema = this.schema();
117
+ if (!schema || schema.kind !== 'collection') return;
118
+
119
+ const requestedView = this.routeData()['viewType'];
120
+ if ((requestedView === 'detail' || key) && key && schema.detail?.enabled) {
121
+ // Si el registro ya está cargado (clic en una fila) se conserva tal
122
+ // cual; si llegamos por URL sólo hay clave, y DetailView trae el resto.
123
+ const current = this.selectedRecord();
124
+ if (!current || String(current[schema.keyField]) !== key) {
125
+ this.selectedRecord.set({ [schema.keyField]: key });
126
+ }
127
+ this.currentView.set('detail');
128
+ } else if (this.currentView() === 'detail') {
129
+ this.selectedRecord.set(null);
130
+ this.currentView.set('list');
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Clic de fila. R9.9.4 · decision table: si el schema declara `select`,
136
+ * el contrato declarado REEMPLAZA el comportamiento local (`route` ⇒
137
+ * emisión al dispatcher, `inline` ⇒ selección local sin URL, `none` ⇒
138
+ * no-op); sin declaración, la navegación local actual sigue intacta
139
+ * (backward compat — R9.9 «existing routes untouched»).
140
+ */
141
+ switchToDetail(record: Record<string, unknown>): void {
142
+ const schema = this.schema();
143
+ const data = this.routeData();
144
+ const binding = data['select'] as SelectionBinding | undefined;
145
+
146
+ if (binding !== undefined) {
147
+ const key =
148
+ schema && record[schema.keyField] !== undefined && record[schema.keyField] !== null
149
+ ? String(record[schema.keyField])
150
+ : '';
151
+ if (binding.mode === 'route') {
152
+ const emit = data['emit'] as SelectionEmitter | undefined;
153
+ emit?.({
154
+ resource: schema?.id ?? String(data['resourceId'] ?? ''),
155
+ key,
156
+ action: binding.on,
157
+ });
158
+ return;
159
+ }
160
+ if (binding.mode === 'none') return;
161
+ // inline ⇒ selección local (ficha embebida), sin tocar la URL
162
+ this.selectedRecord.set(record);
163
+ this.currentView.set('detail');
164
+ return;
165
+ }
166
+
167
+ this.selectedRecord.set(record);
168
+ this.currentView.set('detail');
169
+ const k = schema ? record[schema.keyField] : undefined;
170
+ if (schema && k !== undefined && k !== null && String(k) !== '') {
171
+ void this.router.navigate(['/', schema.id, String(k)]);
172
+ }
173
+ }
174
+
175
+ switchToList(): void {
176
+ this.selectedRecord.set(null);
177
+ this.currentView.set('list');
178
+ const schema = this.schema();
179
+ if (schema && this.params().get('key')) {
180
+ void this.router.navigate(['/', schema.id]);
181
+ }
182
+ }
183
+
184
+ goEdit(record: Record<string, unknown>): void {
185
+ const schema = this.schema();
186
+ if (!schema) return;
187
+ const key = record[schema.keyField] as string | undefined;
188
+ if (key) {
189
+ void this.router.navigate(['/', schema.id, key, 'edit']);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Colección sin ninguna operación posible u singleton sin `update`: no hay
195
+ * nada que mostrar con sentido (base.md §10 «Sin permisos»).
196
+ *
197
+ * R7.5: usa policies cuando existan, fallback a permissions.
198
+ *
199
+ * Un dashboard nunca entra aquí: es un panel de sólo lectura, `ResourcePermissions`
200
+ * no tiene `read` y el acceso real a los datos lo decide el endpoint
201
+ * (A4: el cliente es interfaz, no autoridad).
202
+ */
203
+ hasNoAccess(schema: ResourceSchema): boolean {
204
+ // R7.5: intentar policies primero
205
+ if (schema.policies?.access) {
206
+ // Con policies, ocultar si el usuario no puede hacer NINGUNA operación
207
+ // relevante para el tipo de recurso
208
+ if (schema.kind === 'singleton') {
209
+ return !this.auth.canDoOperation(schema, 'update');
210
+ }
211
+ // collection: ocultar si no puede hacer list, create, update ni remove
212
+ return (
213
+ !this.auth.canDoOperation(schema, 'list') &&
214
+ !this.auth.canDoOperation(schema, 'create') &&
215
+ !this.auth.canDoOperation(schema, 'update') &&
216
+ !this.auth.canDoOperation(schema, 'remove')
217
+ );
218
+ }
219
+
220
+ // Fallback a permissions (legacy)
221
+ const permissions = schema.permissions;
222
+ if (!permissions || schema.kind === 'dashboard') {
223
+ return false;
224
+ }
225
+ if (schema.kind === 'singleton') {
226
+ const hasReadEndpoint = !!schema.endpoint?.get;
227
+ return !hasReadEndpoint && permissions.update === false;
228
+ }
229
+ const hasReadEndpoint = !!schema.endpoint?.list || !!schema.endpoint?.get;
230
+ const canWrite =
231
+ permissions.create !== false || permissions.update !== false || permissions.remove !== false;
232
+ return !hasReadEndpoint && !canWrite;
233
+ }
234
+ }
@@ -0,0 +1,35 @@
1
+ import { Component, computed, input } from '@angular/core';
2
+
3
+ export type BadgeKind = 'success' | 'danger' | 'warning' | 'info' | 'neutral';
4
+
5
+ const KINDS: Record<BadgeKind, string> = {
6
+ success: 'bg-success text-surface',
7
+ danger: 'bg-danger text-surface',
8
+ warning: 'bg-accent/20 text-accent-text font-semibold',
9
+ info: 'bg-primary text-surface',
10
+ neutral: 'bg-neutral text-surface',
11
+ };
12
+
13
+ /**
14
+ * Etiqueta compacta (booleanos, estados).
15
+ *
16
+ * El texto va con `text-surface` y no `text-white` porque el fondo lo da
17
+ * el token y en oscuro se aclara: blanco sobre `--color-danger` oscuro
18
+ * sería 2,8:1. `--color-surface` vale #fff en claro, así que el render
19
+ * actual no cambia, y en oscuro el texto se vuelve oscuro sobre el tono
20
+ * aclarado (6,4–9,2:1).
21
+ */
22
+ @Component({
23
+ selector: 'app-badge',
24
+ host: { '[class]': 'classes()' },
25
+ template: `{{ label() }}`,
26
+ })
27
+ export class Badge {
28
+ readonly label = input.required<string>();
29
+ readonly kind = input<BadgeKind>('neutral');
30
+
31
+ readonly classes = computed(
32
+ () =>
33
+ `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${KINDS[this.kind()]}`,
34
+ );
35
+ }
@@ -0,0 +1,65 @@
1
+ import { Component, computed, input } from '@angular/core';
2
+
3
+ export type ButtonVariant = 'primary' | 'outline' | 'danger' | 'ghost';
4
+ export type ButtonSize = 'md' | 'sm' | 'icon';
5
+
6
+ const BASE =
7
+ 'inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50';
8
+
9
+ const VARIANTS: Record<ButtonVariant, string> = {
10
+ primary: 'bg-primary text-surface hover:bg-primary/90',
11
+ outline: 'border border-neutral/40 bg-surface text-neutral hover:bg-neutral/10',
12
+ danger: 'bg-danger text-surface hover:bg-danger/90',
13
+ ghost: 'text-neutral hover:bg-neutral/10',
14
+ };
15
+
16
+ /**
17
+ * Tamaño: el padding y el tip-size viven AQUÍ, no en `BASE`. Dos botones
18
+ * con la misma propiedad (px-4 vs px-2) los resolvería el orden del CSS
19
+ * generado, no el código — el motivo por el que los chips del textarea
20
+ * acabaron con `!important`.
21
+ *
22
+ * Mobile-first: todas arrancan en `min-h-11` (44 px, objetivo táctil de la
23
+ * guía mobile §8.2) y en `lg` relajan al tamaño de escritorio, así el mismo
24
+ * componente cumple la guía táctil sin engordar la UI de escritorio.
25
+ * - `md` → botón de texto (Guardar, Cancelar…)
26
+ * - `sm` → chips y acciones compactas (texto `xs`)
27
+ * - `icon` → sólo glifo, cuadrado (toolbars de filas, arrastre)
28
+ */
29
+ const SIZES: Record<ButtonSize, string> = {
30
+ md: 'px-4 py-2 text-sm min-h-11 lg:min-h-0',
31
+ sm: 'px-2 py-1 text-xs min-h-11 lg:min-h-0',
32
+ icon: 'p-1 min-h-11 min-w-11 lg:min-h-0 lg:min-w-0',
33
+ };
34
+
35
+ /**
36
+ * Botón nativo (selector por atributo, patrón Material `button[mat-button]`):
37
+ * conserva semántica y estilos de `<button>` y las clases del consumidor.
38
+ */
39
+ @Component({
40
+ selector: 'button[app-button]',
41
+ host: {
42
+ '[class]': 'classes()',
43
+ '[type]': 'type()',
44
+ '[disabled]': 'disabled() || loading()',
45
+ '[attr.aria-busy]': 'loading() || null',
46
+ },
47
+ template: `
48
+ @if (loading()) {
49
+ <span
50
+ class="size-4 animate-spin rounded-full border-2 border-current border-t-transparent"
51
+ aria-hidden="true"
52
+ ></span>
53
+ }
54
+ <ng-content />
55
+ `,
56
+ })
57
+ export class Button {
58
+ readonly variant = input<ButtonVariant>('primary');
59
+ readonly size = input<ButtonSize>('md');
60
+ readonly type = input<'button' | 'submit'>('button');
61
+ readonly disabled = input(false);
62
+ readonly loading = input(false);
63
+
64
+ readonly classes = computed(() => `${BASE} ${VARIANTS[this.variant()]} ${SIZES[this.size()]}`);
65
+ }