@ng-cn/core 1.0.16 → 1.0.18

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 (262) hide show
  1. package/package.json +6 -5
  2. package/src/app/lib/components/ui/accordion/accordion-content.component.ts +11 -14
  3. package/src/app/lib/components/ui/accordion/accordion-context.ts +1 -0
  4. package/src/app/lib/components/ui/accordion/accordion-item.component.ts +8 -0
  5. package/src/app/lib/components/ui/accordion/accordion-trigger.component.ts +5 -1
  6. package/src/app/lib/components/ui/accordion/accordion.component.ts +24 -6
  7. package/src/app/lib/components/ui/alert/alert-variants.ts +18 -4
  8. package/src/app/lib/components/ui/alert-dialog/alert-dialog-action.component.ts +1 -0
  9. package/src/app/lib/components/ui/alert-dialog/alert-dialog-cancel.component.ts +1 -1
  10. package/src/app/lib/components/ui/alert-dialog/alert-dialog-content.component.ts +34 -17
  11. package/src/app/lib/components/ui/alert-dialog/alert-dialog-description.component.ts +1 -0
  12. package/src/app/lib/components/ui/alert-dialog/alert-dialog-footer.component.ts +1 -0
  13. package/src/app/lib/components/ui/alert-dialog/alert-dialog-header.component.ts +1 -0
  14. package/src/app/lib/components/ui/alert-dialog/alert-dialog-title.component.ts +1 -0
  15. package/src/app/lib/components/ui/alert-dialog/alert-dialog-trigger.component.ts +1 -0
  16. package/src/app/lib/components/ui/alert-dialog/alert-dialog.component.ts +4 -0
  17. package/src/app/lib/components/ui/aspect-ratio/aspect-ratio.component.ts +1 -0
  18. package/src/app/lib/components/ui/avatar/avatar-context.ts +9 -0
  19. package/src/app/lib/components/ui/avatar/avatar-fallback.component.ts +6 -9
  20. package/src/app/lib/components/ui/avatar/avatar-image.component.ts +40 -11
  21. package/src/app/lib/components/ui/avatar/avatar.component.ts +18 -13
  22. package/src/app/lib/components/ui/avatar/index.ts +1 -0
  23. package/src/app/lib/components/ui/avatar/ui-avatar.component.ts +13 -20
  24. package/src/app/lib/components/ui/badge/badge-variants.ts +1 -1
  25. package/src/app/lib/components/ui/badge/badge.component.ts +1 -0
  26. package/src/app/lib/components/ui/breadcrumb/breadcrumb-ellipsis.component.ts +1 -0
  27. package/src/app/lib/components/ui/breadcrumb/breadcrumb-item.component.ts +3 -7
  28. package/src/app/lib/components/ui/breadcrumb/breadcrumb-link.component.ts +4 -11
  29. package/src/app/lib/components/ui/breadcrumb/breadcrumb-list.component.ts +3 -7
  30. package/src/app/lib/components/ui/breadcrumb/breadcrumb-page.component.ts +1 -0
  31. package/src/app/lib/components/ui/breadcrumb/breadcrumb-separator.component.ts +20 -24
  32. package/src/app/lib/components/ui/breadcrumb/breadcrumb.component.ts +1 -0
  33. package/src/app/lib/components/ui/button/button-variants.ts +3 -3
  34. package/src/app/lib/components/ui/button/button.component.ts +1 -0
  35. package/src/app/lib/components/ui/button-group/button-group.component.ts +1 -0
  36. package/src/app/lib/components/ui/calendar/calendar.component.ts +5 -1
  37. package/src/app/lib/components/ui/carousel/carousel-content.component.ts +1 -0
  38. package/src/app/lib/components/ui/carousel/carousel-item.component.ts +1 -0
  39. package/src/app/lib/components/ui/carousel/carousel-next.component.ts +1 -0
  40. package/src/app/lib/components/ui/carousel/carousel-previous.component.ts +1 -0
  41. package/src/app/lib/components/ui/carousel/carousel.component.ts +1 -0
  42. package/src/app/lib/components/ui/chart/chart-container.component.ts +1 -0
  43. package/src/app/lib/components/ui/chart/chart-legend-content.component.ts +1 -0
  44. package/src/app/lib/components/ui/chart/chart-legend.component.ts +5 -5
  45. package/src/app/lib/components/ui/chart/chart-tooltip-content.component.ts +5 -5
  46. package/src/app/lib/components/ui/chart/chart-tooltip.component.ts +5 -5
  47. package/src/app/lib/components/ui/chart/chart.component.ts +1 -0
  48. package/src/app/lib/components/ui/checkbox/checkbox.component.ts +1 -1
  49. package/src/app/lib/components/ui/collapsible/collapsible-content.component.ts +3 -1
  50. package/src/app/lib/components/ui/collapsible/collapsible-context.ts +1 -0
  51. package/src/app/lib/components/ui/collapsible/collapsible-trigger.component.ts +2 -0
  52. package/src/app/lib/components/ui/collapsible/collapsible.component.ts +4 -0
  53. package/src/app/lib/components/ui/combobox/combobox-content.component.ts +1 -0
  54. package/src/app/lib/components/ui/combobox/combobox-empty.component.ts +1 -0
  55. package/src/app/lib/components/ui/combobox/combobox-group.component.ts +1 -0
  56. package/src/app/lib/components/ui/combobox/combobox-input.component.ts +1 -0
  57. package/src/app/lib/components/ui/combobox/combobox-item.component.ts +1 -4
  58. package/src/app/lib/components/ui/combobox/combobox-list.component.ts +1 -1
  59. package/src/app/lib/components/ui/combobox/combobox-trigger.component.ts +1 -0
  60. package/src/app/lib/components/ui/combobox/combobox-value.component.ts +1 -0
  61. package/src/app/lib/components/ui/combobox/combobox.component.ts +1 -1
  62. package/src/app/lib/components/ui/command/command-dialog.component.ts +1 -0
  63. package/src/app/lib/components/ui/command/command-empty.component.ts +1 -0
  64. package/src/app/lib/components/ui/command/command-group.component.ts +1 -0
  65. package/src/app/lib/components/ui/command/command-input.component.ts +1 -0
  66. package/src/app/lib/components/ui/command/command-item.component.ts +2 -1
  67. package/src/app/lib/components/ui/command/command-list.component.ts +1 -0
  68. package/src/app/lib/components/ui/command/command-separator.component.ts +1 -0
  69. package/src/app/lib/components/ui/command/command-shortcut.component.ts +1 -0
  70. package/src/app/lib/components/ui/command/command.component.ts +1 -0
  71. package/src/app/lib/components/ui/context-menu/context-menu-checkbox-item.component.ts +1 -0
  72. package/src/app/lib/components/ui/context-menu/context-menu-content.component.ts +49 -17
  73. package/src/app/lib/components/ui/context-menu/context-menu-item.component.ts +2 -1
  74. package/src/app/lib/components/ui/context-menu/context-menu-label.component.ts +1 -0
  75. package/src/app/lib/components/ui/context-menu/context-menu-radio-group.component.ts +1 -0
  76. package/src/app/lib/components/ui/context-menu/context-menu-radio-item.component.ts +1 -0
  77. package/src/app/lib/components/ui/context-menu/context-menu-separator.component.ts +1 -0
  78. package/src/app/lib/components/ui/context-menu/context-menu-shortcut.component.ts +1 -0
  79. package/src/app/lib/components/ui/context-menu/context-menu-sub-content.component.ts +3 -0
  80. package/src/app/lib/components/ui/context-menu/context-menu-sub-trigger.component.ts +32 -2
  81. package/src/app/lib/components/ui/context-menu/context-menu-sub.component.ts +4 -0
  82. package/src/app/lib/components/ui/context-menu/context-menu-trigger.component.ts +1 -0
  83. package/src/app/lib/components/ui/context-menu/context-menu.component.ts +1 -0
  84. package/src/app/lib/components/ui/data-table/data-table-content.component.ts +1 -0
  85. package/src/app/lib/components/ui/data-table/data-table-pagination.component.ts +1 -0
  86. package/src/app/lib/components/ui/data-table/data-table-search.component.ts +1 -0
  87. package/src/app/lib/components/ui/data-table/data-table-toolbar.component.ts +1 -0
  88. package/src/app/lib/components/ui/data-table/data-table-view-options.component.ts +1 -0
  89. package/src/app/lib/components/ui/data-table/data-table.component.ts +1 -1
  90. package/src/app/lib/components/ui/date-picker/date-picker.component.ts +1 -0
  91. package/src/app/lib/components/ui/dialog/dialog-close.component.ts +1 -0
  92. package/src/app/lib/components/ui/dialog/dialog-content.component.ts +32 -21
  93. package/src/app/lib/components/ui/dialog/dialog-description.component.ts +1 -0
  94. package/src/app/lib/components/ui/dialog/dialog-footer.component.ts +1 -0
  95. package/src/app/lib/components/ui/dialog/dialog-header.component.ts +1 -0
  96. package/src/app/lib/components/ui/dialog/dialog-title.component.ts +1 -0
  97. package/src/app/lib/components/ui/dialog/dialog-trigger.component.ts +1 -0
  98. package/src/app/lib/components/ui/dialog/dialog.component.ts +1 -0
  99. package/src/app/lib/components/ui/direction/direction-context.ts +9 -0
  100. package/src/app/lib/components/ui/direction/direction.component.ts +48 -0
  101. package/src/app/lib/components/ui/direction/index.ts +2 -0
  102. package/src/app/lib/components/ui/drawer/drawer-close.component.ts +1 -0
  103. package/src/app/lib/components/ui/drawer/drawer-content.component.ts +45 -0
  104. package/src/app/lib/components/ui/drawer/drawer-description.component.ts +1 -0
  105. package/src/app/lib/components/ui/drawer/drawer-footer.component.ts +1 -0
  106. package/src/app/lib/components/ui/drawer/drawer-header.component.ts +1 -0
  107. package/src/app/lib/components/ui/drawer/drawer-title.component.ts +1 -0
  108. package/src/app/lib/components/ui/drawer/drawer-trigger.component.ts +1 -0
  109. package/src/app/lib/components/ui/drawer/drawer.component.ts +4 -0
  110. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.component.ts +1 -0
  111. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-content.component.ts +3 -2
  112. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-group.component.ts +1 -0
  113. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-item.component.ts +1 -0
  114. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-label.component.ts +1 -0
  115. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.component.ts +1 -0
  116. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.component.ts +2 -0
  117. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-separator.component.ts +1 -0
  118. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.component.ts +1 -0
  119. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.component.ts +3 -0
  120. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.component.ts +30 -3
  121. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub.component.ts +4 -0
  122. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-trigger.component.ts +26 -0
  123. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu.component.ts +1 -0
  124. package/src/app/lib/components/ui/empty/empty-action.component.ts +1 -0
  125. package/src/app/lib/components/ui/empty/empty-description.component.ts +1 -0
  126. package/src/app/lib/components/ui/empty/empty-icon.component.ts +2 -1
  127. package/src/app/lib/components/ui/empty/empty-title.component.ts +1 -0
  128. package/src/app/lib/components/ui/empty/empty.component.ts +1 -0
  129. package/src/app/lib/components/ui/form/form-description.component.ts +2 -2
  130. package/src/app/lib/components/ui/hover-card/hover-card-content.component.ts +109 -60
  131. package/src/app/lib/components/ui/hover-card/hover-card-context.ts +4 -2
  132. package/src/app/lib/components/ui/hover-card/hover-card-trigger.component.ts +6 -3
  133. package/src/app/lib/components/ui/hover-card/hover-card.component.ts +9 -3
  134. package/src/app/lib/components/ui/input-group/input-group-addon.component.ts +1 -0
  135. package/src/app/lib/components/ui/input-group/input-group-input.component.ts +1 -0
  136. package/src/app/lib/components/ui/input-group/input-group.component.ts +1 -0
  137. package/src/app/lib/components/ui/input-otp/input-otp-group.component.ts +1 -0
  138. package/src/app/lib/components/ui/input-otp/input-otp-separator.component.ts +1 -0
  139. package/src/app/lib/components/ui/input-otp/input-otp-slot.component.ts +1 -0
  140. package/src/app/lib/components/ui/input-otp/input-otp.component.ts +1 -0
  141. package/src/app/lib/components/ui/kbd/kbd.component.ts +1 -0
  142. package/src/app/lib/components/ui/menubar/menubar-checkbox-item.component.ts +1 -0
  143. package/src/app/lib/components/ui/menubar/menubar-content.component.ts +2 -1
  144. package/src/app/lib/components/ui/menubar/menubar-item.component.ts +1 -0
  145. package/src/app/lib/components/ui/menubar/menubar-label.component.ts +1 -0
  146. package/src/app/lib/components/ui/menubar/menubar-menu.component.ts +1 -0
  147. package/src/app/lib/components/ui/menubar/menubar-radio-group.component.ts +1 -0
  148. package/src/app/lib/components/ui/menubar/menubar-radio-item.component.ts +1 -0
  149. package/src/app/lib/components/ui/menubar/menubar-separator.component.ts +1 -0
  150. package/src/app/lib/components/ui/menubar/menubar-shortcut.component.ts +1 -0
  151. package/src/app/lib/components/ui/menubar/menubar-sub-content.component.ts +1 -0
  152. package/src/app/lib/components/ui/menubar/menubar-sub-trigger.component.ts +1 -0
  153. package/src/app/lib/components/ui/menubar/menubar-sub.component.ts +1 -0
  154. package/src/app/lib/components/ui/menubar/menubar-trigger.component.ts +1 -0
  155. package/src/app/lib/components/ui/menubar/menubar.component.ts +1 -0
  156. package/src/app/lib/components/ui/native-select/native-select.component.ts +1 -1
  157. package/src/app/lib/components/ui/navigation-menu/navigation-menu-content.component.ts +8 -1
  158. package/src/app/lib/components/ui/navigation-menu/navigation-menu-context.ts +14 -0
  159. package/src/app/lib/components/ui/navigation-menu/navigation-menu-indicator.component.ts +1 -0
  160. package/src/app/lib/components/ui/navigation-menu/navigation-menu-item.component.ts +10 -4
  161. package/src/app/lib/components/ui/navigation-menu/navigation-menu-link.component.ts +1 -0
  162. package/src/app/lib/components/ui/navigation-menu/navigation-menu-list.component.ts +1 -0
  163. package/src/app/lib/components/ui/navigation-menu/navigation-menu-trigger.component.ts +70 -2
  164. package/src/app/lib/components/ui/navigation-menu/navigation-menu-viewport.component.ts +1 -0
  165. package/src/app/lib/components/ui/navigation-menu/navigation-menu.component.ts +36 -4
  166. package/src/app/lib/components/ui/pagination/pagination-content.component.ts +1 -0
  167. package/src/app/lib/components/ui/pagination/pagination-ellipsis.component.ts +1 -0
  168. package/src/app/lib/components/ui/pagination/pagination-item.component.ts +1 -0
  169. package/src/app/lib/components/ui/pagination/pagination-link.component.ts +1 -0
  170. package/src/app/lib/components/ui/pagination/pagination-next.component.ts +1 -0
  171. package/src/app/lib/components/ui/pagination/pagination-previous.component.ts +1 -0
  172. package/src/app/lib/components/ui/pagination/pagination.component.ts +4 -1
  173. package/src/app/lib/components/ui/popover/popover-anchor.component.ts +1 -0
  174. package/src/app/lib/components/ui/popover/popover-content.component.ts +12 -0
  175. package/src/app/lib/components/ui/popover/popover-context.ts +2 -0
  176. package/src/app/lib/components/ui/popover/popover-trigger.component.ts +1 -0
  177. package/src/app/lib/components/ui/popover/popover.component.ts +5 -0
  178. package/src/app/lib/components/ui/progress/progress.component.ts +1 -2
  179. package/src/app/lib/components/ui/resizable/resizable-handle.component.ts +1 -0
  180. package/src/app/lib/components/ui/resizable/resizable-panel-group.component.ts +1 -0
  181. package/src/app/lib/components/ui/resizable/resizable-panel.component.ts +1 -0
  182. package/src/app/lib/components/ui/scroll-area/scroll-area.component.ts +8 -6
  183. package/src/app/lib/components/ui/scroll-area/scroll-bar.component.ts +1 -0
  184. package/src/app/lib/components/ui/segmented/segmented-item.component.ts +1 -0
  185. package/src/app/lib/components/ui/segmented/segmented.component.ts +1 -0
  186. package/src/app/lib/components/ui/select/select-content.component.ts +38 -17
  187. package/src/app/lib/components/ui/select/select-context.ts +10 -0
  188. package/src/app/lib/components/ui/select/select-item.component.ts +25 -7
  189. package/src/app/lib/components/ui/select/select-trigger.component.ts +6 -13
  190. package/src/app/lib/components/ui/select/select.component.ts +46 -0
  191. package/src/app/lib/components/ui/sheet/sheet-close.component.ts +1 -0
  192. package/src/app/lib/components/ui/sheet/sheet-content.component.ts +23 -5
  193. package/src/app/lib/components/ui/sheet/sheet-description.component.ts +1 -0
  194. package/src/app/lib/components/ui/sheet/sheet-footer.component.ts +1 -0
  195. package/src/app/lib/components/ui/sheet/sheet-header.component.ts +1 -0
  196. package/src/app/lib/components/ui/sheet/sheet-title.component.ts +1 -0
  197. package/src/app/lib/components/ui/sheet/sheet-trigger.component.ts +1 -0
  198. package/src/app/lib/components/ui/sheet/sheet.component.ts +4 -0
  199. package/src/app/lib/components/ui/sidebar/sidebar-content.component.ts +1 -0
  200. package/src/app/lib/components/ui/sidebar/sidebar-footer.component.ts +1 -0
  201. package/src/app/lib/components/ui/sidebar/sidebar-group-action.component.ts +1 -0
  202. package/src/app/lib/components/ui/sidebar/sidebar-group-content.component.ts +1 -0
  203. package/src/app/lib/components/ui/sidebar/sidebar-group-label.component.ts +1 -0
  204. package/src/app/lib/components/ui/sidebar/sidebar-group.component.ts +1 -0
  205. package/src/app/lib/components/ui/sidebar/sidebar-header.component.ts +1 -0
  206. package/src/app/lib/components/ui/sidebar/sidebar-input.component.ts +1 -0
  207. package/src/app/lib/components/ui/sidebar/sidebar-inset.component.ts +1 -0
  208. package/src/app/lib/components/ui/sidebar/sidebar-menu-action.component.ts +1 -0
  209. package/src/app/lib/components/ui/sidebar/sidebar-menu-badge.component.ts +1 -0
  210. package/src/app/lib/components/ui/sidebar/sidebar-menu-button.component.ts +1 -0
  211. package/src/app/lib/components/ui/sidebar/sidebar-menu-item.component.ts +1 -0
  212. package/src/app/lib/components/ui/sidebar/sidebar-menu-skeleton.component.ts +1 -0
  213. package/src/app/lib/components/ui/sidebar/sidebar-menu-sub-button.component.ts +1 -0
  214. package/src/app/lib/components/ui/sidebar/sidebar-menu-sub-item.component.ts +1 -0
  215. package/src/app/lib/components/ui/sidebar/sidebar-menu-sub.component.ts +1 -0
  216. package/src/app/lib/components/ui/sidebar/sidebar-menu.component.ts +1 -0
  217. package/src/app/lib/components/ui/sidebar/sidebar-provider.component.ts +1 -0
  218. package/src/app/lib/components/ui/sidebar/sidebar-rail.component.ts +1 -0
  219. package/src/app/lib/components/ui/sidebar/sidebar-separator.component.ts +1 -0
  220. package/src/app/lib/components/ui/sidebar/sidebar-trigger.component.ts +1 -0
  221. package/src/app/lib/components/ui/sidebar/sidebar.component.ts +1 -0
  222. package/src/app/lib/components/ui/slider/slider.component.ts +2 -2
  223. package/src/app/lib/components/ui/sonner/index.ts +2 -0
  224. package/src/app/lib/components/ui/sonner/sonner.component.ts +70 -0
  225. package/src/app/lib/components/ui/spinner/spinner.component.ts +1 -0
  226. package/src/app/lib/components/ui/switch/switch.component.ts +1 -14
  227. package/src/app/lib/components/ui/table/table-body.component.ts +1 -0
  228. package/src/app/lib/components/ui/table/table-caption.component.ts +1 -0
  229. package/src/app/lib/components/ui/table/table-cell.component.ts +1 -0
  230. package/src/app/lib/components/ui/table/table-footer.component.ts +1 -0
  231. package/src/app/lib/components/ui/table/table-head.component.ts +1 -0
  232. package/src/app/lib/components/ui/table/table-header.component.ts +1 -0
  233. package/src/app/lib/components/ui/table/table-row.component.ts +1 -0
  234. package/src/app/lib/components/ui/table/table.component.ts +1 -0
  235. package/src/app/lib/components/ui/tabs/tabs-content.component.ts +3 -6
  236. package/src/app/lib/components/ui/tabs/tabs-list.component.ts +19 -0
  237. package/src/app/lib/components/ui/tabs/tabs-trigger.component.ts +1 -1
  238. package/src/app/lib/components/ui/tabs/tabs.component.ts +1 -0
  239. package/src/app/lib/components/ui/toast/toast-action.component.ts +1 -0
  240. package/src/app/lib/components/ui/toast/toast-description.component.ts +1 -0
  241. package/src/app/lib/components/ui/toast/toast-title.component.ts +1 -0
  242. package/src/app/lib/components/ui/toast/toast.component.ts +1 -0
  243. package/src/app/lib/components/ui/toast/toaster.component.ts +1 -0
  244. package/src/app/lib/components/ui/toggle/toggle-variants.ts +1 -1
  245. package/src/app/lib/components/ui/toggle/toggle.component.ts +12 -6
  246. package/src/app/lib/components/ui/tooltip/tooltip-content.component.ts +142 -17
  247. package/src/app/lib/components/ui/tooltip/tooltip-context.ts +3 -1
  248. package/src/app/lib/components/ui/tooltip/tooltip-provider.component.ts +5 -1
  249. package/src/app/lib/components/ui/tooltip/tooltip-trigger.component.ts +6 -2
  250. package/src/app/lib/components/ui/tooltip/tooltip.component.ts +4 -1
  251. package/src/app/lib/components/ui/typography/typography-blockquote.component.ts +1 -0
  252. package/src/app/lib/components/ui/typography/typography-h1.component.ts +1 -0
  253. package/src/app/lib/components/ui/typography/typography-h2.component.ts +1 -0
  254. package/src/app/lib/components/ui/typography/typography-h3.component.ts +1 -0
  255. package/src/app/lib/components/ui/typography/typography-h4.component.ts +1 -0
  256. package/src/app/lib/components/ui/typography/typography-inline-code.component.ts +1 -0
  257. package/src/app/lib/components/ui/typography/typography-large.component.ts +1 -0
  258. package/src/app/lib/components/ui/typography/typography-lead.component.ts +1 -0
  259. package/src/app/lib/components/ui/typography/typography-list.component.ts +1 -0
  260. package/src/app/lib/components/ui/typography/typography-muted.component.ts +1 -0
  261. package/src/app/lib/components/ui/typography/typography-p.component.ts +1 -0
  262. package/src/app/lib/components/ui/typography/typography-small.component.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-cn/core",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Beautifully designed Angular components built with Tailwind CSS v4 - The official Angular port of shadcn/ui",
