@ng-cn/core 1.0.16 → 1.0.17

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 (219) hide show
  1. package/package.json +1 -1
  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 +25 -9
  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 +9 -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/collapsible/collapsible-content.component.ts +1 -0
  37. package/src/app/lib/components/ui/collapsible/collapsible-trigger.component.ts +1 -0
  38. package/src/app/lib/components/ui/collapsible/collapsible.component.ts +1 -0
  39. package/src/app/lib/components/ui/combobox/combobox-content.component.ts +1 -0
  40. package/src/app/lib/components/ui/combobox/combobox-empty.component.ts +1 -0
  41. package/src/app/lib/components/ui/combobox/combobox-group.component.ts +1 -0
  42. package/src/app/lib/components/ui/combobox/combobox-input.component.ts +1 -0
  43. package/src/app/lib/components/ui/combobox/combobox-item.component.ts +1 -4
  44. package/src/app/lib/components/ui/combobox/combobox-list.component.ts +1 -1
  45. package/src/app/lib/components/ui/combobox/combobox-trigger.component.ts +1 -0
  46. package/src/app/lib/components/ui/combobox/combobox-value.component.ts +1 -0
  47. package/src/app/lib/components/ui/combobox/combobox.component.ts +1 -1
  48. package/src/app/lib/components/ui/command/command-dialog.component.ts +1 -0
  49. package/src/app/lib/components/ui/command/command-empty.component.ts +1 -0
  50. package/src/app/lib/components/ui/command/command-group.component.ts +1 -0
  51. package/src/app/lib/components/ui/command/command-input.component.ts +1 -0
  52. package/src/app/lib/components/ui/command/command-item.component.ts +2 -1
  53. package/src/app/lib/components/ui/command/command-list.component.ts +1 -0
  54. package/src/app/lib/components/ui/command/command-separator.component.ts +1 -0
  55. package/src/app/lib/components/ui/command/command-shortcut.component.ts +1 -0
  56. package/src/app/lib/components/ui/command/command.component.ts +1 -0
  57. package/src/app/lib/components/ui/context-menu/context-menu-checkbox-item.component.ts +1 -0
  58. package/src/app/lib/components/ui/context-menu/context-menu-content.component.ts +1 -0
  59. package/src/app/lib/components/ui/context-menu/context-menu-item.component.ts +2 -1
  60. package/src/app/lib/components/ui/context-menu/context-menu-label.component.ts +1 -0
  61. package/src/app/lib/components/ui/context-menu/context-menu-radio-group.component.ts +1 -0
  62. package/src/app/lib/components/ui/context-menu/context-menu-radio-item.component.ts +1 -0
  63. package/src/app/lib/components/ui/context-menu/context-menu-separator.component.ts +1 -0
  64. package/src/app/lib/components/ui/context-menu/context-menu-shortcut.component.ts +1 -0
  65. package/src/app/lib/components/ui/context-menu/context-menu-sub-content.component.ts +1 -0
  66. package/src/app/lib/components/ui/context-menu/context-menu-sub-trigger.component.ts +2 -1
  67. package/src/app/lib/components/ui/context-menu/context-menu-sub.component.ts +1 -0
  68. package/src/app/lib/components/ui/context-menu/context-menu-trigger.component.ts +1 -0
  69. package/src/app/lib/components/ui/context-menu/context-menu.component.ts +1 -0
  70. package/src/app/lib/components/ui/data-table/data-table-content.component.ts +1 -0
  71. package/src/app/lib/components/ui/data-table/data-table-pagination.component.ts +1 -0
  72. package/src/app/lib/components/ui/data-table/data-table-search.component.ts +1 -0
  73. package/src/app/lib/components/ui/data-table/data-table-toolbar.component.ts +1 -0
  74. package/src/app/lib/components/ui/data-table/data-table-view-options.component.ts +1 -0
  75. package/src/app/lib/components/ui/data-table/data-table.component.ts +1 -1
  76. package/src/app/lib/components/ui/dialog/dialog-close.component.ts +1 -0
  77. package/src/app/lib/components/ui/dialog/dialog-content.component.ts +20 -16
  78. package/src/app/lib/components/ui/dialog/dialog-description.component.ts +1 -0
  79. package/src/app/lib/components/ui/dialog/dialog-footer.component.ts +1 -0
  80. package/src/app/lib/components/ui/dialog/dialog-header.component.ts +1 -0
  81. package/src/app/lib/components/ui/dialog/dialog-title.component.ts +1 -0
  82. package/src/app/lib/components/ui/dialog/dialog-trigger.component.ts +1 -0
  83. package/src/app/lib/components/ui/dialog/dialog.component.ts +1 -0
  84. package/src/app/lib/components/ui/drawer/drawer-close.component.ts +1 -0
  85. package/src/app/lib/components/ui/drawer/drawer-content.component.ts +1 -0
  86. package/src/app/lib/components/ui/drawer/drawer-description.component.ts +1 -0
  87. package/src/app/lib/components/ui/drawer/drawer-footer.component.ts +1 -0
  88. package/src/app/lib/components/ui/drawer/drawer-header.component.ts +1 -0
  89. package/src/app/lib/components/ui/drawer/drawer-title.component.ts +1 -0
  90. package/src/app/lib/components/ui/drawer/drawer-trigger.component.ts +1 -0
  91. package/src/app/lib/components/ui/drawer/drawer.component.ts +4 -0
  92. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.component.ts +1 -0
  93. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-content.component.ts +1 -0
  94. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-group.component.ts +1 -0
  95. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-item.component.ts +1 -0
  96. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-label.component.ts +1 -0
  97. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.component.ts +1 -0
  98. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.component.ts +1 -0
  99. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-separator.component.ts +1 -0
  100. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.component.ts +1 -0
  101. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.component.ts +1 -0
  102. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.component.ts +2 -1
  103. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub.component.ts +1 -0
  104. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-trigger.component.ts +1 -0
  105. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu.component.ts +1 -0
  106. package/src/app/lib/components/ui/hover-card/hover-card-content.component.ts +1 -0
  107. package/src/app/lib/components/ui/hover-card/hover-card-trigger.component.ts +1 -0
  108. package/src/app/lib/components/ui/hover-card/hover-card.component.ts +1 -0
  109. package/src/app/lib/components/ui/input-otp/input-otp-group.component.ts +1 -0
  110. package/src/app/lib/components/ui/input-otp/input-otp-separator.component.ts +1 -0
  111. package/src/app/lib/components/ui/input-otp/input-otp-slot.component.ts +1 -0
  112. package/src/app/lib/components/ui/input-otp/input-otp.component.ts +1 -0
  113. package/src/app/lib/components/ui/kbd/kbd.component.ts +1 -0
  114. package/src/app/lib/components/ui/menubar/menubar-checkbox-item.component.ts +1 -0
  115. package/src/app/lib/components/ui/menubar/menubar-content.component.ts +1 -0
  116. package/src/app/lib/components/ui/menubar/menubar-item.component.ts +1 -0
  117. package/src/app/lib/components/ui/menubar/menubar-label.component.ts +1 -0
  118. package/src/app/lib/components/ui/menubar/menubar-menu.component.ts +1 -0
  119. package/src/app/lib/components/ui/menubar/menubar-radio-group.component.ts +1 -0
  120. package/src/app/lib/components/ui/menubar/menubar-radio-item.component.ts +1 -0
  121. package/src/app/lib/components/ui/menubar/menubar-separator.component.ts +1 -0
  122. package/src/app/lib/components/ui/menubar/menubar-shortcut.component.ts +1 -0
  123. package/src/app/lib/components/ui/menubar/menubar-sub-content.component.ts +1 -0
  124. package/src/app/lib/components/ui/menubar/menubar-sub-trigger.component.ts +1 -0
  125. package/src/app/lib/components/ui/menubar/menubar-sub.component.ts +1 -0
  126. package/src/app/lib/components/ui/menubar/menubar-trigger.component.ts +1 -0
  127. package/src/app/lib/components/ui/menubar/menubar.component.ts +1 -0
  128. package/src/app/lib/components/ui/native-select/native-select.component.ts +1 -1
  129. package/src/app/lib/components/ui/navigation-menu/navigation-menu-content.component.ts +1 -0
  130. package/src/app/lib/components/ui/navigation-menu/navigation-menu-indicator.component.ts +1 -0
  131. package/src/app/lib/components/ui/navigation-menu/navigation-menu-item.component.ts +1 -0
  132. package/src/app/lib/components/ui/navigation-menu/navigation-menu-link.component.ts +1 -0
  133. package/src/app/lib/components/ui/navigation-menu/navigation-menu-list.component.ts +1 -0
  134. package/src/app/lib/components/ui/navigation-menu/navigation-menu-trigger.component.ts +1 -0
  135. package/src/app/lib/components/ui/navigation-menu/navigation-menu-viewport.component.ts +1 -0
  136. package/src/app/lib/components/ui/navigation-menu/navigation-menu.component.ts +4 -0
  137. package/src/app/lib/components/ui/pagination/pagination-content.component.ts +1 -0
  138. package/src/app/lib/components/ui/pagination/pagination-ellipsis.component.ts +1 -0
  139. package/src/app/lib/components/ui/pagination/pagination-item.component.ts +1 -0
  140. package/src/app/lib/components/ui/pagination/pagination-link.component.ts +1 -0
  141. package/src/app/lib/components/ui/pagination/pagination-next.component.ts +1 -0
  142. package/src/app/lib/components/ui/pagination/pagination-previous.component.ts +1 -0
  143. package/src/app/lib/components/ui/pagination/pagination.component.ts +1 -0
  144. package/src/app/lib/components/ui/popover/popover-anchor.component.ts +1 -0
  145. package/src/app/lib/components/ui/popover/popover-content.component.ts +1 -0
  146. package/src/app/lib/components/ui/popover/popover-trigger.component.ts +1 -0
  147. package/src/app/lib/components/ui/popover/popover.component.ts +1 -0
  148. package/src/app/lib/components/ui/resizable/resizable-handle.component.ts +1 -0
  149. package/src/app/lib/components/ui/resizable/resizable-panel-group.component.ts +1 -0
  150. package/src/app/lib/components/ui/resizable/resizable-panel.component.ts +1 -0
  151. package/src/app/lib/components/ui/scroll-area/scroll-area.component.ts +1 -0
  152. package/src/app/lib/components/ui/scroll-area/scroll-bar.component.ts +1 -0
  153. package/src/app/lib/components/ui/select/select-content.component.ts +3 -2
  154. package/src/app/lib/components/ui/sheet/sheet-close.component.ts +1 -0
  155. package/src/app/lib/components/ui/sheet/sheet-content.component.ts +1 -0
  156. package/src/app/lib/components/ui/sheet/sheet-description.component.ts +1 -0
  157. package/src/app/lib/components/ui/sheet/sheet-footer.component.ts +1 -0
  158. package/src/app/lib/components/ui/sheet/sheet-header.component.ts +1 -0
  159. package/src/app/lib/components/ui/sheet/sheet-title.component.ts +1 -0
  160. package/src/app/lib/components/ui/sheet/sheet-trigger.component.ts +1 -0
  161. package/src/app/lib/components/ui/sheet/sheet.component.ts +4 -0
  162. package/src/app/lib/components/ui/sidebar/sidebar-content.component.ts +1 -0
  163. package/src/app/lib/components/ui/sidebar/sidebar-footer.component.ts +1 -0
  164. package/src/app/lib/components/ui/sidebar/sidebar-group-action.component.ts +1 -0
  165. package/src/app/lib/components/ui/sidebar/sidebar-group-content.component.ts +1 -0
  166. package/src/app/lib/components/ui/sidebar/sidebar-group-label.component.ts +1 -0
  167. package/src/app/lib/components/ui/sidebar/sidebar-group.component.ts +1 -0
  168. package/src/app/lib/components/ui/sidebar/sidebar-header.component.ts +1 -0
  169. package/src/app/lib/components/ui/sidebar/sidebar-input.component.ts +1 -0
  170. package/src/app/lib/components/ui/sidebar/sidebar-inset.component.ts +1 -0
  171. package/src/app/lib/components/ui/sidebar/sidebar-menu-action.component.ts +1 -0
  172. package/src/app/lib/components/ui/sidebar/sidebar-menu-badge.component.ts +1 -0
  173. package/src/app/lib/components/ui/sidebar/sidebar-menu-button.component.ts +1 -0
  174. package/src/app/lib/components/ui/sidebar/sidebar-menu-item.component.ts +1 -0
  175. package/src/app/lib/components/ui/sidebar/sidebar-menu-skeleton.component.ts +1 -0
  176. package/src/app/lib/components/ui/sidebar/sidebar-menu-sub-button.component.ts +1 -0
  177. package/src/app/lib/components/ui/sidebar/sidebar-menu-sub-item.component.ts +1 -0
  178. package/src/app/lib/components/ui/sidebar/sidebar-menu-sub.component.ts +1 -0
  179. package/src/app/lib/components/ui/sidebar/sidebar-menu.component.ts +1 -0
  180. package/src/app/lib/components/ui/sidebar/sidebar-provider.component.ts +1 -0
  181. package/src/app/lib/components/ui/sidebar/sidebar-rail.component.ts +1 -0
  182. package/src/app/lib/components/ui/sidebar/sidebar-separator.component.ts +1 -0
  183. package/src/app/lib/components/ui/sidebar/sidebar-trigger.component.ts +1 -0
  184. package/src/app/lib/components/ui/sidebar/sidebar.component.ts +1 -0
  185. package/src/app/lib/components/ui/spinner/spinner.component.ts +1 -0
  186. package/src/app/lib/components/ui/table/table-body.component.ts +1 -0
  187. package/src/app/lib/components/ui/table/table-caption.component.ts +1 -0
  188. package/src/app/lib/components/ui/table/table-cell.component.ts +1 -0
  189. package/src/app/lib/components/ui/table/table-footer.component.ts +1 -0
  190. package/src/app/lib/components/ui/table/table-head.component.ts +1 -0
  191. package/src/app/lib/components/ui/table/table-header.component.ts +1 -0
  192. package/src/app/lib/components/ui/table/table-row.component.ts +1 -0
  193. package/src/app/lib/components/ui/table/table.component.ts +1 -0
  194. package/src/app/lib/components/ui/tabs/tabs-content.component.ts +3 -6
  195. package/src/app/lib/components/ui/tabs/tabs-list.component.ts +1 -0
  196. package/src/app/lib/components/ui/tabs/tabs-trigger.component.ts +1 -0
  197. package/src/app/lib/components/ui/tabs/tabs.component.ts +1 -0
  198. package/src/app/lib/components/ui/toast/toast-action.component.ts +1 -0
  199. package/src/app/lib/components/ui/toast/toast-description.component.ts +1 -0
  200. package/src/app/lib/components/ui/toast/toast-title.component.ts +1 -0
  201. package/src/app/lib/components/ui/toast/toast.component.ts +1 -0
  202. package/src/app/lib/components/ui/toast/toaster.component.ts +1 -0
  203. package/src/app/lib/components/ui/toggle/toggle-variants.ts +1 -1
  204. package/src/app/lib/components/ui/tooltip/tooltip-content.component.ts +1 -0
  205. package/src/app/lib/components/ui/tooltip/tooltip-provider.component.ts +4 -0
  206. package/src/app/lib/components/ui/tooltip/tooltip-trigger.component.ts +1 -0
  207. package/src/app/lib/components/ui/tooltip/tooltip.component.ts +1 -0
  208. package/src/app/lib/components/ui/typography/typography-blockquote.component.ts +1 -0
  209. package/src/app/lib/components/ui/typography/typography-h1.component.ts +1 -0
  210. package/src/app/lib/components/ui/typography/typography-h2.component.ts +1 -0
  211. package/src/app/lib/components/ui/typography/typography-h3.component.ts +1 -0
  212. package/src/app/lib/components/ui/typography/typography-h4.component.ts +1 -0
  213. package/src/app/lib/components/ui/typography/typography-inline-code.component.ts +1 -0
  214. package/src/app/lib/components/ui/typography/typography-large.component.ts +1 -0
  215. package/src/app/lib/components/ui/typography/typography-lead.component.ts +1 -0
  216. package/src/app/lib/components/ui/typography/typography-list.component.ts +1 -0
  217. package/src/app/lib/components/ui/typography/typography-muted.component.ts +1 -0
  218. package/src/app/lib/components/ui/typography/typography-p.component.ts +1 -0
  219. 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.17",
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",
@@ -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
  })
