@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.
- package/package.json +6 -5
- package/src/app/lib/components/ui/alert-dialog/alert-dialog-content.component.ts +22 -21
- package/src/app/lib/components/ui/avatar/ui-avatar.component.ts +4 -0
- package/src/app/lib/components/ui/calendar/calendar.component.ts +70 -13
- package/src/app/lib/components/ui/carousel/carousel-content.component.ts +1 -0
- package/src/app/lib/components/ui/carousel/carousel-item.component.ts +1 -0
- package/src/app/lib/components/ui/carousel/carousel-next.component.ts +1 -0
- package/src/app/lib/components/ui/carousel/carousel-previous.component.ts +1 -0
- package/src/app/lib/components/ui/carousel/carousel.component.ts +1 -0
- package/src/app/lib/components/ui/chart/chart-container.component.ts +1 -0
- package/src/app/lib/components/ui/chart/chart-legend-content.component.ts +1 -0
- package/src/app/lib/components/ui/chart/chart-legend.component.ts +5 -5
- package/src/app/lib/components/ui/chart/chart-tooltip-content.component.ts +5 -5
- package/src/app/lib/components/ui/chart/chart-tooltip.component.ts +5 -5
- package/src/app/lib/components/ui/chart/chart.component.ts +1 -0
- package/src/app/lib/components/ui/checkbox/checkbox.component.ts +1 -1
- package/src/app/lib/components/ui/collapsible/collapsible-content.component.ts +2 -1
- package/src/app/lib/components/ui/collapsible/collapsible-context.ts +1 -0
- package/src/app/lib/components/ui/collapsible/collapsible-trigger.component.ts +1 -0
- package/src/app/lib/components/ui/collapsible/collapsible.component.ts +3 -0
- package/src/app/lib/components/ui/context-menu/context-menu-content.component.ts +49 -17
- package/src/app/lib/components/ui/context-menu/context-menu-sub-content.component.ts +2 -0
- package/src/app/lib/components/ui/context-menu/context-menu-sub-trigger.component.ts +30 -1
- package/src/app/lib/components/ui/context-menu/context-menu-sub.component.ts +3 -0
- package/src/app/lib/components/ui/country-selector/country-data.ts +63 -0
- package/src/app/lib/components/ui/country-selector/country-selector.component.ts +199 -0
- package/src/app/lib/components/ui/country-selector/index.ts +2 -0
- package/src/app/lib/components/ui/date-picker/date-picker.component.ts +48 -5
- package/src/app/lib/components/ui/dialog/dialog-content.component.ts +27 -20
- package/src/app/lib/components/ui/direction/direction-context.ts +9 -0
- package/src/app/lib/components/ui/direction/direction.component.ts +48 -0
- package/src/app/lib/components/ui/direction/index.ts +2 -0
- package/src/app/lib/components/ui/drawer/drawer-content.component.ts +44 -0
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-content.component.ts +25 -23
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.component.ts +1 -0
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.component.ts +2 -0
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.component.ts +28 -2
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub.component.ts +3 -0
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-trigger.component.ts +25 -0
- package/src/app/lib/components/ui/empty/empty-action.component.ts +1 -0
- package/src/app/lib/components/ui/empty/empty-description.component.ts +1 -0
- package/src/app/lib/components/ui/empty/empty-icon.component.ts +2 -1
- package/src/app/lib/components/ui/empty/empty-title.component.ts +1 -0
- package/src/app/lib/components/ui/empty/empty.component.ts +1 -0
- package/src/app/lib/components/ui/field/field-content.component.ts +34 -0
- package/src/app/lib/components/ui/field/field-description.component.ts +35 -0
- package/src/app/lib/components/ui/field/field-error.component.ts +48 -0
- package/src/app/lib/components/ui/field/field-group.component.ts +34 -0
- package/src/app/lib/components/ui/field/field-label.component.ts +46 -0
- package/src/app/lib/components/ui/field/field-legend.component.ts +41 -0
- package/src/app/lib/components/ui/field/field-separator.component.ts +49 -0
- package/src/app/lib/components/ui/field/field-set.component.ts +37 -0
- package/src/app/lib/components/ui/field/field-title.component.ts +30 -0
- package/src/app/lib/components/ui/field/field.component.ts +66 -0
- package/src/app/lib/components/ui/field/index.ts +15 -0
- package/src/app/lib/components/ui/form/form-description.component.ts +2 -2
- package/src/app/lib/components/ui/hover-card/hover-card-content.component.ts +108 -60
- package/src/app/lib/components/ui/hover-card/hover-card-context.ts +4 -2
- package/src/app/lib/components/ui/hover-card/hover-card-trigger.component.ts +5 -3
- package/src/app/lib/components/ui/hover-card/hover-card.component.ts +8 -3
- package/src/app/lib/components/ui/input-group/input-group-addon.component.ts +1 -0
- package/src/app/lib/components/ui/input-group/input-group-input.component.ts +1 -0
- package/src/app/lib/components/ui/input-group/input-group.component.ts +1 -0
- package/src/app/lib/components/ui/item/index.ts +21 -0
- package/src/app/lib/components/ui/item/item-actions.component.ts +29 -0
- package/src/app/lib/components/ui/item/item-content.component.ts +31 -0
- package/src/app/lib/components/ui/item/item-description.component.ts +30 -0
- package/src/app/lib/components/ui/item/item-footer.component.ts +30 -0
- package/src/app/lib/components/ui/item/item-group.component.ts +32 -0
- package/src/app/lib/components/ui/item/item-header.component.ts +30 -0
- package/src/app/lib/components/ui/item/item-media.component.ts +63 -0
- package/src/app/lib/components/ui/item/item-separator.component.ts +33 -0
- package/src/app/lib/components/ui/item/item-title.component.ts +27 -0
- package/src/app/lib/components/ui/item/item.component.ts +77 -0
- package/src/app/lib/components/ui/menubar/menubar-content.component.ts +1 -1
- package/src/app/lib/components/ui/navigation-menu/navigation-menu-content.component.ts +7 -1
- package/src/app/lib/components/ui/navigation-menu/navigation-menu-context.ts +14 -0
- package/src/app/lib/components/ui/navigation-menu/navigation-menu-item.component.ts +9 -4
- package/src/app/lib/components/ui/navigation-menu/navigation-menu-trigger.component.ts +69 -2
- package/src/app/lib/components/ui/navigation-menu/navigation-menu.component.ts +32 -4
- package/src/app/lib/components/ui/pagination/pagination.component.ts +3 -1
- package/src/app/lib/components/ui/phone-input/index.ts +1 -0
- package/src/app/lib/components/ui/phone-input/phone-input.component.ts +169 -0
- package/src/app/lib/components/ui/popover/popover-content.component.ts +11 -0
- package/src/app/lib/components/ui/popover/popover-context.ts +2 -0
- package/src/app/lib/components/ui/popover/popover.component.ts +4 -0
- package/src/app/lib/components/ui/progress/progress.component.ts +1 -2
- package/src/app/lib/components/ui/resizable/resizable-handle.component.ts +2 -2
- package/src/app/lib/components/ui/scroll-area/scroll-area.component.ts +7 -6
- package/src/app/lib/components/ui/segmented/segmented-item.component.ts +1 -0
- package/src/app/lib/components/ui/segmented/segmented.component.ts +1 -0
- package/src/app/lib/components/ui/select/select-content.component.ts +35 -15
- package/src/app/lib/components/ui/select/select-context.ts +10 -0
- package/src/app/lib/components/ui/select/select-item.component.ts +25 -7
- package/src/app/lib/components/ui/select/select-trigger.component.ts +6 -13
- package/src/app/lib/components/ui/select/select.component.ts +46 -0
- package/src/app/lib/components/ui/sheet/sheet-content.component.ts +23 -6
- package/src/app/lib/components/ui/slider/slider.component.ts +2 -2
- package/src/app/lib/components/ui/sonner/index.ts +2 -0
- package/src/app/lib/components/ui/sonner/sonner.component.ts +70 -0
- package/src/app/lib/components/ui/switch/switch.component.ts +1 -14
- package/src/app/lib/components/ui/tabs/tabs-list.component.ts +20 -0
- package/src/app/lib/components/ui/tabs/tabs-trigger.component.ts +0 -1
- package/src/app/lib/components/ui/textarea/textarea.component.ts +110 -10
- package/src/app/lib/components/ui/toast/toast.service.ts +1 -1
- package/src/app/lib/components/ui/toggle/toggle.component.ts +12 -6
- package/src/app/lib/components/ui/tooltip/tooltip-content.component.ts +141 -17
- package/src/app/lib/components/ui/tooltip/tooltip-context.ts +3 -1
- package/src/app/lib/components/ui/tooltip/tooltip-provider.component.ts +1 -1
- package/src/app/lib/components/ui/tooltip/tooltip-trigger.component.ts +5 -2
- 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-[
|
|
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]="
|
|
83
|
-
[attr.data-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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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.
|
|
146
|
+
this.clearCloseTimeout();
|
|
147
|
+
this.cancelScheduledPositionUpdate();
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
onMouseEnter(): void {
|
|
153
|
-
this.
|
|
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.
|
|
159
|
+
this.clearCloseTimeout();
|
|
162
160
|
}
|
|
163
161
|
onFocusOut(event: FocusEvent): void {
|
|
164
162
|
const relatedTarget = event.relatedTarget as HTMLElement | null;
|
|
165
|
-
const trigger = this.
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
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
|
+
}
|