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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +3 -15
  3. package/src/lib/actions/action-registry.spec.ts +52 -0
  4. package/src/lib/actions/action-registry.ts +126 -0
  5. package/src/lib/actions/action-service.spec.ts +215 -0
  6. package/src/lib/actions/action-service.ts +87 -0
  7. package/src/lib/actions/descriptors.spec.ts +155 -0
  8. package/src/lib/actions/descriptors.ts +154 -0
  9. package/src/lib/columns/column-cell.ts +33 -0
  10. package/src/lib/columns/column-defaults.spec.ts +99 -0
  11. package/src/lib/columns/column-defaults.ts +84 -0
  12. package/src/lib/columns/column-format.spec.ts +274 -0
  13. package/src/lib/columns/descriptors.ts +113 -0
  14. package/src/lib/columns/format-badge.ts +51 -0
  15. package/src/lib/columns/format-boolean.ts +58 -0
  16. package/src/lib/columns/format-currency.ts +30 -0
  17. package/src/lib/columns/format-date.ts +35 -0
  18. package/src/lib/columns/format-number.ts +32 -0
  19. package/src/lib/columns/format-text.ts +23 -0
  20. package/src/lib/columns/format-thumbnail.ts +63 -0
  21. package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
  22. package/src/lib/core/guards/auth.guard.spec.ts +88 -0
  23. package/src/lib/core/guards/auth.guard.ts +48 -0
  24. package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
  25. package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
  26. package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
  27. package/src/lib/core/models/api.model.ts +104 -0
  28. package/src/lib/core/models/schema-descriptions.ts +118 -0
  29. package/src/lib/core/models/schema.model.ts +1097 -0
  30. package/src/lib/core/registry/composition-validator.spec.ts +358 -0
  31. package/src/lib/core/registry/composition-validator.ts +270 -0
  32. package/src/lib/core/registry/field-validators.spec.ts +199 -0
  33. package/src/lib/core/registry/field-validators.ts +440 -0
  34. package/src/lib/core/registry/render-host.spec.ts +208 -0
  35. package/src/lib/core/registry/render-host.ts +646 -0
  36. package/src/lib/core/registry/render-registry.spec.ts +96 -0
  37. package/src/lib/core/registry/render-registry.ts +312 -0
  38. package/src/lib/core/registry/schema-catalog.ts +156 -0
  39. package/src/lib/core/services/api.service.spec.ts +1142 -0
  40. package/src/lib/core/services/api.service.ts +371 -0
  41. package/src/lib/core/services/auth.service.spec.ts +653 -0
  42. package/src/lib/core/services/auth.service.ts +503 -0
  43. package/src/lib/core/services/config.service.spec.ts +121 -0
  44. package/src/lib/core/services/config.service.ts +119 -0
  45. package/src/lib/core/services/data-source.service.spec.ts +179 -0
  46. package/src/lib/core/services/data-source.service.ts +142 -0
  47. package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
  48. package/src/lib/core/services/error-mapper.service.ts +49 -0
  49. package/src/lib/core/services/notification.service.ts +73 -0
  50. package/src/lib/core/services/relation-drawer.service.ts +41 -0
  51. package/src/lib/core/services/theme.service.spec.ts +106 -0
  52. package/src/lib/core/services/theme.service.ts +86 -0
  53. package/src/lib/core/services/transport.spec.ts +198 -0
  54. package/src/lib/core/services/transport.ts +164 -0
  55. package/src/lib/core/services/upload.service.ts +81 -0
  56. package/src/lib/core/testing/categories.schema.ts +125 -0
  57. package/src/lib/core/testing/orders.schema.ts +372 -0
  58. package/src/lib/core/testing/products.schema.ts +258 -0
  59. package/src/lib/core/testing/site.schema.ts +195 -0
  60. package/src/lib/core/testing/test-catalog.ts +21 -0
  61. package/src/lib/core/tokens.ts +112 -0
  62. package/src/lib/core/utils/clipboard.spec.ts +55 -0
  63. package/src/lib/core/utils/clipboard.ts +90 -0
  64. package/src/lib/core/utils/context-resolver.spec.ts +95 -0
  65. package/src/lib/core/utils/context-resolver.ts +101 -0
  66. package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
  67. package/src/lib/core/utils/response-unwrapper.ts +103 -0
  68. package/src/lib/core/utils/source-options.spec.ts +96 -0
  69. package/src/lib/core/utils/source-options.ts +54 -0
  70. package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
  71. package/src/lib/core/utils/template-interpolator.ts +56 -0
  72. package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
  73. package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
  74. package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
  75. package/src/lib/fields/boolean/boolean.ts +40 -0
  76. package/src/lib/fields/color/color.descriptor.ts +12 -0
  77. package/src/lib/fields/color/color.ts +34 -0
  78. package/src/lib/fields/currency/currency.descriptor.ts +12 -0
  79. package/src/lib/fields/currency/currency.ts +67 -0
  80. package/src/lib/fields/date/date.descriptor.ts +12 -0
  81. package/src/lib/fields/date/date.ts +36 -0
  82. package/src/lib/fields/descriptors.ts +122 -0
  83. package/src/lib/fields/display/display-badge.ts +65 -0
  84. package/src/lib/fields/display/display-date.ts +39 -0
  85. package/src/lib/fields/display/display-defaults.ts +84 -0
  86. package/src/lib/fields/display/display-file.ts +41 -0
  87. package/src/lib/fields/display/display-image.ts +54 -0
  88. package/src/lib/fields/display/display-list.ts +107 -0
  89. package/src/lib/fields/display/display-number.ts +41 -0
  90. package/src/lib/fields/display/display-text.ts +42 -0
  91. package/src/lib/fields/display/display.spec.ts +242 -0
  92. package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
  93. package/src/lib/fields/dropdown/dropdown.ts +45 -0
  94. package/src/lib/fields/email/email.descriptor.ts +12 -0
  95. package/src/lib/fields/email/email.ts +28 -0
  96. package/src/lib/fields/field-aria.ts +60 -0
  97. package/src/lib/fields/field-host/field-host.spec.ts +118 -0
  98. package/src/lib/fields/field-host/field-host.ts +47 -0
  99. package/src/lib/fields/field-node.ts +88 -0
  100. package/src/lib/fields/file/file.descriptor.ts +14 -0
  101. package/src/lib/fields/file/file.spec.ts +134 -0
  102. package/src/lib/fields/file/file.ts +281 -0
  103. package/src/lib/fields/group/group.descriptor.ts +21 -0
  104. package/src/lib/fields/group/group.ts +110 -0
  105. package/src/lib/fields/image/image.descriptor.ts +12 -0
  106. package/src/lib/fields/image/image.spec.ts +117 -0
  107. package/src/lib/fields/image/image.ts +281 -0
  108. package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
  109. package/src/lib/fields/key-value/key-value.ts +283 -0
  110. package/src/lib/fields/list/list.descriptor.ts +21 -0
  111. package/src/lib/fields/list/list.ts +376 -0
  112. package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
  113. package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
  114. package/src/lib/fields/multiselect/multiselect.ts +152 -0
  115. package/src/lib/fields/number/number.descriptor.ts +12 -0
  116. package/src/lib/fields/number/number.ts +40 -0
  117. package/src/lib/fields/phone/phone.descriptor.ts +12 -0
  118. package/src/lib/fields/phone/phone.ts +46 -0
  119. package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
  120. package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
  121. package/src/lib/fields/relation/relation.descriptor.ts +12 -0
  122. package/src/lib/fields/relation/relation.spec.ts +193 -0
  123. package/src/lib/fields/relation/relation.ts +224 -0
  124. package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
  125. package/src/lib/fields/rich-select/rich-select.ts +175 -0
  126. package/src/lib/fields/select/radio-mode.ts +25 -0
  127. package/src/lib/fields/select/select.descriptor.ts +12 -0
  128. package/src/lib/fields/select/select.spec.ts +162 -0
  129. package/src/lib/fields/select/select.ts +210 -0
  130. package/src/lib/fields/slug/slug.descriptor.ts +19 -0
  131. package/src/lib/fields/slug/slug.ts +67 -0
  132. package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
  133. package/src/lib/fields/string-list/string-list.ts +222 -0
  134. package/src/lib/fields/text/text.descriptor.ts +12 -0
  135. package/src/lib/fields/text/text.ts +35 -0
  136. package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
  137. package/src/lib/fields/textarea/textarea.spec.ts +117 -0
  138. package/src/lib/fields/textarea/textarea.ts +126 -0
  139. package/src/lib/fields/time/time.descriptor.ts +12 -0
  140. package/src/lib/fields/time/time.ts +28 -0
  141. package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
  142. package/src/lib/fields/url/url.descriptor.ts +12 -0
  143. package/src/lib/fields/url/url.ts +53 -0
  144. package/src/lib/gas-admin-ui.spec.ts +21 -0
  145. package/src/lib/gas-admin-ui.ts +9 -0
  146. package/src/lib/layouts/descriptors.ts +9 -0
  147. package/src/lib/pages/dashboard/dashboard.ts +167 -0
  148. package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
  149. package/src/lib/pages/detail-view/detail-view.ts +570 -0
  150. package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
  151. package/src/lib/pages/form-view/form-model.ts +88 -0
  152. package/src/lib/pages/form-view/form-schema.ts +255 -0
  153. package/src/lib/pages/form-view/form-view.ts +1622 -0
  154. package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
  155. package/src/lib/pages/list-view/list-view.ts +882 -0
  156. package/src/lib/pages/login/login.spec.ts +109 -0
  157. package/src/lib/pages/login/login.ts +227 -0
  158. package/src/lib/pages/resource-page/resource-page.ts +234 -0
  159. package/src/lib/shared/components/badge/badge.ts +35 -0
  160. package/src/lib/shared/components/button/button.ts +65 -0
  161. package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
  162. package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
  163. package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
  164. package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
  165. package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
  166. package/src/lib/shared/components/drawer/drawer.ts +172 -0
  167. package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
  168. package/src/lib/shared/components/modal/modal.spec.ts +149 -0
  169. package/src/lib/shared/components/modal/modal.ts +145 -0
  170. package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
  171. package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
  172. package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
  173. package/src/lib/shared/components/toast/toast.ts +58 -0
  174. package/src/lib/shared/utils/col-span.ts +25 -0
  175. package/src/lib/shared/utils/condition-evaluator.ts +53 -0
  176. package/src/lib/shared/utils/depends-on.ts +48 -0
  177. package/src/lib/shared/utils/dialog-focus.ts +28 -0
  178. package/src/lib/shared/utils/field-path.ts +49 -0
  179. package/src/lib/shared/utils/gap-sorting.ts +144 -0
  180. package/src/lib/shared/utils/seed.spec.ts +64 -0
  181. package/src/lib/shared/utils/seed.ts +149 -0
  182. package/src/lib/shared/utils/slugify.ts +13 -0
  183. package/src/lib/shared/utils/validators.ts +41 -0
  184. package/src/lib/shell/layout/layout.ts +63 -0
  185. package/src/lib/shell/shell.routes.spec.ts +59 -0
  186. package/src/lib/shell/shell.routes.ts +122 -0
  187. package/src/lib/shell/sidebar/sidebar.ts +139 -0
  188. package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
  189. package/src/lib/views/calendar-view/calendar-model.ts +426 -0
  190. package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
  191. package/src/lib/views/calendar-view/calendar-view.ts +568 -0
  192. package/src/lib/views/composer/composer-host.spec.ts +301 -0
  193. package/src/lib/views/composer/composer-host.ts +248 -0
  194. package/src/lib/views/composer/composer-imports.spec.ts +54 -0
  195. package/src/lib/views/composer/view-context.spec.ts +117 -0
  196. package/src/lib/views/composer/view-context.ts +109 -0
  197. package/src/lib/views/composer/view-slot.spec.ts +130 -0
  198. package/src/lib/views/composer/view-slot.ts +178 -0
  199. package/src/lib/views/descriptors.ts +46 -0
  200. package/src/lib/views/kanban-view/kanban-model.ts +173 -0
  201. package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
  202. package/src/lib/views/kanban-view/kanban-view.ts +327 -0
  203. package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
  204. package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
  205. package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
  206. package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
  207. package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
  208. package/src/lib/widgets/chart-line/chart-line.ts +219 -0
  209. package/src/lib/widgets/descriptors.ts +29 -0
  210. package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
  211. package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
  212. package/src/lib/widgets/metric-card/metric-card.ts +48 -0
  213. package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
  214. package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
  215. package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
  216. package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
  217. package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
  218. package/src/lib/widgets/record-list/record-list.ts +115 -0
  219. package/src/lib/widgets/series.spec.ts +42 -0
  220. package/src/lib/widgets/series.ts +56 -0
  221. package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
  222. package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
  223. package/src/lib/widgets/status-progress/status-progress.ts +104 -0
  224. package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
  225. package/src/lib/widgets/widget-host/widget-host.ts +99 -0
  226. package/src/public-api.ts +104 -0
  227. package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
  228. package/tsconfig.lib.json +13 -0
  229. package/tsconfig.lib.prod.json +11 -0
  230. package/tsconfig.spec.json +10 -0
  231. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
  232. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
  233. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
  234. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
  235. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
  236. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
  237. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
  238. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
  239. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
  240. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
  241. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
  242. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
  243. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
  244. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
  245. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
  246. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
  247. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
  248. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
  249. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
  250. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
  251. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
  252. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
  253. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
  254. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
  255. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
  256. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
  257. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
  258. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
  259. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
  260. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
  261. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
  262. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
  263. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
  264. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
  265. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
  266. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
  267. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
  268. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
  269. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
  270. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
  271. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
  272. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
  273. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
  274. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
  275. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
  276. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
  277. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
  278. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
  279. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
  280. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
  281. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
  282. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
  283. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
  284. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
  285. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
  286. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
  287. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
  288. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
  289. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
  290. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
  291. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
  292. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
  293. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
  294. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
  295. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
  296. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
  297. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
  298. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
  299. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
  300. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
  301. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
  302. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
  303. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
  304. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
  305. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
  306. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
  307. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
  308. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
  309. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
  310. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
  311. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
  312. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
  313. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
  314. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
  315. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
  316. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
  317. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
  318. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
  319. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
  320. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
  321. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
  322. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
  323. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
  324. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
  325. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
  326. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
  327. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
  328. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
  329. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
  330. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
  331. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
  332. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
  333. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
  334. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
  335. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
  336. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
  337. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
  338. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
  339. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
  340. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
  341. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
  342. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
  343. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
  344. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
  345. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
  346. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
  347. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
  348. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
  349. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
  350. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
  351. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
  352. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
  353. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
  354. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
  355. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
  356. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
  357. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
  358. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
  359. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
  360. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
  361. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
  362. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
  363. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
  364. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
  365. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
  366. package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
  367. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
  368. package/types/tinya-code-gas-admin-ui.d.ts +0 -3632
