@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,48 @@
1
+ import { Component, input, output } from '@angular/core';
2
+
3
+ import { Button } from '../button/button';
4
+ import { Modal } from '../modal/modal';
5
+
6
+ /**
7
+ * Opciones del diálogo. Es estructuralmente compatible con
8
+ * `core.NotificationService['confirmation']` (mismos campos) — `shared` no
9
+ * importa `core`, quien une ambos lados es el shell (Fase 7).
10
+ */
11
+ export interface ConfirmDialogOptions {
12
+ title: string;
13
+ message: string;
14
+ confirmLabel?: string;
15
+ cancelLabel?: string;
16
+ }
17
+
18
+ @Component({
19
+ selector: 'app-confirm-dialog',
20
+ imports: [Modal, Button],
21
+ template: `
22
+ @if (options(); as opts) {
23
+ <app-modal [open]="true" [title]="opts.title" (close)="cancelled.emit()">
24
+ <p class="text-sm text-neutral">{{ opts.message }}</p>
25
+ <div class="mt-6 flex justify-end gap-3">
26
+ <button app-button variant="outline" type="button" (click)="cancelled.emit()">
27
+ {{ opts.cancelLabel ?? 'Cancel' }}
28
+ </button>
29
+ <button
30
+ app-button
31
+ type="button"
32
+ [variant]="danger() ? 'danger' : 'primary'"
33
+ (click)="confirmed.emit()"
34
+ >
35
+ {{ opts.confirmLabel ?? 'Confirm' }}
36
+ </button>
37
+ </div>
38
+ </app-modal>
39
+ }
40
+ `,
41
+ })
42
+ export class ConfirmDialog {
43
+ /** `null` = oculto (viene de `NotificationService.confirmation`). */
44
+ readonly options = input<ConfirmDialogOptions | null>(null);
45
+ readonly danger = input(false);
46
+ readonly confirmed = output<void>();
47
+ readonly cancelled = output<void>();
48
+ }
@@ -0,0 +1,56 @@
1
+ import { Component, inject, input, signal } from '@angular/core';
2
+ import { LucideCopy, LucideCheck } from '@lucide/angular';
3
+
4
+ import { writeClipboard } from '../../../core/utils/clipboard';
5
+ import { NotificationService } from '../../../core/services/notification.service';
6
+ import { Button } from '../button/button';
7
+
8
+ /**
9
+ * Botón para copiar un valor al portapapeles.
10
+ *
11
+ * Muestra un ícono de copiar que se reemplaza por una marca de éxito tras
12
+ * copiar. Usa el mismo canal de portapapeles que `CopyShare` para consistencia.
13
+ */
14
+ @Component({
15
+ selector: 'app-copy-button',
16
+ imports: [Button, LucideCopy, LucideCheck],
17
+ template: `
18
+ <button
19
+ app-button
20
+ type="button"
21
+ variant="ghost"
22
+ size="icon"
23
+ [disabled]="copied()"
24
+ (click)="copy()"
25
+ [title]="'Copiar ' + value()"
26
+ class="text-neutral hover:text-primary"
27
+ >
28
+ @if (copied()) {
29
+ <svg lucideCheck size="16" class="text-primary" />
30
+ } @else {
31
+ <svg lucideCopy size="16" />
32
+ }
33
+ </button>
34
+ `,
35
+ })
36
+ export class CopyButton {
37
+ /** Valor a copiar al portapapeles. */
38
+ readonly value = input.required<unknown>();
39
+
40
+ private readonly notify = inject(NotificationService);
41
+ private readonly copied = signal(false);
42
+
43
+ protected readonly copied$ = this.copied.asReadonly();
44
+
45
+ protected async copy(): Promise<void> {
46
+ const text = String(this.value() ?? '');
47
+ const ok = await writeClipboard(text);
48
+ if (ok) {
49
+ this.copied.set(true);
50
+ this.notify.success('Copiado al portapapeles.');
51
+ setTimeout(() => this.copied.set(false), 1500);
52
+ } else {
53
+ this.notify.error('No se pudo copiar.');
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,122 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+ import { vi } from 'vitest';
3
+
4
+ import { NotificationService } from '../../../core/services/notification.service';
5
+ import { CopyShare } from './copy-share';
6
+
7
+ const URL_UNDER_TEST = 'https://ejemplo.test/p/abc123';
8
+
9
+ /**
10
+ * `navigator.clipboard` / `navigator.share` no existen en el DOM de prueba y
11
+ * son propiedades del prototipo en los navegadores reales: se tapan con una
12
+ * propiedad propia configurable y se restaura el descriptor original.
13
+ */
14
+ const OWN = new Map<string, PropertyDescriptor | undefined>();
15
+
16
+ function stubNavigator(key: 'clipboard' | 'share', value: unknown): void {
17
+ if (!OWN.has(key)) {
18
+ OWN.set(key, Object.getOwnPropertyDescriptor(navigator, key));
19
+ }
20
+ Object.defineProperty(navigator, key, { value, configurable: true, writable: true });
21
+ }
22
+
23
+ function clearNavigator(key: 'clipboard' | 'share'): void {
24
+ if (!OWN.has(key)) {
25
+ OWN.set(key, Object.getOwnPropertyDescriptor(navigator, key));
26
+ }
27
+ Object.defineProperty(navigator, key, { value: undefined, configurable: true, writable: true });
28
+ }
29
+
30
+ function restoreNavigator(): void {
31
+ for (const [key, descriptor] of OWN) {
32
+ if (descriptor) {
33
+ Object.defineProperty(navigator, key, descriptor);
34
+ } else {
35
+ delete (navigator as unknown as Record<string, unknown>)[key];
36
+ }
37
+ }
38
+ OWN.clear();
39
+ }
40
+
41
+ describe('CopyShare', () => {
42
+ let fixture: ComponentFixture<CopyShare>;
43
+ let notifications: NotificationService;
44
+
45
+ beforeEach(async () => {
46
+ await TestBed.configureTestingModule({ imports: [CopyShare] }).compileComponents();
47
+ fixture = TestBed.createComponent(CopyShare);
48
+ // `canShare()` es un computed SIN dependencias reactivas: se cachea en el
49
+ // primer render. Los tests tapan `navigator` ANTES del primer detectChanges.
50
+ fixture.componentRef.setInput('url', URL_UNDER_TEST);
51
+ notifications = TestBed.inject(NotificationService);
52
+ });
53
+
54
+ afterEach(() => restoreNavigator());
55
+
56
+ const root = (): HTMLElement => fixture.nativeElement as HTMLElement;
57
+
58
+ const button = (text: string): HTMLButtonElement =>
59
+ [...root().querySelectorAll<HTMLButtonElement>('button')].find((el) =>
60
+ el.textContent.trim().startsWith(text),
61
+ )!;
62
+
63
+ const messages = (): string[] => notifications.toasts().map((toast) => toast.message);
64
+
65
+ it('copia el enlace al portapapeles y dispara la notificación de éxito', async () => {
66
+ const writeText = vi.fn().mockResolvedValue(undefined);
67
+ stubNavigator('clipboard', { writeText });
68
+ fixture.detectChanges();
69
+
70
+ button('Copiar enlace').click();
71
+ await fixture.whenStable();
72
+
73
+ expect(writeText).toHaveBeenCalledWith(URL_UNDER_TEST);
74
+ expect(messages()).toContain('Mensaje copiado.');
75
+ });
76
+
77
+ it('copia el MENSAJE formateado completo cuando hay shareText', async () => {
78
+ const writeText = vi.fn().mockResolvedValue(undefined);
79
+ stubNavigator('clipboard', { writeText });
80
+ const message = `Hola Ana, puede retirar su pedido A-100 aquí: ${URL_UNDER_TEST}`;
81
+ fixture.componentRef.setInput('shareText', message);
82
+ fixture.detectChanges();
83
+
84
+ button('Copiar enlace').click();
85
+ await fixture.whenStable();
86
+
87
+ // El bug: sólo se escribía la URL y el saludo/referencia/PIN se perdían.
88
+ expect(writeText).toHaveBeenCalledWith(message);
89
+ expect(writeText).not.toHaveBeenCalledWith(URL_UNDER_TEST);
90
+ });
91
+
92
+ it('sin clipboard no revienta: cae al fallback y avisa si tampoco funciona', async () => {
93
+ clearNavigator('clipboard');
94
+ fixture.detectChanges();
95
+
96
+ expect(() => button('Copiar enlace').click()).not.toThrow();
97
+ await fixture.whenStable();
98
+
99
+ expect(messages()).toContain('No se pudo copiar el mensaje.');
100
+ });
101
+
102
+ it('sin navigator.share no se muestra la acción de compartir nativa', () => {
103
+ clearNavigator('share');
104
+ fixture.detectChanges();
105
+
106
+ const labels = [...root().querySelectorAll('button')].map((el) => el.textContent.trim());
107
+ expect(labels).toContain('WhatsApp');
108
+ expect(labels).not.toContain('Compartir');
109
+ });
110
+
111
+ it('con navigator.share se ofrece Compartir y delega en la API nativa', async () => {
112
+ const share = vi.fn().mockResolvedValue(undefined);
113
+ stubNavigator('share', share);
114
+ fixture.detectChanges();
115
+
116
+ button('Compartir').click();
117
+ await fixture.whenStable();
118
+
119
+ expect(share).toHaveBeenCalledWith({ url: URL_UNDER_TEST, text: '' });
120
+ expect(button('WhatsApp')).toBeUndefined();
121
+ });
122
+ });
@@ -0,0 +1,107 @@
1
+ import { Component, computed, inject, input } from '@angular/core';
2
+
3
+ import { LucideCopy, LucideShare } from '@lucide/angular';
4
+
5
+ import { buildShareMessage, writeClipboard } from '../../../core/utils/clipboard';
6
+ import { NotificationService } from '../../../core/services/notification.service';
7
+ import { Button } from '../button/button';
8
+
9
+ /**
10
+ * Copia el enlace de un registro al portapapeles y ofrece compartirlo.
11
+ *
12
+ * Es lo que hace útil `public_base_url` (Etapa 2, S5): con este botón el
13
+ * operador entrega el enlace público del pedido.
14
+ *
15
+ * Tres caminos, todos degradando en vez de romper:
16
+ *
17
+ * 1. **Copiar** — `navigator.clipboard.writeText`; si el navegador no lo
18
+ * tiene (contexto no seguro) cae a un `textarea` + `execCommand('copy')`.
19
+ * 2. **Compartir** — `navigator.share`, sólo renderizado si existe
20
+ * (`'share' in navigator`; no está en desktop).
21
+ * 3. **WhatsApp** — el reemplazo del 2 cuando no hay `navigator.share`:
22
+ * `wa.me/?text=` con `shareText` + `url`.
23
+ *
24
+ * `navigator.share` se llama con `{ url, text }`: no se envía `title` porque
25
+ * no hay input dedicado y `label` es el rótulo del botón de copiar, no un
26
+ * título de compartir.
27
+ */
28
+ @Component({
29
+ selector: 'app-copy-share',
30
+ imports: [Button, LucideCopy, LucideShare],
31
+ template: `
32
+ <div class="flex flex-wrap items-center gap-2">
33
+ <button app-button type="button" variant="outline" (click)="copy()">
34
+ <svg lucideCopy size="16" />
35
+ {{ label() }}
36
+ </button>
37
+
38
+ @if (canShare()) {
39
+ <button app-button type="button" variant="ghost" (click)="share()">
40
+ <svg lucideShare size="16" />
41
+ Compartir
42
+ </button>
43
+ } @else {
44
+ <button app-button type="button" variant="ghost" (click)="shareWhatsApp()">
45
+ <svg lucideShare size="16" />
46
+ WhatsApp
47
+ </button>
48
+ }
49
+ </div>
50
+ `,
51
+ })
52
+ export class CopyShare {
53
+ /** Enlace público a copiar/compartir. */
54
+ readonly url = input.required<string>();
55
+ /** Rótulo del botón de copiar. */
56
+ readonly label = input('Copiar enlace');
57
+ /** Texto que viaja junto al enlace al compartir (nativo o WhatsApp). */
58
+ readonly shareText = input('');
59
+
60
+ private readonly notify = inject(NotificationService);
61
+
62
+ /**
63
+ * El share nativo no existe en desktop: sin él se ofrece WhatsApp.
64
+ *
65
+ * Se comprueba `typeof … === 'function'` y no `'share' in navigator` como
66
+ * dice el plan: el `in` da `true` también para una propiedad propia con
67
+ * valor `undefined` (polyfills y stubs hacen exactamente eso), y eso
68
+ * montaría un botón que revienta al pulsarlo.
69
+ */
70
+ protected readonly canShare = computed(
71
+ () => typeof navigator !== 'undefined' && typeof navigator.share === 'function',
72
+ );
73
+
74
+ protected readonly waUrl = computed(() => {
75
+ // Mismo texto que copia el botón: `buildShareMessage` evita pegar el
76
+ // enlace DOS veces cuando el `shareTextTemplate` ya lo trae escrito.
77
+ const text = buildShareMessage(this.url(), this.shareText());
78
+ return `https://wa.me/?text=${encodeURIComponent(text)}`;
79
+ });
80
+
81
+ protected async copy(): Promise<void> {
82
+ // El mensaje formateado completo (saludo + ref + enlace + PIN), no sólo
83
+ // la URL: es lo que el operador pega en la conversación con el cliente.
84
+ const copied = await writeClipboard(buildShareMessage(this.url(), this.shareText()));
85
+ if (copied) {
86
+ this.notify.success('Mensaje copiado.');
87
+ } else {
88
+ this.notify.error('No se pudo copiar el mensaje.');
89
+ }
90
+ }
91
+
92
+ protected async share(): Promise<void> {
93
+ if (!this.canShare()) {
94
+ this.shareWhatsApp();
95
+ return;
96
+ }
97
+ try {
98
+ await navigator.share({ url: this.url(), text: this.shareText() });
99
+ } catch {
100
+ // Cancelar la hoja de compartir dispara AbortError: no es un error.
101
+ }
102
+ }
103
+
104
+ protected shareWhatsApp(): void {
105
+ window.open(this.waUrl(), '_blank', 'noopener,noreferrer');
106
+ }
107
+ }
@@ -0,0 +1,127 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
3
+ import { vi } from 'vitest';
4
+
5
+ import { Drawer } from './drawer';
6
+
7
+ /** Disparador real: el foco tiene que volver a él al cerrar. */
8
+ @Component({
9
+ template: `
10
+ <button type="button" data-testid="trigger">Abrir</button>
11
+ <app-drawer [open]="open()" [title]="title()" (close)="open.set(false)">
12
+ <button type="button" data-testid="body-action">Dentro del cuerpo</button>
13
+ <div drawer-actions>
14
+ <button type="button" data-testid="footer-action">Guardar</button>
15
+ </div>
16
+ </app-drawer>
17
+ `,
18
+ imports: [Drawer],
19
+ })
20
+ class DrawerHost {
21
+ readonly open = signal(false);
22
+ readonly title = signal('Panel de prueba');
23
+ }
24
+
25
+ describe('Drawer', () => {
26
+ let fixture: ComponentFixture<DrawerHost>;
27
+
28
+ beforeEach(async () => {
29
+ await TestBed.configureTestingModule({ imports: [DrawerHost] }).compileComponents();
30
+ fixture = TestBed.createComponent(DrawerHost);
31
+ fixture.detectChanges();
32
+ });
33
+
34
+ const dialog = (): HTMLElement | null => fixture.nativeElement.querySelector('[role="dialog"]');
35
+ const trigger = (): HTMLButtonElement =>
36
+ fixture.nativeElement.querySelector('[data-testid="trigger"]');
37
+
38
+ const open = async (): Promise<void> => {
39
+ trigger().focus();
40
+ fixture.componentInstance.open.set(true);
41
+ fixture.detectChanges();
42
+ await fixture.whenStable();
43
+ fixture.detectChanges();
44
+ };
45
+
46
+ it('cerrado no hay diálogo; abierto hay uno con nombre y aria-modal', async () => {
47
+ expect(dialog()).toBeNull();
48
+
49
+ await open();
50
+
51
+ const panel = dialog();
52
+ expect(panel).not.toBeNull();
53
+ expect(panel!.getAttribute('aria-modal')).toBe('true');
54
+ const labelledBy = panel!.getAttribute('aria-labelledby');
55
+ expect(document.getElementById(labelledBy!)?.textContent).toContain('Panel de prueba');
56
+ });
57
+
58
+ it('Escape y el clic en el fondo piden cerrar', async () => {
59
+ await open();
60
+
61
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
62
+ fixture.detectChanges();
63
+ expect(fixture.componentInstance.open()).toBe(false);
64
+
65
+ await open();
66
+ (fixture.nativeElement.querySelector('.absolute.inset-0') as HTMLElement).click();
67
+ fixture.detectChanges();
68
+ expect(fixture.componentInstance.open()).toBe(false);
69
+ });
70
+
71
+ it('el foco entra al abrir y vuelve al disparador al cerrar', async () => {
72
+ await open();
73
+ expect(dialog()!.contains(document.activeElement)).toBe(true);
74
+
75
+ fixture.componentInstance.open.set(false);
76
+ fixture.detectChanges();
77
+ await fixture.whenStable();
78
+
79
+ expect(document.activeElement).toBe(trigger());
80
+ });
81
+
82
+ it('destruirse con el panel abierto devuelve el foco (no se pierde detrás del fondo)', async () => {
83
+ await open();
84
+ expect(dialog()!.contains(document.activeElement)).toBe(true);
85
+
86
+ // Es lo que pasa cuando el consumidor lo monta bajo un @if y borra el
87
+ // nodo sin que la señal pase por `false`.
88
+ fixture.componentInstance.open.set(false);
89
+ fixture.destroy();
90
+
91
+ expect(document.activeElement).toBe(trigger());
92
+ });
93
+
94
+ it('el Tab queda atrapado: del último salta al primero', async () => {
95
+ await open();
96
+
97
+ const focusable = Array.from(
98
+ dialog()!.querySelectorAll<HTMLElement>('button, [href], input, select, textarea'),
99
+ );
100
+ const last = focusable[focusable.length - 1];
101
+ last.focus();
102
+ expect(document.activeElement).toBe(last);
103
+
104
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
105
+ expect(document.activeElement).toBe(focusable[0]);
106
+ });
107
+
108
+ it('el Shift+Tab queda atrapado: del primero salta al último', async () => {
109
+ await open();
110
+
111
+ const focusable = Array.from(
112
+ dialog()!.querySelectorAll<HTMLElement>('button, [href], input, select, textarea'),
113
+ );
114
+ expect(focusable.length).toBeGreaterThan(1);
115
+
116
+ // Angular iguala `fullKey` contra la tecla armada con los modificadores:
117
+ // sin el binding `keydown.shift.tab` este evento no llega al trap.
118
+ const first = focusable[0];
119
+ first.focus();
120
+ expect(document.activeElement).toBe(first);
121
+
122
+ document.dispatchEvent(
123
+ new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, shiftKey: true }),
124
+ );
125
+ expect(document.activeElement).toBe(focusable[focusable.length - 1]);
126
+ });
127
+ });
@@ -0,0 +1,172 @@
1
+ import {
2
+ Component,
3
+ ElementRef,
4
+ OnDestroy,
5
+ afterRenderEffect,
6
+ computed,
7
+ input,
8
+ output,
9
+ viewChild,
10
+ } from '@angular/core';
11
+ import { LucideX } from '@lucide/angular';
12
+
13
+ import { FOCUSABLE, isVisibleNow } from '../../utils/dialog-focus';
14
+
15
+ /** Ancho del panel: `sm` para 1–3 campos, `md`/`lg` para formularios. */
16
+ export type DrawerSize = 'sm' | 'md' | 'lg';
17
+
18
+ const WIDTHS: Record<DrawerSize, string> = {
19
+ sm: 'max-w-sm',
20
+ md: 'max-w-lg',
21
+ lg: 'max-w-2xl',
22
+ };
23
+
24
+ let titleCounter = 0;
25
+
26
+ /**
27
+ * Panel lateral (drawer) — guía §5: «edición rápida de pocos campos: panel
28
+ * lateral en lugar de pantalla nueva, para no perder contexto».
29
+ *
30
+ * Sigue el mismo contrato que `modal`: `role="dialog"` con nombre por
31
+ * `aria-labelledby`, foco al abrirse y restaurado al cerrarse, Escape y el
32
+ * fondo cierran. Además ATRAPA el Tab dentro del panel: sin eso un modal se
33
+ * puede tabular hacia la página de atrás y el foco se pierde detrás del
34
+ * fondo oscuro.
35
+ *
36
+ * Componente tonto: `open`/`title` entran por `input()` y el estado vive
37
+ * fuera. El cuerpo se proyecta en el área con scroll y el pie con el
38
+ * atributo `drawer-actions`.
39
+ */
40
+ @Component({
41
+ selector: 'app-drawer',
42
+ imports: [LucideX],
43
+ host: {
44
+ '(document:keydown.escape)': 'onEscape()',
45
+ // Angular arma la tecla CON sus modificadores y exige igualdad exacta:
46
+ // sin esta segunda entrada, Shift+Tab llega como `shift.tab`, no casa con
47
+ // `tab` y el foco escapa del panel hacia atrás.
48
+ '(document:keydown.tab)': 'onTab($event)',
49
+ '(document:keydown.shift.tab)': 'onTab($event)',
50
+ },
51
+ template: `
52
+ @if (open()) {
53
+ <div class="fixed inset-0 z-50">
54
+ <div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
55
+ <section
56
+ #dialog
57
+ role="dialog"
58
+ aria-modal="true"
59
+ [attr.aria-labelledby]="titleId"
60
+ tabindex="-1"
61
+ class="absolute inset-y-0 right-0 flex w-full flex-col bg-surface-raised shadow-2xl focus:outline-none"
62
+ [class]="widthClass()"
63
+ >
64
+ <header
65
+ class="flex items-start justify-between gap-4 border-b border-neutral/20 px-5 py-4"
66
+ >
67
+ <div>
68
+ <h2 [id]="titleId" class="font-display text-section font-semibold">{{ title() }}</h2>
69
+ @if (subtitle(); as subtitleText) {
70
+ <p class="mt-1 text-sm text-neutral/70">{{ subtitleText }}</p>
71
+ }
72
+ </div>
73
+ <button
74
+ type="button"
75
+ 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"
76
+ [attr.aria-label]="closeLabel()"
77
+ (click)="close.emit()"
78
+ >
79
+ <svg lucideX size="18" />
80
+ </button>
81
+ </header>
82
+
83
+ <div class="flex-1 overflow-y-auto px-5 py-4">
84
+ <ng-content />
85
+ </div>
86
+
87
+ <div class="border-t border-neutral/20 px-5 py-4">
88
+ <ng-content select="[drawer-actions]" />
89
+ </div>
90
+ </section>
91
+ </div>
92
+ }
93
+ `,
94
+ })
95
+ export class Drawer implements OnDestroy {
96
+ readonly open = input(false);
97
+ readonly title = input('Editar');
98
+ readonly subtitle = input<string | null>(null);
99
+ readonly size = input<DrawerSize>('md');
100
+ readonly closeLabel = input('Cerrar');
101
+ readonly close = output<void>();
102
+
103
+ readonly titleId = `drawer-title-${++titleCounter}`;
104
+ readonly widthClass = computed(() => WIDTHS[this.size()]);
105
+
106
+ private readonly dialogRef = viewChild<ElementRef<HTMLElement>>('dialog');
107
+ private lastFocused: HTMLElement | null = null;
108
+
109
+ private readonly focusEffect = afterRenderEffect(() => {
110
+ const open = this.open();
111
+ const dialog = this.dialogRef();
112
+ if (open && dialog) {
113
+ const active = document.activeElement;
114
+ if (active !== dialog.nativeElement && this.lastFocused === null) {
115
+ this.lastFocused = active instanceof HTMLElement ? active : null;
116
+ }
117
+ dialog.nativeElement.focus();
118
+ } else if (!open && this.lastFocused !== null) {
119
+ this.lastFocused.focus();
120
+ this.lastFocused = null;
121
+ }
122
+ });
123
+
124
+ /**
125
+ * El consumidor puede destruir el componente con el panel todavía abierto
126
+ * (se monta bajo un `@if`): en ese caso el effect nunca ve la transición a
127
+ * `false` y el foco se quedaría perdido en el body.
128
+ */
129
+ ngOnDestroy(): void {
130
+ if (this.lastFocused?.isConnected) {
131
+ this.lastFocused.focus();
132
+ }
133
+ this.lastFocused = null;
134
+ }
135
+
136
+ onEscape(): void {
137
+ if (this.open()) {
138
+ this.close.emit();
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Mantiene el Tab dentro del panel mientras el diálogo está abierto.
144
+ * Recibe `Event` porque los host listeners de Angular tipean `$event` así;
145
+ * el teclado se relee acá dentro.
146
+ */
147
+ onTab(rawEvent: Event): void {
148
+ const event = rawEvent as KeyboardEvent;
149
+ const dialog = this.dialogRef()?.nativeElement;
150
+ if (!this.open() || !dialog) {
151
+ return;
152
+ }
153
+ const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
154
+ (element) => isVisibleNow(element),
155
+ );
156
+ if (focusable.length === 0) {
157
+ event.preventDefault();
158
+ dialog.focus();
159
+ return;
160
+ }
161
+ const first = focusable[0];
162
+ const last = focusable[focusable.length - 1];
163
+ const active = document.activeElement;
164
+ if (event.shiftKey && (active === first || active === dialog)) {
165
+ event.preventDefault();
166
+ last.focus();
167
+ } else if (!event.shiftKey && active === last) {
168
+ event.preventDefault();
169
+ first.focus();
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,30 @@
1
+ import { Component, input } from '@angular/core';
2
+ import { LucideInbox } from '@lucide/angular';
3
+
4
+ /**
5
+ * Estado vacío universal (structure.md §6). El contenido proyectado es la
6
+ * acción opcional (normalmente un `button[app-button]`).
7
+ */
8
+ @Component({
9
+ selector: 'app-empty-state',
10
+ imports: [LucideInbox],
11
+ host: { role: 'status' },
12
+ template: `
13
+ <div
14
+ class="flex flex-col items-center gap-2 rounded-xl border border-dashed border-neutral/30 px-6 py-12 text-center"
15
+ >
16
+ <svg lucideInbox size="32" class="text-neutral" />
17
+ <h2 class="font-display text-base font-semibold">{{ title() }}</h2>
18
+ @if (message()) {
19
+ <p class="text-sm text-neutral">{{ message() }}</p>
20
+ }
21
+ <div class="mt-2">
22
+ <ng-content />
23
+ </div>
24
+ </div>
25
+ `,
26
+ })
27
+ export class EmptyState {
28
+ readonly title = input.required<string>();
29
+ readonly message = input('');
30
+ }