@ng-cn/core 1.0.17 → 1.0.20

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 (111) hide show
  1. package/package.json +6 -5
  2. package/src/app/lib/components/ui/alert-dialog/alert-dialog-content.component.ts +22 -21
  3. package/src/app/lib/components/ui/avatar/ui-avatar.component.ts +4 -0
  4. package/src/app/lib/components/ui/calendar/calendar.component.ts +70 -13
  5. package/src/app/lib/components/ui/carousel/carousel-content.component.ts +1 -0
  6. package/src/app/lib/components/ui/carousel/carousel-item.component.ts +1 -0
  7. package/src/app/lib/components/ui/carousel/carousel-next.component.ts +1 -0
  8. package/src/app/lib/components/ui/carousel/carousel-previous.component.ts +1 -0
  9. package/src/app/lib/components/ui/carousel/carousel.component.ts +1 -0
  10. package/src/app/lib/components/ui/chart/chart-container.component.ts +1 -0
  11. package/src/app/lib/components/ui/chart/chart-legend-content.component.ts +1 -0
  12. package/src/app/lib/components/ui/chart/chart-legend.component.ts +5 -5
  13. package/src/app/lib/components/ui/chart/chart-tooltip-content.component.ts +5 -5
  14. package/src/app/lib/components/ui/chart/chart-tooltip.component.ts +5 -5
  15. package/src/app/lib/components/ui/chart/chart.component.ts +1 -0
  16. package/src/app/lib/components/ui/checkbox/checkbox.component.ts +1 -1
  17. package/src/app/lib/components/ui/collapsible/collapsible-content.component.ts +2 -1
  18. package/src/app/lib/components/ui/collapsible/collapsible-context.ts +1 -0
  19. package/src/app/lib/components/ui/collapsible/collapsible-trigger.component.ts +1 -0
  20. package/src/app/lib/components/ui/collapsible/collapsible.component.ts +3 -0
  21. package/src/app/lib/components/ui/context-menu/context-menu-content.component.ts +49 -17
  22. package/src/app/lib/components/ui/context-menu/context-menu-sub-content.component.ts +2 -0
  23. package/src/app/lib/components/ui/context-menu/context-menu-sub-trigger.component.ts +30 -1
  24. package/src/app/lib/components/ui/context-menu/context-menu-sub.component.ts +3 -0
  25. package/src/app/lib/components/ui/country-selector/country-data.ts +63 -0
  26. package/src/app/lib/components/ui/country-selector/country-selector.component.ts +199 -0
  27. package/src/app/lib/components/ui/country-selector/index.ts +2 -0
  28. package/src/app/lib/components/ui/date-picker/date-picker.component.ts +48 -5
  29. package/src/app/lib/components/ui/dialog/dialog-content.component.ts +27 -20
  30. package/src/app/lib/components/ui/direction/direction-context.ts +9 -0
  31. package/src/app/lib/components/ui/direction/direction.component.ts +48 -0
  32. package/src/app/lib/components/ui/direction/index.ts +2 -0
  33. package/src/app/lib/components/ui/drawer/drawer-content.component.ts +44 -0
  34. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-content.component.ts +25 -23
  35. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.component.ts +1 -0
  36. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.component.ts +2 -0
  37. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.component.ts +28 -2
  38. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub.component.ts +3 -0
  39. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-trigger.component.ts +25 -0
  40. package/src/app/lib/components/ui/empty/empty-action.component.ts +1 -0
  41. package/src/app/lib/components/ui/empty/empty-description.component.ts +1 -0
  42. package/src/app/lib/components/ui/empty/empty-icon.component.ts +2 -1
  43. package/src/app/lib/components/ui/empty/empty-title.component.ts +1 -0
  44. package/src/app/lib/components/ui/empty/empty.component.ts +1 -0
  45. package/src/app/lib/components/ui/field/field-content.component.ts +34 -0
  46. package/src/app/lib/components/ui/field/field-description.component.ts +35 -0
  47. package/src/app/lib/components/ui/field/field-error.component.ts +48 -0
  48. package/src/app/lib/components/ui/field/field-group.component.ts +34 -0
  49. package/src/app/lib/components/ui/field/field-label.component.ts +46 -0
  50. package/src/app/lib/components/ui/field/field-legend.component.ts +41 -0
  51. package/src/app/lib/components/ui/field/field-separator.component.ts +49 -0
  52. package/src/app/lib/components/ui/field/field-set.component.ts +37 -0
  53. package/src/app/lib/components/ui/field/field-title.component.ts +30 -0
  54. package/src/app/lib/components/ui/field/field.component.ts +66 -0
  55. package/src/app/lib/components/ui/field/index.ts +15 -0
  56. package/src/app/lib/components/ui/form/form-description.component.ts +2 -2
  57. package/src/app/lib/components/ui/hover-card/hover-card-content.component.ts +108 -60
  58. package/src/app/lib/components/ui/hover-card/hover-card-context.ts +4 -2
  59. package/src/app/lib/components/ui/hover-card/hover-card-trigger.component.ts +5 -3
  60. package/src/app/lib/components/ui/hover-card/hover-card.component.ts +8 -3
  61. package/src/app/lib/components/ui/input-group/input-group-addon.component.ts +1 -0
  62. package/src/app/lib/components/ui/input-group/input-group-input.component.ts +1 -0
  63. package/src/app/lib/components/ui/input-group/input-group.component.ts +1 -0
  64. package/src/app/lib/components/ui/item/index.ts +21 -0
  65. package/src/app/lib/components/ui/item/item-actions.component.ts +29 -0
  66. package/src/app/lib/components/ui/item/item-content.component.ts +31 -0
  67. package/src/app/lib/components/ui/item/item-description.component.ts +30 -0
  68. package/src/app/lib/components/ui/item/item-footer.component.ts +30 -0
  69. package/src/app/lib/components/ui/item/item-group.component.ts +32 -0
  70. package/src/app/lib/components/ui/item/item-header.component.ts +30 -0
  71. package/src/app/lib/components/ui/item/item-media.component.ts +63 -0
  72. package/src/app/lib/components/ui/item/item-separator.component.ts +33 -0
  73. package/src/app/lib/components/ui/item/item-title.component.ts +27 -0
  74. package/src/app/lib/components/ui/item/item.component.ts +77 -0
  75. package/src/app/lib/components/ui/menubar/menubar-content.component.ts +1 -1
  76. package/src/app/lib/components/ui/navigation-menu/navigation-menu-content.component.ts +7 -1
  77. package/src/app/lib/components/ui/navigation-menu/navigation-menu-context.ts +14 -0
  78. package/src/app/lib/components/ui/navigation-menu/navigation-menu-item.component.ts +9 -4
  79. package/src/app/lib/components/ui/navigation-menu/navigation-menu-trigger.component.ts +69 -2
  80. package/src/app/lib/components/ui/navigation-menu/navigation-menu.component.ts +32 -4
  81. package/src/app/lib/components/ui/pagination/pagination.component.ts +3 -1
  82. package/src/app/lib/components/ui/phone-input/index.ts +1 -0
  83. package/src/app/lib/components/ui/phone-input/phone-input.component.ts +169 -0
  84. package/src/app/lib/components/ui/popover/popover-content.component.ts +11 -0
  85. package/src/app/lib/components/ui/popover/popover-context.ts +2 -0
  86. package/src/app/lib/components/ui/popover/popover.component.ts +4 -0
  87. package/src/app/lib/components/ui/progress/progress.component.ts +1 -2
  88. package/src/app/lib/components/ui/resizable/resizable-handle.component.ts +2 -2
  89. package/src/app/lib/components/ui/scroll-area/scroll-area.component.ts +7 -6
  90. package/src/app/lib/components/ui/segmented/segmented-item.component.ts +1 -0
  91. package/src/app/lib/components/ui/segmented/segmented.component.ts +1 -0
  92. package/src/app/lib/components/ui/select/select-content.component.ts +35 -15
  93. package/src/app/lib/components/ui/select/select-context.ts +10 -0
  94. package/src/app/lib/components/ui/select/select-item.component.ts +25 -7
  95. package/src/app/lib/components/ui/select/select-trigger.component.ts +6 -13
  96. package/src/app/lib/components/ui/select/select.component.ts +46 -0
  97. package/src/app/lib/components/ui/sheet/sheet-content.component.ts +23 -6
  98. package/src/app/lib/components/ui/slider/slider.component.ts +2 -2
  99. package/src/app/lib/components/ui/sonner/index.ts +2 -0
  100. package/src/app/lib/components/ui/sonner/sonner.component.ts +70 -0
  101. package/src/app/lib/components/ui/switch/switch.component.ts +1 -14
  102. package/src/app/lib/components/ui/tabs/tabs-list.component.ts +20 -0
  103. package/src/app/lib/components/ui/tabs/tabs-trigger.component.ts +0 -1
  104. package/src/app/lib/components/ui/textarea/textarea.component.ts +110 -10
  105. package/src/app/lib/components/ui/toast/toast.service.ts +1 -1
  106. package/src/app/lib/components/ui/toggle/toggle.component.ts +12 -6
  107. package/src/app/lib/components/ui/tooltip/tooltip-content.component.ts +141 -17
  108. package/src/app/lib/components/ui/tooltip/tooltip-context.ts +3 -1
  109. package/src/app/lib/components/ui/tooltip/tooltip-provider.component.ts +1 -1
  110. package/src/app/lib/components/ui/tooltip/tooltip-trigger.component.ts +5 -2
  111. package/src/app/lib/components/ui/tooltip/tooltip.component.ts +3 -1