5
5
  "keywords": [
6
6
  "angular",
@@ -58,6 +58,8 @@
58
58
  ]
59
59
  },
60
60
  "dependencies": {
61
+ "@angular-devkit/core": "^21.2.5",
62
+ "@angular-devkit/schematics": "^21.2.5",
61
63
  "@angular/cdk": "^21.2.4",
62
64
  "@angular/common": "^21.2.6",
63
65
  "@angular/compiler": "^21.2.6",
@@ -74,18 +76,17 @@
74
76
  "express": "^5.1.0",
75
77
  "lucide-angular": "^1.0.0",
76
78
  "ng-apexcharts": "^2.3.0",
79
+ "ngx-sonner": "^3.1.0",
77
80
  "postcss": "^8.5.8",
78
81
  "rxjs": "~7.8.0",
79
82
  "shiki": "^4.0.2",
80
83
  "tailwind-merge": "^3.5.0",
81
84
  "tailwindcss": "^4.2.2",
82
- "tslib": "^2.3.0",
83
- "@angular-devkit/core": "^21.2.5",
84
- "@angular-devkit/schematics": "^21.2.5"
85
+ "tslib": "^2.3.0"
85
86
  },
86
87
  "devDependencies": {
87
- "@analogjs/vitest-angular": "^2.3.1",
88
88
  "@analogjs/vite-plugin-angular": "^2.2.0",
89
+ "@analogjs/vitest-angular": "^2.3.1",
89
90
  "@angular/build": "^21.2.5",
90
91
  "@angular/cli": "^21.2.5",
91
92
  "@angular/compiler-cli": "^21.2.6",
@@ -4,7 +4,8 @@ import { ACCORDION_ITEM_CONTEXT } from './accordion-context';
4
4
 
5
5
  /**
6
6
  * AccordionContent component - expandable content area.
7
- * Uses role="region" and aria-labelledby to associate with trigger.
7
+ * Uses CSS grid animation (grid-template-rows: 0fr 1fr) for smooth open/close.
8
+ * Content is always in the DOM; visibility is controlled by the grid row height.
8
9
  *
9
10
  * @example
10
11
  * <AccordionContent>
@@ -14,19 +15,17 @@ import { ACCORDION_ITEM_CONTEXT } from './accordion-context';
14
15
  @Component({
15
16
  selector: 'AccordionContent',
16
17
  template: `
17
- @if (item.isOpen()) {
18
- <div [class]="innerClass()">
19
- <ng-content />
20
- </div>
21
- }
18
+ <div [class]="innerClass()">
19
+ <ng-content />
20
+ </div>
22
21
  `,
23
22
  host: {
23
+ 'attr.data-slot': '"accordion-content"',
24
24
  role: 'region',
25
25
  '[class]': 'computedClass()',
26
26
  '[attr.id]': 'item.contentId',
27
27
  '[attr.data-state]': 'item.isOpen() ? "open" : "closed"',
28
28
  '[attr.aria-labelledby]': 'item.triggerId',
29
- '[attr.aria-hidden]': '!item.isOpen()',
30
29
  },
31
30
  changeDetection: ChangeDetectionStrategy.OnPush,
32
31
  })
@@ -36,13 +35,11 @@ export class AccordionContent {
36
35
 
37
36
  protected readonly item = inject(ACCORDION_ITEM_CONTEXT);
38
37
 
39
- protected readonly computedClass = computed(() =>
40
- cn(
41
- 'overflow-hidden text-sm',
42
- 'data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
43
- ),
44
- );
38
+ // No overflow-hidden on the host — grid-template-rows handles expansion.
39
+ // Global CSS ([data-slot='accordion-content'] > div { overflow: hidden }) clips the inner div.
40
+ protected readonly computedClass = computed(() => cn('text-sm', this.class()));
41
+
45
42
  protected readonly innerClass = computed(() =>
46
- cn('pb-4 pt-2 px-1 text-muted-foreground leading-relaxed', this.class()),
43
+ cn('pb-4 pt-2 px-1 text-muted-foreground leading-relaxed'),
47
44
  );
48
45
  }
@@ -16,6 +16,7 @@ export interface AccordionItemContext {
16
16
  value: () => string;
17
17
  isOpen: () => boolean;
18
18
  toggle: () => void;
19
+ disabled: () => boolean;
19
20
  /** Unique IDs for ARIA relationships */
20
21
  triggerId: string;
21
22
  contentId: string;
@@ -1,6 +1,7 @@
1
1
  import { cn } from '@/lib/utils';
2
2
  import { AriaIdService } from '@/lib/utils/accessibility';
3
3
  import {
4
+ booleanAttribute,
4
5
  ChangeDetectionStrategy,
5
6
  Component,
6
7
  computed,
@@ -30,9 +31,12 @@ import {
30
31
  selector: 'AccordionItem',
31
32
  template: `<ng-content />`,
32
33
  host: {
34
+ 'attr.data-slot': '"accordion-item"',
33
35
  '[class]': 'computedClass()',
34
36
  '[attr.data-state]': 'isOpen() ? "open" : "closed"',
35
37
  '[attr.data-value]': 'value()',
38
+ '[attr.data-disabled]': 'disabled() ? "" : null',
39
+ '[attr.aria-disabled]': 'disabled() || null',
36
40
  },
37
41
  providers: [
38
42
  {
@@ -46,6 +50,9 @@ export class AccordionItem implements AccordionItemContext, OnInit, OnDestroy {
46
50
  /** Unique value for this accordion item */
47
51
  readonly value = input.required<string>();
48
52
 
53
+ /** Whether this item is disabled */
54
+ readonly disabled = input<boolean, unknown>(false, { transform: booleanAttribute });
55
+
49
56
  /** Additional CSS classes */
50
57
  readonly class = input<string>('');
51
58
 
@@ -77,6 +84,7 @@ export class AccordionItem implements AccordionItemContext, OnInit, OnDestroy {
77
84
 
78
85
  /** Toggle this item's open state */
79
86
  toggle(): void {
87
+ if (this.disabled()) return;
80
88
  this._accordion.onValueChange(this.value());
81
89
  }
82
90
  }
@@ -15,7 +15,6 @@ import { ACCORDION_ITEM_CONTEXT } from './accordion-context';
15
15
  <span class="me-2"><ng-content /></span>
16
16
  <svg
17
17
  class="size-4 shrink-0 ms-auto text-muted-foreground transition-transform duration-200"
18
- [class.rotate-180]="item.isOpen()"
19
18
  xmlns="http://www.w3.org/2000/svg"
20
19
  width="24"
21
20
  height="24"
@@ -30,11 +29,13 @@ import { ACCORDION_ITEM_CONTEXT } from './accordion-context';
30
29
  </svg>
31
30
  `,
32
31
  host: {
32
+ 'attr.data-slot': '"accordion-trigger"',
33
33
  '[class]': 'computedClass()',
34
34
  '[attr.id]': 'item.triggerId',
35
35
  '[attr.data-state]': 'item.isOpen() ? "open" : "closed"',
36
36
  '[attr.aria-expanded]': 'item.isOpen()',
37
37
  '[attr.aria-controls]': 'item.contentId',
38
+ '[attr.aria-disabled]': 'item.disabled() || null',
38
39
  '(click)': 'onClick()',
39
40
  '(keydown.enter)': 'onClick()',
40
41
  '(keydown.space)': 'onSpace($event)',
@@ -52,15 +53,18 @@ export class AccordionTrigger {
52
53
  protected readonly computedClass = computed(() =>
53
54
  cn(
54
55
  'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180 cursor-pointer w-full',
56
+ this.item.disabled() && 'cursor-not-allowed opacity-50 hover:no-underline',
55
57
  this.class(),
56
58
  ),
57
59
  );
58
60
 
59
61
  protected onClick(): void {
62
+ if (this.item.disabled()) return;
60
63
  this.item.toggle();
61
64
  }
62
65
  protected onSpace(event: Event): void {
63
66
  event.preventDefault();
67
+ if (this.item.disabled()) return;
64
68
  this.item.toggle();
65
69
  }
66
70
  }
@@ -7,6 +7,7 @@ import {
7
7
  effect,
8
8
  forwardRef,
9
9
  input,
10
+ output,
10
11
  signal,
11
12
  } from '@angular/core';
12
13
  import { ACCORDION_CONTEXT, AccordionContext, AccordionType } from './accordion-context';
@@ -28,6 +29,7 @@ import { ACCORDION_CONTEXT, AccordionContext, AccordionType } from './accordion-
28
29
  selector: 'Accordion',
29
30
  template: `<ng-content />`,
30
31
  host: {
32
+ 'attr.data-slot': '"accordion"',
31
33
  '[class]': 'computedClass()',
32
34
  '[attr.data-orientation]': '"vertical"',
33
35
  '(keydown)': 'onKeydown($event)',
@@ -69,6 +71,9 @@ export class Accordion implements AccordionContext {
69
71
  /** Additional CSS classes */
70
72
  readonly class = input<string>('');
71
73
 
74
+ /** Emits the new value whenever the open state changes */
75
+ readonly valueChange = output<string | string[] | undefined>();
76
+
72
77
  protected readonly computedClass = computed(() => cn('w-full', this.class()));
73
78
  /** Get current value(s) */
74
79
  readonly value = computed(() => {
@@ -119,6 +124,7 @@ export class Accordion implements AccordionContext {
119
124
 
120
125
  return newSet;
121
126
  });
127
+ this.valueChange.emit(this.value());
122
128
  }
123
129
  /** Check if an item is open */
124
130
  isItemOpen(itemValue: string): boolean {
@@ -179,12 +185,24 @@ export class Accordion implements AccordionContext {
179
185
 
180
186
  if (handled) {
181
187
  event.preventDefault();
182
- // Focus the trigger of the new item
183
- const newValue = values[newIndex];
184
- const newTrigger = document.querySelector(
185
- `AccordionItem[data-value="${newValue}"] AccordionTrigger, [data-accordion-trigger="${newValue}"]`,
186
- ) as HTMLElement;
187
- newTrigger?.focus();
188
+ const direction =
189
+ event.key === 'ArrowDown' || event.key === 'End' ? 1 : -1;
190
+ let attempts = 0;
191
+ while (attempts < values.length) {
192
+ const candidateValue = values[newIndex];
193
+ const candidateItem = document.querySelector(
194
+ `AccordionItem[data-value="${candidateValue}"]`,
195
+ );
196
+ if (!candidateItem?.hasAttribute('data-disabled')) {
197
+ const trigger = document.querySelector(
198
+ `AccordionItem[data-value="${candidateValue}"] AccordionTrigger`,
199
+ ) as HTMLElement;
200
+ trigger?.focus();
201
+ break;
202
+ }
203
+ newIndex = (newIndex + direction + values.length) % values.length;
204
+ attempts++;
205
+ }
188
206
  }
189
207
  }
190
208
  }
@@ -1,17 +1,31 @@
1
1
  import { cva, type VariantProps } from 'class-variance-authority';
2
2
 
3
3
  /**
4
- * Alert variants using class-variance-authority
5
- * WCAG AA compliant contrast ratios for all variants
4
+ * Alert variants using class-variance-authority.
5
+ * Selectors cover both native <svg> and lucide-angular's <lucide-icon> element.
6
6
  */
7
7
  export const alertVariants = cva(
8
- 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
8
+ [
9
+ 'relative w-full rounded-lg border px-4 py-3 text-sm',
10
+ // Grid base — 1-col by default, switches to 2-col when icon is present
11
+ 'grid grid-cols-[0_1fr]',
12
+ 'has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr]',
13
+ 'has-[>lucide-icon]:grid-cols-[calc(var(--spacing)*4)_1fr]',
14
+ // Column gap when icon present
15
+ 'has-[>svg]:gap-x-3',
16
+ 'has-[>lucide-icon]:gap-x-3',
17
+ 'gap-y-0.5 items-start',
18
+ // Icon sizing and alignment — native SVG
19
+ '[&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
20
+ // Icon sizing and alignment — lucide-angular
21
+ '[&>lucide-icon]:size-4 [&>lucide-icon]:translate-y-0.5 [&>lucide-icon]:text-current',
22
+ ].join(' '),
9
23
  {
10
24
  variants: {
11
25
  variant: {
12
26
  default: 'bg-card text-card-foreground border-border',
13
27
  destructive:
14
- 'bg-destructive/10 text-destructive border-destructive/30 [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
28
+ 'bg-destructive/10 text-destructive border-destructive/30 [&>svg]:text-current [&>lucide-icon]:text-current *:data-[slot=alert-description]:text-destructive/90',
15
29
  },
16
30
  },
17
31
  defaultVariants: {
@@ -15,6 +15,7 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
15
15
  selector: 'AlertDialogAction',
16
16
  template: `<ng-content />`,
17
17
  host: {
18
+ 'attr.data-slot': '"alert-dialog-action"',
18
19
  '[class]': 'computedClass()',
19
20
  '(click)': 'onClick($event)',
20
21
  },
@@ -18,7 +18,7 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
18
18
  host: {
19
19
  '[class]': 'computedClass()',
20
20
  '(click)': 'onClick($event)',
21
- 'data-slot': 'alert-dialog-cancel',
21
+ 'attr.data-slot': '"alert-dialog-cancel"',
22
22
  },
23
23
  changeDetection: ChangeDetectionStrategy.OnPush,
24
24
  })
@@ -1,7 +1,8 @@
1
- import { cn } from '@/lib/utils';
1
+ import { cn, Presence } from '@/lib/utils';
2
2
  import { FocusTrapDirective } from '@/lib/utils/accessibility';
3
3
  import {
4
4
  ChangeDetectionStrategy,
5
+ ChangeDetectorRef,
5
6
  Component,
6
7
  computed,
7
8
  DestroyRef,
@@ -21,14 +22,20 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
21
22
  * - Overlay/backdrop click does NOT close the dialog
22
23
  * - Focus is trapped within the dialog
23
24
  * - User must explicitly click Cancel or Action to close
25
+ * - Exit animations handled by Presence component (no setTimeout needed)
26
+ * - Focus restored on any programmatic close (Action/Cancel/Escape)
24
27
  */
25
28
  @Component({
26
29
  selector: 'AlertDialogContent',
27
- imports: [FocusTrapDirective],
30
+ imports: [FocusTrapDirective, Presence],
28
31
  template: `
29
- @if (context.isOpen()) {
32
+ <Presence [present]="context.isOpen()">
30
33
  <!-- Overlay - does NOT close on click -->
31
- <div class="fixed inset-0 z-50 bg-black/80" aria-hidden="true"></div>
34
+ <div
35
+ class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 duration-200"
36
+ [attr.data-state]="context.isOpen() ? 'open' : 'closed'"
37
+ aria-hidden="true"
38
+ ></div>
32
39
  <!-- Content Dialog -->
33
40
  <div
34
41
  hlmFocusTrap
@@ -37,6 +44,7 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
37
44
  [restoreFocus]="false"
38
45
  [initialFocus]="'[data-slot=alert-dialog-cancel]'"
39
46
  [class]="computedClass()"
47
+ [attr.data-state]="context.isOpen() ? 'open' : 'closed'"
40
48
  [attr.id]="context.contentId"
41
49
  [attr.aria-labelledby]="context.titleId"
42
50
  [attr.aria-describedby]="context.descriptionId"
@@ -45,36 +53,45 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
45
53
  >
46
54
  <ng-content />
47
55
  </div>
48
- }
56
+ </Presence>
49
57
  `,
50
58
  host: {
59
+ 'attr.data-slot': '"alert-dialog-content"',
51
60
  class: 'contents',
52
61
  },
53
62
  changeDetection: ChangeDetectionStrategy.OnPush,
54
63
  })
55
64
  export class AlertDialogContent {
56
65
  constructor() {
57
- // Handle body scroll lock based on open state
66
+ let wasOpen = false;
67
+
58
68
  effect(() => {
59
69
  const isOpen = this.context.isOpen();
70
+ this._cdr.markForCheck();
71
+
60
72
  if (isOpen) {
73
+ wasOpen = true;
61
74
  this.lockBodyScroll();
62
75
  } else {
63
76
  this.unlockBodyScroll();
77
+ // Restore focus whenever dialog closes (covers Action/Cancel/Escape paths)
78
+ if (wasOpen) {
79
+ this.restoreFocus();
80
+ }
81
+ wasOpen = false;
64
82
  }
65
83
  });
66
84
 
67
- // Cleanup on destroy
68
85
  this._destroyRef.onDestroy(() => {
69
86
  this.unlockBodyScroll();
70
87
  this.restoreFocus();
71
88
  });
72
89
  }
73
90
 
74
- /** Additional CSS classes */
75
91
  readonly class = input<string>('');
76
92
 
77
93
  private readonly _destroyRef = inject(DestroyRef);
94
+ private readonly _cdr = inject(ChangeDetectorRef);
78
95
 
79
96
  protected readonly context = inject(ALERT_DIALOG_CONTEXT);
80
97
 
@@ -91,37 +108,37 @@ export class AlertDialogContent {
91
108
  ),
92
109
  );
93
110
 
94
- /** Previous body overflow for restoration */
95
111
  private previousBodyOverflow = '';
112
+ private previousBodyPaddingRight = '';
96
113
 
97
114
  @HostListener('document:keydown.escape')
98
115
  onEscapeKey(): void {
99
116
  if (this.context.isOpen()) {
100
- this.close();
117
+ this.context.setOpen(false);
101
118
  }
102
119
  }
103
120
 
104
121
  private lockBodyScroll(): void {
105
122
  if (typeof document !== 'undefined') {
123
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
106
124
  this.previousBodyOverflow = document.body.style.overflow;
125
+ this.previousBodyPaddingRight = document.body.style.paddingRight;
107
126
  document.body.style.overflow = 'hidden';
127
+ if (scrollbarWidth > 0) {
128
+ document.body.style.paddingRight = scrollbarWidth + 'px';
129
+ }
108
130
  }
109
131
  }
110
132
  private unlockBodyScroll(): void {
111
133
  if (typeof document !== 'undefined') {
112
134
  document.body.style.overflow = this.previousBodyOverflow;
135
+ document.body.style.paddingRight = this.previousBodyPaddingRight;
113
136
  }
114
137
  }
115
- private close(): void {
116
- this.restoreFocus();
117
- this.context.setOpen(false);
118
- }
119
138
  private restoreFocus(): void {
120
139
  const triggerEl = this.context.getTriggerElement();
121
140
  if (triggerEl) {
122
- setTimeout(() => {
123
- triggerEl.focus();
124
- }, 0);
141
+ setTimeout(() => triggerEl.focus(), 0);
125
142
  }
126
143
  }
127
144
  }
@@ -14,6 +14,7 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
14
14
  selector: 'AlertDialogDescription',
15
15
  template: `<ng-content />`,
16
16
  host: {
17
+ 'attr.data-slot': '"alert-dialog-description"',
17
18
  '[class]': 'computedClass()',
18
19
  '[attr.id]': 'context.descriptionId',
19
20
  },
@@ -16,6 +16,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
16
16
  selector: 'AlertDialogFooter',
17
17
  template: `<ng-content />`,
18
18
  host: {
19
+ 'attr.data-slot': '"alert-dialog-footer"',
19
20
  '[class]': 'computedClass()',
20
21
  },
21
22
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -16,6 +16,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
16
16
  selector: 'AlertDialogHeader',
17
17
  template: `<ng-content />`,
18
18
  host: {
19
+ 'attr.data-slot': '"alert-dialog-header"',
19
20
  '[class]': 'computedClass()',
20
21
  },
21
22
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -14,6 +14,7 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
14
14
  selector: 'AlertDialogTitle',
15
15
  template: `<ng-content />`,
16
16
  host: {
17
+ 'attr.data-slot': '"alert-dialog-title"',
17
18
  '[class]': 'computedClass()',
18
19
  '[attr.id]': 'context.titleId',
19
20
  },
@@ -15,6 +15,7 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
15
15
  selector: 'AlertDialogTrigger',
16
16
  template: `<ng-content />`,
17
17
  host: {
18
+ 'attr.data-slot': '"alert-dialog-trigger"',
18
19
  '(click)': 'onClick($event)',
19
20
  '[attr.aria-haspopup]': '"dialog"',
20
21
  '[attr.aria-expanded]': 'context.isOpen()',
@@ -43,6 +43,10 @@ import { ALERT_DIALOG_CONTEXT, type AlertDialogContextValue } from './alert-dial
43
43
  useExisting: forwardRef(() => AlertDialog),
44
44
  },
45
45
  ],
46
+ host: {
47
+ 'attr.data-slot': '"alert-dialog"',
48
+ style: 'display: contents',
49
+ },
46
50
  changeDetection: ChangeDetectionStrategy.OnPush,
47
51
  })
48
52
  export class AlertDialog implements AlertDialogContextValue {
@@ -30,6 +30,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
30
30
  </div>
31
31
  `,
32
32
  host: {
33
+ 'attr.data-slot': '"aspect-ratio"',
33
34
  '[class]': 'computedClass()',
34
35
  '[style.padding-bottom]': 'paddingBottom()',
35
36
  '[style.position]': '"relative"',
@@ -0,0 +1,9 @@
1
+ import { InjectionToken, WritableSignal } from '@angular/core';
2
+
3
+ export type AvatarImageStatus = 'idle' | 'loaded' | 'error';
4
+
5
+ export interface AvatarContext {
6
+ imageStatus: WritableSignal<AvatarImageStatus>;
7
+ }
8
+
9
+ export const AVATAR_CONTEXT = new InjectionToken<AvatarContext>('AvatarContext');
@@ -1,24 +1,21 @@
1
1
  import { cn } from '@/lib/utils';
2
- import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
3
+ import { AVATAR_CONTEXT } from './avatar-context';
3
4
 
4
- /**
5
- * Avatar fallback component.
6
- * Shown when the image fails to load or is not provided.
7
- *
8
- * @example
9
- * <AvatarFallback>JD</AvatarFallback>
10
- */
11
5
  @Component({
12
6
  selector: 'AvatarFallback',
13
7
  template: `<ng-content />`,
14
8
  host: {
9
+ 'attr.data-slot': '"avatar-fallback"',
15
10
  '[class]': 'computedClass()',
16
- 'data-slot': 'avatar-fallback',
11
+ // Hidden via display:none when image has loaded; always in DOM for layout stability
12
+ '[style.display]': 'context.imageStatus() === "loaded" ? "none" : null',
17
13
  },
18
14
  changeDetection: ChangeDetectionStrategy.OnPush,
19
15
  })
20
16
  export class AvatarFallback {
21
17
  readonly class = input<string>('');
18
+ protected readonly context = inject(AVATAR_CONTEXT);
22
19
 
23
20
  protected readonly computedClass = computed(() =>
24
21
  cn('bg-muted flex size-full items-center justify-center rounded-full text-xs', this.class()),
@@ -1,24 +1,53 @@
1
1
  import { cn } from '@/lib/utils';
2
- import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ computed,
6
+ effect,
7
+ inject,
8
+ input,
9
+ } from '@angular/core';
10
+ import { AVATAR_CONTEXT } from './avatar-context';
3
11
 
4
- /**
5
- * Avatar image component.
6
- * The image to display for the avatar.
7
- *
8
- * @example
9
- * <AvatarImage src="/avatar.png" alt="User avatar" />
10
- */
11
12
  @Component({
12
13
  selector: 'AvatarImage',
13
- template: `<ng-content />`,
14
+ template: `
15
+ @if (context.imageStatus() !== 'error') {
16
+ <img
17
+ [src]="src()"
18
+ [alt]="alt()"
19
+ [class]="computedClass()"
20
+ (load)="onLoad()"
21
+ (error)="onError()"
22
+ />
23
+ }
24
+ `,
14
25
  host: {
15
- '[class]': 'computedClass()',
16
- 'data-slot': 'avatar-image',
26
+ 'attr.data-slot': '"avatar-image"',
17
27
  },
18
28
  changeDetection: ChangeDetectionStrategy.OnPush,
19
29
  })
20
30
  export class AvatarImage {
31
+ readonly src = input<string>('');
32
+ readonly alt = input<string>('');
21
33
  readonly class = input<string>('');
22
34
 
35
+ protected readonly context = inject(AVATAR_CONTEXT);
23
36
  protected readonly computedClass = computed(() => cn('aspect-square size-full', this.class()));
37
+
38
+ constructor() {
39
+ // Reset status whenever src changes so a new src gets a fresh load attempt
40
+ effect(() => {
41
+ const _ = this.src();
42
+ this.context.imageStatus.set('idle');
43
+ });
44
+ }
45
+
46
+ protected onLoad(): void {
47
+ this.context.imageStatus.set('loaded');
48
+ }
49
+
50
+ protected onError(): void {
51
+ this.context.imageStatus.set('error');
52
+ }
24
53
  }
@@ -1,27 +1,32 @@
1
1
  import { cn } from '@/lib/utils';
2
- import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ computed,
6
+ forwardRef,
7
+ input,
8
+ signal,
9
+ } from '@angular/core';
10
+ import { AVATAR_CONTEXT, AvatarContext, AvatarImageStatus } from './avatar-context';
3
11
 
4
- /**
5
- * Avatar container component.
6
- * Wraps the avatar image and fallback.
7
- *
8
- * @example
9
- * <Avatar>
10
- * <img AvatarImage src="/avatar.png" alt="User" />
11
- * <AvatarFallback>JD</AvatarFallback>
12
- * </Avatar>
13
- */
14
12
  @Component({
15
13
  selector: 'Avatar',
16
14
  template: `<ng-content />`,
17
15
  host: {
16
+ 'attr.data-slot': '"avatar"',
18
17
  '[class]': 'computedClass()',
19
- 'data-slot': 'avatar',
20
18
  },
19
+ providers: [
20
+ {
21
+ provide: AVATAR_CONTEXT,
22
+ useExisting: forwardRef(() => Avatar),
23
+ },
24
+ ],
21
25
  changeDetection: ChangeDetectionStrategy.OnPush,
22
26
  })
23
- export class Avatar {
27
+ export class Avatar implements AvatarContext {
24
28
  readonly class = input<string>('');
29
+ readonly imageStatus = signal<AvatarImageStatus>('idle');
25
30
 
26
31
  protected readonly computedClass = computed(() =>
27
32
  cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', this.class()),
@@ -1,3 +1,4 @@
1
+ export { AVATAR_CONTEXT, type AvatarContext, type AvatarImageStatus } from './avatar-context';
1
2
  export { AvatarFallback } from './avatar-fallback.component';
2
3
  export { AvatarImage } from './avatar-image.component';
3
4
  export { Avatar } from './avatar.component';