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