@@ -0,0 +1,30 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+
4
+ /**
5
+ * FieldTitle component - title text inside FieldContent.
6
+ *
7
+ * @example
8
+ * <FieldContent>
9
+ * <FieldTitle>Two-factor authentication</FieldTitle>
10
+ * <FieldDescription>Add an extra layer of security.</FieldDescription>
11
+ * </FieldContent>
12
+ */
13
+ @Component({
14
+ selector: 'FieldTitle',
15
+ template: `<ng-content />`,
16
+ host: {
17
+ 'attr.data-slot': '"field-title"',
18
+ '[class]': 'computedClass()',
19
+ },
20
+ changeDetection: ChangeDetectionStrategy.OnPush,
21
+ })
22
+ export class FieldTitle {
23
+ /** Additional CSS classes to apply */
24
+ readonly class = input<string>('');
25
+
26
+ /** Computed class combining base styles and custom classes */
27
+ protected readonly computedClass = computed(() =>
28
+ cn('flex w-fit items-center gap-2 text-sm leading-snug font-medium', this.class()),
29
+ );
30
+ }
@@ -0,0 +1,66 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ /**
6
+ * Field variants using class-variance-authority.
7
+ * Matches shadcn/ui React field exactly.
8
+ */
9
+ export const fieldVariants = cva('group/field flex w-full gap-3 data-[invalid=true]:text-destructive', {
10
+ variants: {
11
+ orientation: {
12
+ vertical: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto',
13
+ horizontal: 'flex-row items-center [&>[data-slot=field-label]]:flex-auto',
14
+ responsive:
15
+ 'flex-col [&>*]:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ orientation: 'vertical',
20
+ },
21
+ });
22
+
23
+ export type FieldVariants = VariantProps<typeof fieldVariants>;
24
+ export type FieldOrientation = 'vertical' | 'horizontal' | 'responsive';
25
+
26
+ /**
27
+ * Field component - wraps a single form field with its label,
28
+ * control, description and error message.
29
+ *
30
+ * @example
31
+ * <!-- Vertical (default) -->
32
+ * <Field>
33
+ * <FieldLabel htmlFor="email">Email</FieldLabel>
34
+ * <input Input id="email" type="email" />
35
+ * <FieldDescription>We never share your email.</FieldDescription>
36
+ * </Field>
37
+ *
38
+ * <!-- Horizontal -->
39
+ * <Field orientation="horizontal">
40
+ * <FieldLabel htmlFor="newsletter">Subscribe to newsletter</FieldLabel>
41
+ * <Switch id="newsletter" />
42
+ * </Field>
43
+ */
44
+ @Component({
45
+ selector: 'Field',
46
+ template: `<ng-content />`,
47
+ host: {
48
+ 'attr.data-slot': '"field"',
49
+ role: 'group',
50
+ '[attr.data-orientation]': 'orientation()',
51
+ '[class]': 'computedClass()',
52
+ },
53
+ changeDetection: ChangeDetectionStrategy.OnPush,
54
+ })
55
+ export class Field {
56
+ /** Layout orientation of the field */
57
+ readonly orientation = input<FieldOrientation>('vertical');
58
+
59
+ /** Additional CSS classes to apply */
60
+ readonly class = input<string>('');
61
+
62
+ /** Computed class combining base styles, variants and custom classes */
63
+ protected readonly computedClass = computed(() =>
64
+ cn(fieldVariants({ orientation: this.orientation() }), this.class()),
65
+ );
66
+ }
@@ -0,0 +1,15 @@
1
+ export { FieldContent } from './field-content.component';
2
+ export { FieldDescription } from './field-description.component';
3
+ export { FieldError } from './field-error.component';
4
+ export { FieldGroup } from './field-group.component';
5
+ export { FieldLabel } from './field-label.component';
6
+ export { FieldLegend, type FieldLegendVariant } from './field-legend.component';
7
+ export { FieldSeparator } from './field-separator.component';
8
+ export { FieldSet } from './field-set.component';
9
+ export { FieldTitle } from './field-title.component';
10
+ export {
11
+ Field,
12
+ fieldVariants,
13
+ type FieldOrientation,
14
+ type FieldVariants,
15
+ } from './field.component';
@@ -14,7 +14,7 @@ import { FORM_FIELD_CONTEXT } from './form-context';
14
14
  host: {
15
15
  '[class]': 'computedClass()',
16
16
  '[attr.id]': 'fieldContext?.formDescriptionId()',
17
- 'data-slot': 'form-description',
17
+ 'attr.data-slot': '"form-description"',
18
18
  },
19
19
  changeDetection: ChangeDetectionStrategy.OnPush,
20
20
  })