@@ -2,6 +2,7 @@ import { cn } 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,
@@ -9,9 +10,13 @@ import {
9
10
  HostListener,
10
11
  inject,
11
12
  input,
13
+ signal,
12
14
  } from '@angular/core';
13
15
  import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
14
16
 
17
+ /** Animation duration in ms — must match Tailwind's duration-200 */
18
+ const EXIT_ANIMATION_MS = 200;
19
+
15
20
  /**
16
21
  * AlertDialogContent component - the modal content of the alert dialog.
17
22
  * Matches shadcn/ui React AlertDialogContent exactly.
@@ -26,9 +31,13 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
26
31
  selector: 'AlertDialogContent',
27
32
  imports: [FocusTrapDirective],
28
33
  template: `
29
- @if (context.isOpen()) {
34
+ @if (shouldRender()) {
30
35
  <!-- Overlay - does NOT close on click -->
31
- <div class="fixed inset-0 z-50 bg-black/80" aria-hidden="true"></div>
36
+ <div
37
+ 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"
38
+ [attr.data-state]="context.isOpen() ? 'open' : 'closed'"
39
+ aria-hidden="true"
40
+ ></div>
32
41
  <!-- Content Dialog -->
33
42
  <div
34
43
  hlmFocusTrap
@@ -37,6 +46,7 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
37
46
  [restoreFocus]="false"
38
47
  [initialFocus]="'[data-slot=alert-dialog-cancel]'"
39
48
  [class]="computedClass()"
49
+ [attr.data-state]="context.isOpen() ? 'open' : 'closed'"
40
50
  [attr.id]="context.contentId"
41
51
  [attr.aria-labelledby]="context.titleId"
42
52
  [attr.aria-describedby]="context.descriptionId"
@@ -48,35 +58,44 @@ import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
48
58
  }
49
59
  `,
50
60
  host: {
61
+ 'attr.data-slot': '"alert-dialog-content"',
51
62
  class: 'contents',
52
63
  },
53
64
  changeDetection: ChangeDetectionStrategy.OnPush,
54
65
  })
55
66
  export class AlertDialogContent {
56
67
  constructor() {
57
- // Handle body scroll lock based on open state
58
68
  effect(() => {
59
69
  const isOpen = this.context.isOpen();
70
+ this._cdr.markForCheck();
71
+
60
72
  if (isOpen) {
73
+ this.shouldRender.set(true);
61
74
  this.lockBodyScroll();
62
75
  } else {
63
76
  this.unlockBodyScroll();
77
+ if (this.shouldRender()) {
78
+ setTimeout(() => {
79
+ this.shouldRender.set(false);
80
+ this._cdr.markForCheck();
81
+ }, EXIT_ANIMATION_MS);
82
+ }
64
83
  }
65
84
  });
66
85
 
67
- // Cleanup on destroy
68
86
  this._destroyRef.onDestroy(() => {
69
87
  this.unlockBodyScroll();
70
88
  this.restoreFocus();
71
89
  });
72
90
  }
73
91
 
74
- /** Additional CSS classes */
75
92
  readonly class = input<string>('');
