@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,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
+ }