@@ -26,6 +26,6 @@ export class FormDescription {
26
26
 
27
27
  /** Computed class combining base styles and custom classes */
28
28
  protected readonly computedClass = computed(() =>
29
- cn('text-muted-foreground text-[0.8rem]', this.class()),
29
+ cn('text-muted-foreground text-[length:var(--font-size-description)]', this.class()),
30
30
  );
31
31
  }
@@ -1,12 +1,16 @@
1
- import { cn, Presence } from '@/lib/utils';
1
+ import { Align, cn, computePosition, getTransformOrigin, Presence, Side } from '@/lib/utils';
2
2
  import {
3
+ afterNextRender,
3
4
  ChangeDetectionStrategy,
4
5
  Component,
5
6
  computed,
7
+ effect,
6
8
  ElementRef,
7
9
  inject,
10
+ Injector,
8
11
  input,
9
12
  OnDestroy,
13
+ signal,
10
14
  } from '@angular/core';
11
15
  import { HOVER_CARD_CONTEXT, HoverCardAlign, HoverCardSide } from './hover-card-context';
12
16
 
@@ -38,31 +42,6 @@ export interface HoverCardContentProps {
38
42
  * HoverCardContent displays the preview content. It stays open when
39
43
  * hovered, allowing users to interact with the content.
40
44
  *
41
- * ## Features
42
- * - Stays open when content is hovered
43
- * - Configurable side and alignment
44
- * - Smooth animations
45
- * - Escape key to dismiss
46
- *
47
- * ## Accessibility
48
- * - `role="dialog"` on the content
49
- * - Focusable content items
50
- * - Escape returns focus to trigger
51
- *
52
- * @example Basic usage
53
- * ```html
54
- * <HoverCardContent>
55
- * <p>Preview content</p>
56
- * </HoverCardContent>
57
- * ```
58
- *
59
- * @example With positioning
60
- * ```html
61
- * <HoverCardContent side="right" align="start">
62
- * <p>Right-aligned content</p>
63
- * </HoverCardContent>
64
- * ```
65
- *
66
45
  * @data-attributes
67
46
  * - `data-state` - 'open' | 'closed'
68
47
  * - `data-side` - 'top' | 'right' | 'bottom' | 'left'
@@ -78,9 +57,10 @@ export interface HoverCardContentProps {
78
57
  [attr.aria-modal]="false"
79
58
  tabindex="-1"
80
59
  [class]="computedClass()"
60
+ [style]="positionStyles()"
81
61
  [attr.data-state]="state()"
82
- [attr.data-side]="side()"
83
- [attr.data-align]="align()"
62
+ [attr.data-side]="computedSide()"
63
+ [attr.data-align]="computedAlign()"
84
64
  data-slot="hover-card-content"
85
65
  (mouseenter)="onMouseEnter()"
86
66
  (mouseleave)="onMouseLeave()"
@@ -99,6 +79,25 @@ export interface HoverCardContentProps {
99
79
  changeDetection: ChangeDetectionStrategy.OnPush,
100
80
  })
101
81
  export class HoverCardContent implements OnDestroy {
82
+ constructor() {
83
+ effect(() => {
84
+ const isOpen = this.context.open();
85
+ if (isOpen) {
86
+ this.isPositioned.set(false);
87
+ afterNextRender(
88
+ () => {
89
+ this.schedulePositionUpdate();
90
+ },
91
+ { injector: this._injector },
92
+ );
93
+ } else {
94
+ this.cancelScheduledPositionUpdate();
95
+ this.isPositioned.set(false);
96
+ this.positionStyles.set({ position: 'fixed', top: '-9999px', left: '-9999px' });
97
+ }
98
+ });
99
+ }
100
+
102
101
  /** The preferred side of the trigger to render against */
103
102
  readonly side = input<HoverCardSide>('bottom');
104
103
  /** The distance in pixels from the trigger */
@@ -109,80 +108,129 @@ export class HoverCardContent implements OnDestroy {
109
108
  readonly class = input<string>('');
110
109
 
111
110
  private readonly _elementRef = inject(ElementRef<HTMLElement>);
111
+ private readonly _injector = inject(Injector);
112
112
 
113
113
  protected readonly context = inject(HOVER_CARD_CONTEXT);
114
114
 
115
- protected readonly computedClass = computed(() => {
116
- const sideClasses = {
117
- top: 'bottom-full mb-2',
118
- bottom: 'top-full mt-2',
119
- left: 'right-full mr-2',
120
- right: 'left-full ml-2',
121
- };
122
-
123
- const alignClasses = {
124
- start: 'left-0',
125
- center: 'left-1/2 -translate-x-1/2',
126
- end: 'right-0',
127
- };
128
-
129
- return cn(
130
- 'absolute z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none',
115
+ protected readonly computedClass = computed(() =>
116
+ cn(
117
+ 'fixed z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none',
131
118
  'data-[state=open]:animate-in data-[state=closed]:animate-out',
132
119
  'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
133
120
  'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
134
121
  'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
135
122
  'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
136
- sideClasses[this.side()],
137
- this.side() === 'top' || this.side() === 'bottom' ? alignClasses[this.align()] : '',
123
+ !this.isPositioned() && 'pointer-events-none opacity-0',
138
124
  this.class(),
139
- );
125
+ ),
126
+ );
127
+
128
+ protected readonly positionStyles = signal<Record<string, string>>({
129
+ position: 'fixed',
130
+ top: '-9999px',
131
+ left: '-9999px',
140
132
  });
133
+ protected readonly isPositioned = signal(false);
134
+ protected readonly computedSide = signal<Side>('bottom');
135
+ protected readonly computedAlign = signal<Align>('center');
141
136
 
142
- private closeTimeout: ReturnType<typeof setTimeout> | null = null;
143
137
  /** Current state: open or closed */
144
138
  protected readonly state = computed<HoverCardContentState>(() =>
145
139
  this.context.open() ? 'open' : 'closed',
146
140
  );
147
141
 
142
+ private closeTimeout: ReturnType<typeof setTimeout> | null = null;
143
+ private positionFrameId: number | null = null;
144
+
148
145
  ngOnDestroy(): void {
149
- this.clearTimeout();
146
+ this.clearCloseTimeout();
147
+ this.cancelScheduledPositionUpdate();
150
148
  }
151
149
 
152
150
  onMouseEnter(): void {
153
- this.clearTimeout();
151
+ this.clearCloseTimeout();
154
152
  }
155
153
  onMouseLeave(): void {
156
154
  this.closeTimeout = setTimeout(() => {
157
155
  this.context.setOpen(false);
158
- }, this.context.closeDelay);
156
+ }, this.context.closeDelay());
159
157
  }
160
158
  onFocusIn(): void {
161
- this.clearTimeout();
159
+ this.clearCloseTimeout();
162
160
  }
163
161
  onFocusOut(event: FocusEvent): void {
164
162
  const relatedTarget = event.relatedTarget as HTMLElement | null;
165
- const trigger = this._elementRef.nativeElement.parentElement?.querySelector('[data-state]');
163
+ const trigger = this.context.triggerRef();
166
164
 
167
- // Check if focus moved to trigger or stayed within content
168
165
  if (relatedTarget && (trigger === relatedTarget || trigger?.contains(relatedTarget))) {
169
166
  return;
170
167
  }
171
168
 
172
169
  this.closeTimeout = setTimeout(() => {
173
170
  this.context.setOpen(false);
174
- }, this.context.closeDelay);
171
+ }, this.context.closeDelay());
175
172
  }
176
173
  onEscape(): void {
177
174
  this.context.setOpen(false);
178
- // Return focus to trigger
179
- const trigger = this._elementRef.nativeElement.parentElement?.querySelector(
180
- '[data-state]',
175
+ this.context.triggerRef()?.focus();
176
+ }
177
+
178
+ private schedulePositionUpdate(): void {
179
+ this.cancelScheduledPositionUpdate();
180
+ this.positionFrameId = requestAnimationFrame(() => {
181
+ this.updatePosition();
182
+ this.positionFrameId = requestAnimationFrame(() => {
183
+ this.updatePosition();
184
+ this.positionFrameId = null;
185
+ });
186
+ });
187
+ }
188
+ private cancelScheduledPositionUpdate(): void {
189
+ if (this.positionFrameId !== null) {
190
+ cancelAnimationFrame(this.positionFrameId);
191
+ this.positionFrameId = null;
192
+ }
193
+ }
194
+ private updatePosition(): void {
195
+ const triggerElement = this.context.triggerRef();
196
+ const contentElement = this._elementRef.nativeElement.querySelector(
197
+ '[role="dialog"]',
181
198
  ) as HTMLElement;
182
- trigger?.focus();
199
+
200
+ if (!triggerElement || !contentElement) return;
201
+
202
+ const triggerRect = triggerElement.getBoundingClientRect();
203
+ const contentRect = contentElement.getBoundingClientRect();
204
+ const overlayWidth = Math.round(contentRect.width || 256);
205
+ const overlayHeight = Math.round(contentRect.height || 100);
206
+
207
+ const result = computePosition(
208
+ triggerRect,
209
+ { width: overlayWidth, height: overlayHeight },
210
+ {
211
+ side: this.side(),
212
+ align: this.align(),
213
+ sideOffset: this.sideOffset(),
214
+ alignOffset: 0,
215
+ avoidCollisions: true,
216
+ collisionPadding: 8,
217
+ },
218
+ );
219
+
220
+ this.computedSide.set(result.side);
221
+ this.computedAlign.set(result.align);
222
+
223
+ const transformOrigin = getTransformOrigin(result.side, result.align);
224
+ this.positionStyles.set({
225
+ position: 'fixed',
226
+ top: result.styles.top || '',
227
+ left: result.styles.left || '',
228
+ '--radix-hover-card-content-transform-origin': transformOrigin,
229
+ });
230
+ this.isPositioned.set(true);
183
231
  }
184
232
 
185
- private clearTimeout(): void {
233
+ private clearCloseTimeout(): void {
186
234
  if (this.closeTimeout) {
187
235
  clearTimeout(this.closeTimeout);
188
236
  this.closeTimeout = null;
@@ -9,9 +9,11 @@ export interface HoverCardContextValue {
9
9
  /** Set open state */
10
10
  setOpen: (open: boolean) => void;
11
11
  /** The duration from when the pointer enters the trigger until the hover card opens (ms) */
12
- openDelay: number;
12
+ openDelay: () => number;
13
13
  /** The duration from when the pointer leaves the trigger/content until the hover card closes (ms) */
14
- closeDelay: number;
14
+ closeDelay: () => number;
15
+ /** Reference to the trigger element for fixed positioning */
16
+ triggerRef: WritableSignal<HTMLElement | null>;
15
17
  }
16
18
 
17
19
  export const HOVER_CARD_CONTEXT = new InjectionToken<HoverCardContextValue>('HOVER_CARD_CONTEXT');
@@ -96,18 +96,20 @@ export class HoverCardTrigger implements OnDestroy {
96
96
  }
97
97
 
98
98
  onMouseEnter(): void {
99
+ this.context.triggerRef.set(this._elementRef.nativeElement);
99
100
  this.clearTimeouts();
100
101
  this.openTimeout = setTimeout(() => {
101
102
  this.context.setOpen(true);
102
- }, this.context.openDelay);
103
+ }, this.context.openDelay());
103
104
  }
104
105
  onMouseLeave(): void {
105
106
  this.clearTimeouts();
106
107
  this.closeTimeout = setTimeout(() => {
107
108
  this.context.setOpen(false);
108
- }, this.context.closeDelay);
109
+ }, this.context.closeDelay());
109
110
  }
110
111
  onFocus(): void {
112
+ this.context.triggerRef.set(this._elementRef.nativeElement);
111
113
  this.clearTimeouts();
112
114
  // Open immediately on focus for keyboard users
113
115
  this.context.setOpen(true);
@@ -127,7 +129,7 @@ export class HoverCardTrigger implements OnDestroy {
127
129
  this.clearTimeouts();
128
130
  this.closeTimeout = setTimeout(() => {
129
131
  this.context.setOpen(false);
130
- }, this.context.closeDelay);
132
+ }, this.context.closeDelay());
131
133
  }