76
93
 
77
94
  private readonly _destroyRef = inject(DestroyRef);
95
+ private readonly _cdr = inject(ChangeDetectorRef);
78
96
 
79
97
  protected readonly context = inject(ALERT_DIALOG_CONTEXT);
98
+ protected readonly shouldRender = signal(false);
80
99
 
81
100
  protected readonly computedClass = computed(() =>
82
101
  cn(
@@ -91,7 +110,6 @@ export class AlertDialogContent {
91
110
  ),
92
111
  );
93
112
 
94
- /** Previous body overflow for restoration */
95
113
  private previousBodyOverflow = '';
96
114
 
97
115
  @HostListener('document:keydown.escape')
@@ -119,9 +137,7 @@ export class AlertDialogContent {
119
137
  private restoreFocus(): void {
120
138
  const triggerEl = this.context.getTriggerElement();
121
139
  if (triggerEl) {
122
- setTimeout(() => {
123
- triggerEl.focus();
124
- }, 0);
140
+ setTimeout(() => triggerEl.focus(), 0);
125
141
  }
126
142
  }
127
143
  }
@@ -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';
@@ -1,30 +1,25 @@
1
- import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
2
  import { AvatarFallback } from './avatar-fallback.component';
3
+ import { AvatarImage } from './avatar-image.component';
3
4
  import { Avatar } from './avatar.component';
4
5
 
5
6
  /**
6
- * Avatar component that handles image loading and fallback display.
7
- * This is a convenience component that combines Avatar, AvatarImage, and AvatarFallback.
7
+ * Convenience component combining Avatar + AvatarImage + AvatarFallback.
8
+ * Error handling is automatic via the shared Avatar context.
8
9
  *
9
10
  * @example
10
- * <ui-avatar
11
- * src="/avatar.png"
12
- * alt="John Doe"
13
- * fallback="JD"
14
- * />
11
+ * <ui-avatar src="/avatar.png" alt="John Doe" fallback="JD" />
15
12
  */
16
13
  @Component({
17
14
  selector: 'ui-avatar',
18
15
  changeDetection: ChangeDetectionStrategy.OnPush,
19
- imports: [Avatar, AvatarFallback],
16
+ imports: [Avatar, AvatarImage, AvatarFallback],
20
17
  template: `
21
18
  <Avatar [class]="class()">
22
- @if (src() && !imageError()) {
23
- <img AvatarImage [src]="src()" [alt]="alt()" (error)="onImageError()" />
24
- }
25
- @if (!src() || imageError()) {
26
- <AvatarFallback>{{ fallback() }}</AvatarFallback>
19
+ @if (src()) {
20
+ <AvatarImage [src]="src()" [alt]="alt()" />
27
21
  }
22
+ <AvatarFallback>{{ fallback() }}</AvatarFallback>
28
23
  </Avatar>
29
24
  `,
30
25
  })
@@ -33,10 +28,4 @@ export class UiAvatar {
33
28
  readonly alt = input<string>('');
34
29
  readonly fallback = input<string>('');
35
30
  readonly class = input<string>('');
36
-
37
- protected readonly imageError = signal(false);
38
-
39
- protected onImageError(): void {
40
- this.imageError.set(true);
41
- }
42
31
  }