@@ -0,0 +1,149 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
3
+
4
+ import { FOCUSABLE, isVisibleNow } from '../../utils/dialog-focus';
5
+ import { Modal } from './modal';
6
+
7
+ /**
8
+ * Fase 9.3 — contrato de foco y cierre del diálogo.
9
+ *
10
+ * Mismo escenario que `drawer.spec.ts`: el disparador real está fuera del
11
+ * diálogo, así que se puede comprobar que el foco vuelve a él — también
12
+ * cuando el consumidor destruye el componente con el diálogo todavía abierto
13
+ * (se monta bajo un `@if`).
14
+ */
15
+ @Component({
16
+ template: `
17
+ <button type="button" data-testid="trigger">Abrir</button>
18
+ <app-modal [open]="open()" [title]="title()" (close)="open.set(false)">
19
+ <button type="button" data-testid="body-action">Dentro del diálogo</button>
20
+ <button type="button" data-testid="secondary-action">Cancelar</button>
21
+ </app-modal>
22
+ `,
23
+ imports: [Modal],
24
+ })
25
+ class ModalHost {
26
+ readonly open = signal(false);
27
+ readonly title = signal('Confirmar borrado');
28
+ }
29
+
30
+ describe('Modal — 9.3: foco, Escape y restauración', () => {
31
+ let fixture: ComponentFixture<ModalHost>;
32
+
33
+ beforeEach(async () => {
34
+ await TestBed.configureTestingModule({ imports: [ModalHost] }).compileComponents();
35
+ fixture = TestBed.createComponent(ModalHost);
36
+ fixture.detectChanges();
37
+ });
38
+
39
+ const dialog = (): HTMLElement | null => fixture.nativeElement.querySelector('[role="dialog"]');
40
+ const trigger = (): HTMLButtonElement =>
41
+ fixture.nativeElement.querySelector('[data-testid="trigger"]');
42
+
43
+ const open = async (): Promise<void> => {
44
+ trigger().focus();
45
+ fixture.componentInstance.open.set(true);
46
+ fixture.detectChanges();
47
+ await fixture.whenStable();
48
+ fixture.detectChanges();
49
+ };
50
+
51
+ it('cerrado no hay diálogo; abierto hay uno con nombre y aria-modal', async () => {
52
+ expect(dialog()).toBeNull();
53
+
54
+ await open();
55
+
56
+ const panel = dialog();
57
+ expect(panel).not.toBeNull();
58
+ expect(panel!.getAttribute('aria-modal')).toBe('true');
59
+ const labelledBy = panel!.getAttribute('aria-labelledby');
60
+ expect(document.getElementById(labelledBy!)?.textContent).toContain('Confirmar borrado');
61
+ });
62
+
63
+ it('Escape y el botón ✕ piden cerrar', async () => {
64
+ await open();
65
+
66
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
67
+ fixture.detectChanges();
68
+ expect(fixture.componentInstance.open()).toBe(false);
69
+
70
+ await open();
71
+ (fixture.nativeElement.querySelector('[aria-label="Cerrar"]') as HTMLElement).click();
72
+ fixture.detectChanges();
73
+ expect(fixture.componentInstance.open()).toBe(false);
74
+ });
75
+
76
+ it('el clic en el fondo pide cerrar', async () => {
77
+ await open();
78
+
79
+ (fixture.nativeElement.querySelector('.absolute.inset-0') as HTMLElement).click();
80
+ fixture.detectChanges();
81
+ expect(fixture.componentInstance.open()).toBe(false);
82
+ });
83
+
84
+ it('el foco entra al abrir y vuelve al disparador al cerrar', async () => {
85
+ await open();
86
+ expect(dialog()!.contains(document.activeElement)).toBe(true);
87
+
88
+ fixture.componentInstance.open.set(false);
89
+ fixture.detectChanges();
90
+ await fixture.whenStable();
91
+
92
+ expect(document.activeElement).toBe(trigger());
93
+ });
94
+
95
+ it('destruirse con el diálogo abierto devuelve el foco (no se pierde detrás del fondo)', async () => {
96
+ await open();
97
+ expect(document.activeElement).toBe(dialog());
98
+
99
+ // Es lo que pasa cuando el consumidor lo monta bajo un `@if` y borra el
100
+ // nodo sin que la señal pase por `false`: el effect no llega a correr.
101
+ fixture.componentInstance.open.set(false);
102
+ fixture.destroy();
103
+
104
+ expect(document.activeElement).toBe(trigger());
105
+ });
106
+
107
+ it('el Tab queda atrapado: del último salta al primero', async () => {
108
+ await open();
109
+
110
+ const focusable = Array.from(
111
+ dialog()!.querySelectorAll<HTMLElement>('button, [href], input, select, textarea'),
112
+ );
113
+ expect(focusable.length).toBeGreaterThan(1);
114
+
115
+ const last = focusable[focusable.length - 1];
116
+ last.focus();
117
+ expect(document.activeElement).toBe(last);
118
+
119
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
120
+ expect(document.activeElement).toBe(focusable[0]);
121
+ });
122
+
123
+ it('el Shift+Tab queda atrapado: del primero salta al último', async () => {
124
+ await open();
125
+
126
+ const focusable = Array.from(
127
+ dialog()!.querySelectorAll<HTMLElement>('button, [href], input, select, textarea'),
128
+ );
129
+ expect(focusable.length).toBeGreaterThan(1);
130
+
131
+ // Diagnóstico: la lista que ve el trap debe ser la misma que la del test
132
+ // (si divergen, el selector de `FOCUSABLE` y el del test dejan de
133
+ // describir el mismo conjunto de elementos).
134
+ expect(
135
+ Array.from(dialog()!.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(isVisibleNow),
136
+ 'el selector del trap difiere del del test',
137
+ ).toEqual(focusable);
138
+
139
+ const first = focusable[0];
140
+ first.focus();
141
+ expect(document.activeElement).toBe(first);
142
+
143
+ document.dispatchEvent(
144
+ new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, shiftKey: true }),
145
+ );
146
+
147
+ expect(document.activeElement).toBe(focusable[focusable.length - 1]);
148
+ });
149
+ });
@@ -0,0 +1,145 @@
1
+ import {
2
+ Component,
3
+ ElementRef,
4
+ OnDestroy,
5
+ afterRenderEffect,
6
+ input,
7
+ output,
8
+ viewChild,
9
+ } from '@angular/core';
10
+ import { LucideX } from '@lucide/angular';
11
+
12
+ import { FOCUSABLE, isVisibleNow } from '../../utils/dialog-focus';
13
+
14
+ let titleCounter = 0;
15
+
16
+ /**
17
+ * Diálogo modal accesible: `aria-modal` con nombre por `aria-labelledby`,
18
+ * foco al abrirse y restaurado al cerrarse — también si el consumidor lo
19
+ * destruye con el diálogo todavía abierto — Escape y botón ✕ cierran.
20
+ *
21
+ * ATRAPA el Tab dentro del diálogo: sin eso el foco se tabula hacia la página
22
+ * de atrás y se pierde detrás del fondo oscuro. Mismo contrato que `drawer`,
23
+ * con el que comparte el selector de elementos focables.
24
+ *
25
+ * El clic en el fondo es un atajo para puntero; el teclado usa ✕ o Escape.
26
+ * Componente tonto: `open`/`title` entran por `input()`, el estado vive
27
+ * fuera.
28
+ */
29
+ @Component({
30
+ selector: 'app-modal',
31
+ imports: [LucideX],
32
+ host: {
33
+ '(document:keydown.escape)': 'onEscape()',
34
+ // Angular arma la tecla CON sus modificadores y exige igualdad exacta:
35
+ // sin esta segunda entrada, Shift+Tab llega como `shift.tab`, no casa con
36
+ // `tab` y el foco escapa del diálogo hacia atrás.
37
+ '(document:keydown.tab)': 'onTab($event)',
38
+ '(document:keydown.shift.tab)': 'onTab($event)',
39
+ },
40
+ template: `
41
+ @if (open()) {
42
+ <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
43
+ <div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
44
+ <div
45
+ #dialog
46
+ role="dialog"
47
+ aria-modal="true"
48
+ [attr.aria-labelledby]="titleId"
49
+ tabindex="-1"
50
+ class="relative w-full max-w-lg rounded-xl bg-surface-raised p-6 shadow-xl focus:outline-none"
51
+ >
52
+ <div class="mb-4 flex items-start justify-between gap-4">
53
+ <h2 [id]="titleId" class="font-display text-section font-semibold">
54
+ {{ title() }}
55
+ </h2>
56
+ <button
57
+ type="button"
58
+ class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
59
+ [attr.aria-label]="closeLabel()"
60
+ (click)="close.emit()"
61
+ >
62
+ <svg lucideX size="18" />
63
+ </button>
64
+ </div>
65
+ <ng-content />
66
+ </div>
67
+ </div>
68
+ }
69
+ `,
70
+ })
71
+ export class Modal implements OnDestroy {
72
+ readonly open = input(false);
73
+ readonly title = input.required<string>();
74
+ readonly closeLabel = input('Cerrar');
75
+ readonly close = output<void>();
76
+
77
+ readonly titleId = `modal-title-${++titleCounter}`;
78
+
79
+ private readonly dialogRef = viewChild<ElementRef<HTMLDivElement>>('dialog');
80
+ private lastFocused: HTMLElement | null = null;
81
+
82
+ private readonly focusEffect = afterRenderEffect(() => {
83
+ const open = this.open();
84
+ const dialog = this.dialogRef();
85
+ if (open && dialog) {
86
+ const active = document.activeElement;
87
+ if (active !== dialog.nativeElement && this.lastFocused === null) {
88
+ this.lastFocused = active instanceof HTMLElement ? active : null;
89
+ }
90
+ dialog.nativeElement.focus();
91
+ } else if (!open && this.lastFocused !== null) {
92
+ this.lastFocused.focus();
93
+ this.lastFocused = null;
94
+ }
95
+ });
96
+
97
+ /**
98
+ * El consumidor puede destruir el componente con el diálogo todavía abierto
99
+ * (se monta bajo un `@if`): entonces el effect nunca ve la transición a
100
+ * `false` y el foco se quedaría perdido en el body.
101
+ */
102
+ ngOnDestroy(): void {
103
+ if (this.lastFocused?.isConnected) {
104
+ this.lastFocused.focus();
105
+ }
106
+ this.lastFocused = null;
107
+ }
108
+
109
+ onEscape(): void {
110
+ if (this.open()) {
111
+ this.close.emit();
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Mantiene el Tab dentro del diálogo mientras está abierto. Recibe `Event`
117
+ * porque los host listeners de Angular tipean `$event` así; el teclado se
118
+ * relee acá dentro.
119
+ */
120
+ onTab(rawEvent: Event): void {
121
+ const event = rawEvent as KeyboardEvent;
122
+ const dialog = this.dialogRef()?.nativeElement;
123
+ if (!this.open() || !dialog) {
124
+ return;
125
+ }
126
+ const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
127
+ (element) => isVisibleNow(element),
128
+ );
129
+ if (focusable.length === 0) {
130
+ event.preventDefault();
131
+ dialog.focus();
132
+ return;
133
+ }
134
+ const first = focusable[0];
135
+ const last = focusable[focusable.length - 1];
136
+ const active = document.activeElement;
137
+ if (event.shiftKey && (active === first || active === dialog)) {
138
+ event.preventDefault();
139
+ last.focus();
140
+ } else if (!event.shiftKey && active === last) {
141
+ event.preventDefault();
142
+ first.focus();
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,165 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { By } from '@angular/platform-browser';
3
+ import { describe, expect, it, beforeEach } from 'vitest';
4
+
5
+ import type { PostCreateSchema, ResourceSchema } from '../../../core/models/schema.model';
6
+ import { CopyShare } from '../copy-share/copy-share';
7
+ import { PostCreateModal } from './post-create-modal';
8
+
9
+ describe('PostCreateModal', () => {
10
+ let fixture: ComponentFixture<PostCreateModal>;
11
+
12
+ const mockSchema: ResourceSchema = {
13
+ id: 'orders',
14
+ label: 'Pedido',
15
+ labelPlural: 'Pedidos',
16
+ kind: 'collection',
17
+ endpoint: { list: '/admin/orders' },
18
+ keyField: 'ref',
19
+ titleField: 'customer_name',
20
+ listColumns: [],
21
+ fields: [
22
+ { key: 'ref', type: 'text', label: 'Referencia' },
23
+ { key: 'customer_name', type: 'text', label: 'Cliente' },
24
+ { key: 'pickup_date', type: 'date', label: 'Fecha de Retiro' },
25
+ ],
26
+ };
27
+
28
+ const mockConfig: PostCreateSchema = {
29
+ mode: 'modal',
30
+ title: '¡Pedido {ref} Creado!',
31
+ description: 'El pedido de {customer_name} ya está listo para compartir.',
32
+ summaryFields: ['ref', 'customer_name', 'pickup_date'],
33
+ actions: [
34
+ {
35
+ id: 'share',
36
+ type: 'copy-share',
37
+ label: 'Copiar Enlace de Retiro',
38
+ urlTemplate: '{public_base_url}/p/{ref}',
39
+ shareTextTemplate: 'Hola {customer_name}, tu pedido {ref} está listo.',
40
+ },
41
+ ],
42
+ };
43
+
44
+ beforeEach(async () => {
45
+ await TestBed.configureTestingModule({
46
+ imports: [PostCreateModal],
47
+ }).compileComponents();
48
+ fixture = TestBed.createComponent(PostCreateModal);
49
+ });
50
+
51
+ it('1. interpola título y descripción con los datos del registro', () => {
52
+ fixture.componentRef.setInput('schema', mockSchema);
53
+ fixture.componentRef.setInput('config', mockConfig);
54
+ fixture.componentRef.setInput('record', {
55
+ ref: 'ORD-555',
56
+ customer_name: 'Esteban Quito',
57
+ pickup_date: '2026-10-10',
58
+ });
59
+ fixture.componentRef.setInput('configValues', {
60
+ public_base_url: 'https://pickpass.app',
61
+ });
62
+ fixture.detectChanges();
63
+
64
+ const root = fixture.nativeElement as HTMLElement;
65
+ expect(root.textContent).toContain('¡Pedido ORD-555 Creado!');
66
+ expect(root.textContent).toContain('El pedido de Esteban Quito ya está listo para compartir.');
67
+ });
68
+
69
+ it('2. renderiza los campos de resumen especificados en summaryFields', () => {
70
+ fixture.componentRef.setInput('schema', mockSchema);
71
+ fixture.componentRef.setInput('config', mockConfig);
72
+ fixture.componentRef.setInput('record', {
73
+ ref: 'ORD-555',
74
+ customer_name: 'Esteban Quito',
75
+ pickup_date: '2026-10-10',
76
+ });
77
+ fixture.detectChanges();
78
+
79
+ const root = fixture.nativeElement as HTMLElement;
80
+ expect(root.textContent).toContain('Referencia');
81
+ expect(root.textContent).toContain('ORD-555');
82
+ expect(root.textContent).toContain('Cliente');
83
+ expect(root.textContent).toContain('Esteban Quito');
84
+ expect(root.textContent).toContain('Fecha de Retiro');
85
+ expect(root.textContent).toContain('2026-10-10');
86
+ });
87
+
88
+ it('3. renderiza el componente CopyShare cuando hay una acción copy-share', () => {
89
+ fixture.componentRef.setInput('schema', mockSchema);
90
+ fixture.componentRef.setInput('config', mockConfig);
91
+ fixture.componentRef.setInput('record', {
92
+ ref: 'ORD-888',
93
+ customer_name: 'Lucia',
94
+ });
95
+ fixture.componentRef.setInput('configValues', {
96
+ public_base_url: 'https://retiro.com',
97
+ });
98
+ fixture.detectChanges();
99
+
100
+ const root = fixture.nativeElement as HTMLElement;
101
+ expect(root.querySelector('app-copy-share')).not.toBeNull();
102
+ expect(root.textContent).toContain('Copiar Enlace de Retiro');
103
+ });
104
+
105
+ it('4. emite evento closed al hacer clic en el botón Continuar', () => {
106
+ let closedEmitted = false;
107
+ fixture.componentInstance.closed.subscribe(() => {
108
+ closedEmitted = true;
109
+ });
110
+
111
+ fixture.componentRef.setInput('schema', mockSchema);
112
+ fixture.componentRef.setInput('config', mockConfig);
113
+ fixture.componentRef.setInput('record', { ref: 'ORD-1' });
114
+ fixture.detectChanges();
115
+
116
+ const root = fixture.nativeElement as HTMLElement;
117
+ const btns = Array.from(root.querySelectorAll<HTMLButtonElement>('button[app-button]'));
118
+ const btn = btns.find((b) => b.textContent?.trim() === 'Continuar');
119
+ btn?.click();
120
+ fixture.detectChanges();
121
+
122
+ expect(closedEmitted).toBe(true);
123
+ });
124
+
125
+ it('5. el enlace copiado lleva la URL pública sin que nadie pase configValues', () => {
126
+ // Réplica EXACTA del binding de producción: `form-view.ts:420-425` sólo
127
+ // pasa `schema`, `config`, `record` y `closed`. Nadie bindea
128
+ // `[configValues]`, así que la única fuente posible de
129
+ // `{public_base_url}` es `ConfigService` inyectado en el componente
130
+ // (D18): antes no lo estaba y el enlace salía sin dominio.
131
+ const configConUrl: PostCreateSchema = {
132
+ ...mockConfig,
133
+ actions: [
134
+ {
135
+ id: 'share',
136
+ type: 'copy-share',
137
+ label: 'Copiar Enlace de Retiro',
138
+ urlTemplate: '{public_base_url}/p/{ref}',
139
+ shareTextTemplate:
140
+ 'Hola {customer_name}, puede retirar su pedido {ref} aquí: {public_base_url}/p/{ref}.',
141
+ },
142
+ ],
143
+ };
144
+
145
+ fixture.componentRef.setInput('schema', mockSchema);
146
+ fixture.componentRef.setInput('config', configConUrl);
147
+ fixture.componentRef.setInput('record', { ref: 'ORD-999', customer_name: 'Ana Pérez' });
148
+ fixture.detectChanges();
149
+
150
+ const share = fixture.debugElement.query(By.directive(CopyShare))
151
+ .componentInstance as CopyShare;
152
+
153
+ // Sin `configValues`, el dominio lo aporta `ConfigService` (fallback a
154
+ // `window.location.origin`, config.service.ts:30).
155
+ const esperado = `${window.location.origin}/p/ORD-999`;
156
+ expect(share.url(), 'el enlace quedó sin dominio').toBe(esperado);
157
+ expect(share.shareText(), 'el mensaje quedó con la URL rota').toContain(esperado);
158
+
159
+ // Y el input sigue mandando cuando alguien lo pasa: es un override,
160
+ // no la fuente principal.
161
+ fixture.componentRef.setInput('configValues', { public_base_url: 'https://retiro.com' });
162
+ fixture.detectChanges();
163
+ expect(share.url(), 'el override dejó de aplicar').toBe('https://retiro.com/p/ORD-999');
164
+ });
165
+ });
@@ -0,0 +1,159 @@
1
+ import { Component, computed, inject, input, output } from '@angular/core';
2
+ import { LucideCheckCircle2 } from '@lucide/angular';
3
+
4
+ import type {
5
+ CopyShareAction,
6
+ PostCreateSchema,
7
+ ResourceAction,
8
+ ResourceSchema,
9
+ } from '../../../core/models/schema.model';
10
+ import { isShareAction, resolveShare, type ResolvedShare } from '../../../actions/action-registry';
11
+ import { ConfigService } from '../../../core/services/config.service';
12
+ import { interpolateTemplate } from '../../../core/utils/template-interpolator';
13
+ import { Button } from '../button/button';
14
+ import { CopyShare } from '../copy-share/copy-share';
15
+ import { Modal } from '../modal/modal';
16
+
17
+ interface SummaryItem {
18
+ key: string;
19
+ label: string;
20
+ value: string;
21
+ }
22
+
23
+ /**
24
+ * Modal de éxito y resumen tras crear un registro.
25
+ * Permite visualizar datos clave y acciones directas (como CopyShare o compartir por WhatsApp).
26
+ */
27
+ @Component({
28
+ selector: 'app-post-create-modal',
29
+ imports: [Modal, Button, CopyShare, LucideCheckCircle2],
30
+ template: `
31
+ <app-modal [open]="true" [title]="title()" (close)="closed.emit()">
32
+ <div class="flex flex-col gap-4">
33
+ <!-- Encabezado con icono de éxito -->
34
+ <div class="flex items-center gap-3">
35
+ <div
36
+ class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500"
37
+ >
38
+ <svg lucideCheckCircle2 size="22" />
39
+ </div>
40
+ <div>
41
+ <p class="text-sm text-neutral-light">{{ description() }}</p>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Tarjeta de campos de resumen -->
46
+ @if (summaryItems().length > 0) {
47
+ <div class="rounded-lg border border-neutral/15 bg-surface p-3.5">
48
+ <dl class="grid grid-cols-1 gap-x-4 gap-y-2.5 sm:grid-cols-2">
49
+ @for (item of summaryItems(); track item.key) {
50
+ <div class="flex flex-col">
51
+ <dt class="text-xs font-medium text-neutral">{{ item.label }}</dt>
52
+ <dd class="text-sm font-semibold text-neutral-light truncate">
53
+ {{ item.value }}
54
+ </dd>
55
+ </div>
56
+ }
57
+ </dl>
58
+ </div>
59
+ }
60
+
61
+ <!-- Acciones declaradas (ej. CopyShare) -->
62
+ @if (copyShareAction(); as action) {
63
+ <div class="rounded-lg border border-primary/20 bg-primary/5 p-3.5">
64
+ <p class="mb-2 text-xs font-medium text-primary">Enlace de Seguimiento</p>
65
+ @let share = shareOf(action);
66
+ <app-copy-share
67
+ [url]="share.url"
68
+ [shareText]="share.shareText"
69
+ [label]="action.label"
70
+ />
71
+ </div>
72
+ }
73
+
74
+ <!-- Botón de cierre primario -->
75
+ <div class="mt-2 flex justify-end">
76
+ <button app-button type="button" variant="primary" (click)="closed.emit()">
77
+ Continuar
78
+ </button>
79
+ </div>
80
+ </div>
81
+ </app-modal>
82
+ `,
83
+ })
84
+ export class PostCreateModal {
85
+ readonly schema = input.required<ResourceSchema>();
86
+ readonly config = input.required<PostCreateSchema>();
87
+ readonly record = input.required<Record<string, unknown>>();
88
+
89
+ /**
90
+ * Override puntual del contexto de interpolación (tests, o quien quiera
91
+ * forzar un valor). **No** es la vía normal: la config global la aporta
92
+ * `ConfigService`, igual que en `list-view`.
93
+ */
94
+ readonly configValues = input<Record<string, unknown>>({});
95
+
96
+ readonly closed = output<void>();
97
+
98
+ /**
99
+ * Inyectado y NO opcional a propósito. El bug que motivó esto (D18): con
100
+ * el input `configValues` como única fuente y nadie bindeándolo, la
101
+ * plantilla salía con `{public_base_url}` vacío y el operador copiaba
102
+ * `…aquí: /p/ORD-0001.` — un enlace inpegable.
103
+ *
104
+ * `buildInterpolationContext` mezcla config global + fila y fija
105
+ * `public_base_url` (`config.service.ts:64-73`); `configValues` se aplica
106
+ * encima, sólo como override.
107
+ */
108
+ private readonly configService = inject(ConfigService);
109
+
110
+ constructor() {
111
+ // El modal se abre con la sesión ya iniciada: si el load del bootstrap
112
+ // falló (arranque sin sesión), éste es el momento de reintentarlo para
113
+ // que el mensaje de compartir salga del singleton y no «sólo la URL».
114
+ // `globalConfig` es signal: cuando llegue, el contexto y `shareOf` se
115
+ // recalculan solos.
116
+ void this.configService.ensureLoaded();
117
+ }
118
+
119
+ /** Contexto único con el que se resuelven título, descripción, URL y mensaje. */
120
+ private readonly context = computed(() => ({
121
+ ...this.configService.buildInterpolationContext(this.record()),
122
+ ...this.configValues(),
123
+ }));
124
+
125
+ protected readonly title = computed(() => {
126
+ const raw = this.config().title || '¡Registro Creado con Éxito!';
127
+ return interpolateTemplate(raw, this.context());
128
+ });
129
+
130
+ protected readonly description = computed(() => {
131
+ const raw =
132
+ this.config().description ||
133
+ 'El registro fue creado correctamente y ya se encuentra guardado en el sistema.';
134
+ return interpolateTemplate(raw, this.context());
135
+ });
136
+
137
+ protected readonly summaryItems = computed<SummaryItem[]>(() => {
138
+ const fields = this.config().summaryFields || [];
139
+ const rec = this.record();
140
+ const schemaFields = this.schema().fields || [];
141
+
142
+ return fields.map((key) => {
143
+ const fieldDef = schemaFields.find((f) => f.key === key);
144
+ const label = fieldDef?.label || key.replace(/_/g, ' ');
145
+ const rawVal = rec[key];
146
+ const value = rawVal === null || rawVal === undefined || rawVal === '' ? '—' : String(rawVal);
147
+ return { key, label, value };
148
+ });
149
+ });
150
+
151
+ protected readonly copyShareAction = computed<CopyShareAction | null>(
152
+ () => (this.config().actions || []).find(isShareAction) ?? null,
153
+ );
154
+
155
+ /** Enlace y mensaje, resueltos por `resolveShare` (R3.10: una sola copia). */
156
+ protected shareOf(action: CopyShareAction): ResolvedShare {
157
+ return resolveShare(action, this.context());
158
+ }
159
+ }
@@ -0,0 +1,31 @@
1
+ import { Component, computed, input } from '@angular/core';
2
+
3
+ /**
4
+ * Bloque de carga decorativo: oculto para lectores (`aria-hidden`); la
5
+ * región que lo contiene debe exponer `aria-busy="true"`.
6
+ */
7
+ @Component({
8
+ selector: 'app-skeleton',
9
+ host: { class: 'block space-y-2', 'aria-hidden': 'true' },
10
+ template: `
11
+ @for (line of lineCount(); track line) {
12
+ <div
13
+ class="animate-pulse rounded bg-neutral/15"
14
+ [style.height.px]="height()"
15
+ [style.width]="width()"
16
+ ></div>
17
+ }
18
+ `,
19
+ })
20
+ export class Skeleton {
21
+ /** Cantidad de barras (mínimo 1). */
22
+ readonly lines = input(1);
23
+ readonly height = input(16);
24
+ readonly width = input('100%');
25
+
26
+ readonly lineCount = computed(() => {
27
+ const lines = Math.floor(this.lines());
28
+ const count = Number.isFinite(lines) && lines > 0 ? lines : 1;
29
+ return Array.from({ length: count }, (_, index) => index);
30
+ });
31
+ }