132
134
  onKeyDown(event: Event): void {
133
135
  event.preventDefault();
@@ -5,6 +5,8 @@ import {
5
5
  input,
6
6
  output,
7
7
  signal,
8
+ Signal,
9
+ WritableSignal,
8
10
  } from '@angular/core';
9
11
  import { HOVER_CARD_CONTEXT, type HoverCardContextValue } from './hover-card-context';
10
12
 
@@ -93,7 +95,7 @@ export interface HoverCardProps {
93
95
  template: `<ng-content />`,
94
96
  host: {
95
97
  'attr.data-slot': '"hover-card"',
96
- class: 'relative inline-block',
98
+ class: 'inline-block',
97
99
  },
98
100
  providers: [
99
101
  {
@@ -120,10 +122,13 @@ export class HoverCard implements HoverCardContextValue {
120
122
 
121
123
  readonly open = signal(false);
122
124
 
125
+ /** Reference to the trigger element for fixed positioning */
126
+ readonly triggerRef: WritableSignal<HTMLElement | null> = signal<HTMLElement | null>(null);
127
+
123
128
  /** The duration from when the pointer enters the trigger until the hover card opens (ms) */
124
- readonly openDelay = 700;
129
+ readonly openDelay = input<number>(700);
125
130
  /** The duration from when the pointer leaves the trigger/content until the hover card closes (ms) */
126
- readonly closeDelay = 300;
131
+ readonly closeDelay = input<number>(300);
127
132
 
128
133
  setOpen(open: boolean): void {
129
134
  if (this.controlledOpen() === undefined) {
@@ -14,6 +14,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
14
14
  selector: 'InputGroupAddon',
15
15
  template: `<ng-content />`,
16
16
  host: {
17
+ 'attr.data-slot': '"input-group-addon"',
17
18
  '[class]': 'computedClass()',
18
19
  },
19
20
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -10,6 +10,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
10
10
  selector: 'InputGroupInput',
11
11
  template: ``,
12
12
  host: {
13
+ 'attr.data-slot': '"input-group-input"',
13
14
  '[class]': 'computedClass()',
14
15
  },
15
16
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -30,6 +30,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
30
30
  selector: 'InputGroup',
31
31
  template: `<ng-content />`,
32
32
  host: {
33
+ 'attr.data-slot': '"input-group"',
33
34
  '[class]': 'computedClass()',
34
35
  },
35
36
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -0,0 +1,21 @@
1
+ export { ItemActions } from './item-actions.component';
2
+ export { ItemContent } from './item-content.component';
3
+ export { ItemDescription } from './item-description.component';
4
+ export { ItemFooter } from './item-footer.component';
5
+ export { ItemGroup } from './item-group.component';
6
+ export { ItemHeader } from './item-header.component';
7
+ export {
8
+ ItemMedia,
9
+ itemMediaVariants,
10
+ type ItemMediaVariant,
11
+ type ItemMediaVariants,
12
+ } from './item-media.component';
13
+ export { ItemSeparator } from './item-separator.component';
14
+ export { ItemTitle } from './item-title.component';
15
+ export {
16
+ Item,
17
+ itemVariants,
18
+ type ItemSize,
19
+ type ItemVariant,
20
+ type ItemVariants,
21
+ } from './item.component';
@@ -0,0 +1,29 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+
4
+ /**
5
+ * ItemActions component - trailing actions (buttons, icons) for an Item.
6
+ *
7
+ * @example
8
+ * <ItemActions>
9
+ * <Button variant="outline" size="sm">Open</Button>
10
+ * </ItemActions>
11
+ */
12
+ @Component({
13
+ selector: 'ItemActions',
14
+ template: `<ng-content />`,
15
+ host: {
16
+ 'attr.data-slot': '"item-actions"',
17
+ '[class]': 'computedClass()',
18
+ },
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ })
21
+ export class ItemActions {
22
+ /** Additional CSS classes to apply */
23
+ readonly class = input<string>('');
24
+
25
+ /** Computed class combining base styles and custom classes */
26
+ protected readonly computedClass = computed(() =>
27
+ cn('flex items-center gap-2', this.class()),
28
+ );
29
+ }
@@ -0,0 +1,31 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+
4
+ /**
5
+ * ItemContent component - main content area of an Item, holding
6
+ * the title and description.
7
+ *
8
+ * @example
9
+ * <ItemContent>
10
+ * <ItemTitle>Title</ItemTitle>
11
+ * <ItemDescription>Description</ItemDescription>
12
+ * </ItemContent>
13
+ */
14
+ @Component({
15
+ selector: 'ItemContent',
16
+ template: `<ng-content />`,
17
+ host: {
18
+ 'attr.data-slot': '"item-content"',
19
+ '[class]': 'computedClass()',
20
+ },
21
+ changeDetection: ChangeDetectionStrategy.OnPush,
22
+ })
23
+ export class ItemContent {
24
+ /** Additional CSS classes to apply */
25
+ readonly class = input<string>('');
26
+
27
+ /** Computed class combining base styles and custom classes */
28
+ protected readonly computedClass = computed(() =>
29
+ cn('flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none', this.class()),
30
+ );
31
+ }
@@ -0,0 +1,30 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+
4
+ /**
5
+ * ItemDescription component - secondary description text for an Item.
6
+ *
7
+ * @example
8
+ * <ItemDescription>A short summary of the item.</ItemDescription>
9
+ */
10
+ @Component({
11
+ selector: 'ItemDescription',
12
+ template: `<ng-content />`,
13
+ host: {
14
+ 'attr.data-slot': '"item-description"',
15
+ '[class]': 'computedClass()',
16
+ },
17
+ changeDetection: ChangeDetectionStrategy.OnPush,
18
+ })
19
+ export class ItemDescription {
20
+ /** Additional CSS classes to apply */
21
+ readonly class = input<string>('');
22
+
23
+ /** Computed class combining base styles and custom classes */
24
+ protected readonly computedClass = computed(() =>
25
+ cn(
26
+ 'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
27
+ this.class(),
28
+ ),
29
+ );
30
+ }
@@ -0,0 +1,30 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+
4
+ /**
5
+ * ItemFooter component - full-width footer row inside an Item.
6
+ *
7
+ * @example
8
+ * <Item>
9
+ * <ItemContent>...</ItemContent>
10
+ * <ItemFooter>Footer content</ItemFooter>
11
+ * </Item>
12
+ */
13
+ @Component({
14
+ selector: 'ItemFooter',
15
+ template: `<ng-content />`,
16
+ host: {
17
+ 'attr.data-slot': '"item-footer"',
18
+ '[class]': 'computedClass()',
19
+ },
20
+ changeDetection: ChangeDetectionStrategy.OnPush,
21
+ })
22
+ export class ItemFooter {
23
+ /** Additional CSS classes to apply */
24
+ readonly class = input<string>('');
25
+
26
+ /** Computed class combining base styles and custom classes */
27
+ protected readonly computedClass = computed(() =>
28
+ cn('flex basis-full items-center justify-between gap-2', this.class()),
29
+ );
30
+ }
@@ -0,0 +1,32 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+
4
+ /**
5
+ * ItemGroup component - container that stacks multiple Item components.
6
+ *
7
+ * @example
8
+ * <ItemGroup>
9
+ * <Item>...</Item>
10
+ * <ItemSeparator />
11
+ * <Item>...</Item>
12
+ * </ItemGroup>
13
+ */
14
+ @Component({
15
+ selector: 'ItemGroup',
16
+ template: `<ng-content />`,
17
+ host: {
18
+ 'attr.data-slot': '"item-group"',
19
+ role: 'list',
20
+ '[class]': 'computedClass()',
21
+ },
22
+ changeDetection: ChangeDetectionStrategy.OnPush,
23
+ })
24
+ export class ItemGroup {
25
+ /** Additional CSS classes to apply */
26
+ readonly class = input<string>('');
27
+
28
+ /** Computed class combining base styles and custom classes */
29
+ protected readonly computedClass = computed(() =>
30
+ cn('group/item-group flex flex-col', this.class()),
31
+ );
32
+ }