@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-cn/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "Beautifully designed Angular components built with Tailwind CSS v4 - The official Angular port of shadcn/ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
]
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
+
"@angular-devkit/core": "^21.2.5",
|
|
62
|
+
"@angular-devkit/schematics": "^21.2.5",
|
|
61
63
|
"@angular/cdk": "^21.2.4",
|
|
62
64
|
"@angular/common": "^21.2.6",
|
|
63
65
|
"@angular/compiler": "^21.2.6",
|
|
@@ -74,18 +76,17 @@
|
|
|
74
76
|
"express": "^5.1.0",
|
|
75
77
|
"lucide-angular": "^1.0.0",
|
|
76
78
|
"ng-apexcharts": "^2.3.0",
|
|
79
|
+
"ngx-sonner": "^3.1.0",
|
|
77
80
|
"postcss": "^8.5.8",
|
|
78
81
|
"rxjs": "~7.8.0",
|
|
79
82
|
"shiki": "^4.0.2",
|
|
80
83
|
"tailwind-merge": "^3.5.0",
|
|
81
84
|
"tailwindcss": "^4.2.2",
|
|
82
|
-
"tslib": "^2.3.0"
|
|
83
|
-
"@angular-devkit/core": "^21.2.5",
|
|
84
|
-
"@angular-devkit/schematics": "^21.2.5"
|
|
85
|
+
"tslib": "^2.3.0"
|
|
85
86
|
},
|
|
86
87
|
"devDependencies": {
|
|
87
|
-
"@analogjs/vitest-angular": "^2.3.1",
|
|
88
88
|
"@analogjs/vite-plugin-angular": "^2.2.0",
|
|
89
|
+
"@analogjs/vitest-angular": "^2.3.1",
|
|
89
90
|
"@angular/build": "^21.2.5",
|
|
90
91
|
"@angular/cli": "^21.2.5",
|
|
91
92
|
"@angular/compiler-cli": "^21.2.6",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cn } from '@/lib/utils';
|
|
1
|
+
import { cn, Presence } from '@/lib/utils';
|
|
2
2
|
import { FocusTrapDirective } from '@/lib/utils/accessibility';
|
|
3
3
|
import {
|
|
4
4
|
ChangeDetectionStrategy,
|
|
@@ -10,13 +10,9 @@ import {
|
|
|
10
10
|
HostListener,
|
|
11
11
|
inject,
|
|
12
12
|
input,
|
|
13
|
-
signal,
|
|
14
13
|
} from '@angular/core';
|
|
15
14
|
import { ALERT_DIALOG_CONTEXT } from './alert-dialog-context';
|
|
16
15
|
|
|
17
|
-
/** Animation duration in ms — must match Tailwind's duration-200 */
|
|
18
|
-
const EXIT_ANIMATION_MS = 200;
|
|
19
|
-
|
|
20
16
|
/**
|
|
21
17
|
* AlertDialogContent component - the modal content of the alert dialog.
|
|
22
18
|
* Matches shadcn/ui React AlertDialogContent exactly.
|
|
@@ -26,12 +22,14 @@ const EXIT_ANIMATION_MS = 200;
|
|
|
26
22
|
* - Overlay/backdrop click does NOT close the dialog
|
|
27
23
|
* - Focus is trapped within the dialog
|
|
28
24
|
* - User must explicitly click Cancel or Action to close
|
|
25
|
+
* - Exit animations handled by Presence component (no setTimeout needed)
|
|
26
|
+
* - Focus restored on any programmatic close (Action/Cancel/Escape)
|
|
29
27
|
*/
|
|
30
28
|
@Component({
|
|
31
29
|
selector: 'AlertDialogContent',
|
|
32
|
-
imports: [FocusTrapDirective],
|
|
30
|
+
imports: [FocusTrapDirective, Presence],
|
|
33
31
|
template: `
|
|
34
|
-
|
|
32
|
+
<Presence [present]="context.isOpen()">
|
|
35
33
|
<!-- Overlay - does NOT close on click -->
|
|
36
34
|
<div
|
|
37
35
|
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 duration-200"
|
|
@@ -55,7 +53,7 @@ const EXIT_ANIMATION_MS = 200;
|
|
|
55
53
|
>
|
|
56
54
|
<ng-content />
|
|
57
55
|
</div>
|
|
58
|
-
|
|
56
|
+
</Presence>
|
|
59
57
|
`,
|
|
60
58
|
host: {
|
|
61
59
|
'attr.data-slot': '"alert-dialog-content"',
|
|
@@ -65,21 +63,22 @@ const EXIT_ANIMATION_MS = 200;
|
|
|
65
63
|
})
|
|
66
64
|
export class AlertDialogContent {
|
|
67
65
|
constructor() {
|
|
66
|
+
let wasOpen = false;
|
|
67
|
+
|
|
68
68
|
effect(() => {
|
|
69
69
|
const isOpen = this.context.isOpen();
|
|
70
70
|
this._cdr.markForCheck();
|
|
71
71
|
|
|
72
72
|
if (isOpen) {
|
|
73
|
-
|
|
73
|
+
wasOpen = true;
|
|
74
74
|
this.lockBodyScroll();
|
|
75
75
|
} else {
|
|
76
76
|
this.unlockBodyScroll();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this._cdr.markForCheck();
|
|
81
|
-
}, EXIT_ANIMATION_MS);
|
|
77
|
+
// Restore focus whenever dialog closes (covers Action/Cancel/Escape paths)
|
|
78
|
+
if (wasOpen) {
|
|
79
|
+
this.restoreFocus();
|
|
82
80
|
}
|
|
81
|
+
wasOpen = false;
|
|
83
82
|
}
|
|
84
83
|
});
|
|
85
84
|
|
|
@@ -95,7 +94,6 @@ export class AlertDialogContent {
|
|
|
95
94
|
private readonly _cdr = inject(ChangeDetectorRef);
|
|
96
95
|
|
|
97
96
|
protected readonly context = inject(ALERT_DIALOG_CONTEXT);
|
|
98
|
-
protected readonly shouldRender = signal(false);
|
|
99
97
|
|
|
100
98
|
protected readonly computedClass = computed(() =>
|
|
101
99
|
cn(
|
|
@@ -111,29 +109,32 @@ export class AlertDialogContent {
|
|
|
111
109
|
);
|
|
112
110
|
|
|
113
111
|
private previousBodyOverflow = '';
|
|
112
|
+
private previousBodyPaddingRight = '';
|
|
114
113
|
|
|
115
114
|
@HostListener('document:keydown.escape')
|
|
116
115
|
onEscapeKey(): void {
|
|
117
116
|
if (this.context.isOpen()) {
|
|
118
|
-
this.
|
|
117
|
+
this.context.setOpen(false);
|
|
119
118
|
}
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
private lockBodyScroll(): void {
|
|
123
|
-
if (typeof document !== 'undefined') {
|
|
122
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
123
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
124
124
|
this.previousBodyOverflow = document.body.style.overflow;
|
|
125
|
+
this.previousBodyPaddingRight = document.body.style.paddingRight;
|
|
125
126
|
document.body.style.overflow = 'hidden';
|
|
127
|
+
if (scrollbarWidth > 0) {
|
|
128
|
+
document.body.style.paddingRight = scrollbarWidth + 'px';
|
|
129
|
+
}
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
private unlockBodyScroll(): void {
|
|
129
133
|
if (typeof document !== 'undefined') {
|
|
130
134
|
document.body.style.overflow = this.previousBodyOverflow;
|
|
135
|
+
document.body.style.paddingRight = this.previousBodyPaddingRight;
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
|
-
private close(): void {
|
|
134
|
-
this.restoreFocus();
|
|
135
|
-
this.context.setOpen(false);
|
|
136
|
-
}
|
|
137
138
|
private restoreFocus(): void {
|
|
138
139
|
const triggerEl = this.context.getTriggerElement();
|
|
139
140
|
if (triggerEl) {
|
|
@@ -14,6 +14,10 @@ import { Avatar } from './avatar.component';
|
|
|
14
14
|
selector: 'ui-avatar',
|
|
15
15
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
16
|
imports: [Avatar, AvatarImage, AvatarFallback],
|
|
17
|
+
host: {
|
|
18
|
+
'attr.data-slot': '"ui-avatar"',
|
|
19
|
+
style: 'display: contents',
|
|
20
|
+
},
|
|
17
21
|
template: `
|
|
18
22
|
<Avatar [class]="class()">
|
|
19
23
|
@if (src()) {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ChangeDetectionStrategy,
|
|
5
5
|
Component,
|
|
6
6
|
computed,
|
|
7
|
+
ElementRef,
|
|
7
8
|
inject,
|
|
8
9
|
input,
|
|
9
10
|
model,
|
|
@@ -26,8 +27,12 @@ import { buttonVariants } from '../button';
|
|
|
26
27
|
@Component({
|
|
27
28
|
selector: 'Calendar',
|
|
28
29
|
imports: [LucideAngularModule],
|
|
30
|
+
host: {
|
|
31
|
+
'attr.data-slot': '"calendar"',
|
|
32
|
+
'[class]': 'computedClass()',
|
|
33
|
+
},
|
|
29
34
|
template: `
|
|
30
|
-
<div
|
|
35
|
+
<div role="application" [attr.aria-label]="ariaLabel()">
|
|
31
36
|
<!-- Header with navigation -->
|
|
32
37
|
<div class="w-full">
|
|
33
38
|
<div class="w-full space-y-4">
|
|
@@ -47,6 +52,7 @@ import { buttonVariants } from '../button';
|
|
|
47
52
|
type="button"
|
|
48
53
|
[class]="navButtonClass()"
|
|
49
54
|
(click)="previousMonth()"
|
|
55
|
+
[disabled]="isPrevMonthDisabled()"
|
|
50
56
|
[attr.aria-label]="'Go to previous month, ' + getPreviousMonthLabel()"
|
|
51
57
|
>
|
|
52
58
|
<lucide-icon [img]="ChevronLeftIcon" class="h-4 w-4" aria-hidden="true" />
|
|
@@ -55,6 +61,7 @@ import { buttonVariants } from '../button';
|
|
|
55
61
|
type="button"
|
|
56
62
|
[class]="navButtonClass()"
|
|
57
63
|
(click)="nextMonth()"
|
|
64
|
+
[disabled]="isNextMonthDisabled()"
|
|
58
65
|
[attr.aria-label]="'Go to next month, ' + getNextMonthLabel()"
|
|
59
66
|
>
|
|
60
67
|
<lucide-icon [img]="ChevronRightIcon" class="h-4 w-4" aria-hidden="true" />
|
|
@@ -94,6 +101,7 @@ import { buttonVariants } from '../button';
|
|
|
94
101
|
<button
|
|
95
102
|
type="button"
|
|
96
103
|
[class]="getDayClass(day)"
|
|
104
|
+
[attr.data-date]="day.date.getFullYear() + '-' + day.date.getMonth() + '-' + day.date.getDate()"
|
|
97
105
|
[attr.aria-label]="getDateLabel(day.date)"
|
|
98
106
|
[attr.aria-selected]="isSelected(day.date) ? 'true' : null"
|
|
99
107
|
[attr.aria-current]="isToday(day.date) ? 'date' : null"
|
|
@@ -134,12 +142,19 @@ export class Calendar {
|
|
|
134
142
|
readonly showOutsideDays = input<boolean>(true);
|
|
135
143
|
/** Disabled dates function */
|
|
136
144
|
readonly disabled = input<((date: Date) => boolean) | undefined>(undefined);
|
|
145
|
+
/** Minimum selectable date — dates before this are disabled, navigation before this month is blocked */
|
|
146
|
+
readonly minDate = input<Date | undefined>(undefined);
|
|
147
|
+
/** Maximum selectable date — dates after this are disabled, navigation after this month is blocked */
|
|
148
|
+
readonly maxDate = input<Date | undefined>(undefined);
|
|
149
|
+
/** Locale for date formatting (e.g. 'en-US', 'fr-FR') */
|
|
150
|
+
readonly locale = input<string>('en-US');
|
|
137
151
|
/** Accessible label for the calendar */
|
|
138
152
|
readonly ariaLabel = input<string>('Calendar');
|
|
139
153
|
/** Additional CSS classes */
|
|
140
154
|
readonly class = input<string>('');
|
|
141
155
|
|
|
142
156
|
private readonly _liveAnnouncer = inject(LiveAnnouncerService);
|
|
157
|
+
private readonly _elementRef = inject(ElementRef);
|
|
143
158
|
|
|
144
159
|
protected readonly computedClass = computed(() => cn('w-full p-3', this.class()));
|
|
145
160
|
protected readonly navButtonClass = computed(() =>
|
|
@@ -148,9 +163,25 @@ export class Calendar {
|
|
|
148
163
|
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 hover:bg-gray-100 dark:hover:bg-neutral-800',
|
|
149
164
|
),
|
|
150
165
|
);
|
|
166
|
+
/** True when navigating to the previous month would go before minDate */
|
|
167
|
+
protected readonly isPrevMonthDisabled = computed(() => {
|
|
168
|
+
const min = this.minDate();
|
|
169
|
+
if (!min) return false;
|
|
170
|
+
const current = this.currentMonth();
|
|
171
|
+
const prevMonthEnd = new Date(current.getFullYear(), current.getMonth(), 0);
|
|
172
|
+
return prevMonthEnd < new Date(min.getFullYear(), min.getMonth(), 1);
|
|
173
|
+
});
|
|
174
|
+
/** True when navigating to the next month would go after maxDate */
|
|
175
|
+
protected readonly isNextMonthDisabled = computed(() => {
|
|
176
|
+
const max = this.maxDate();
|
|
177
|
+
if (!max) return false;
|
|
178
|
+
const current = this.currentMonth();
|
|
179
|
+
const nextMonthStart = new Date(current.getFullYear(), current.getMonth() + 1, 1);
|
|
180
|
+
return nextMonthStart > new Date(max.getFullYear(), max.getMonth() + 1, 0);
|
|
181
|
+
});
|
|
151
182
|
protected readonly monthYear = computed(() => {
|
|
152
183
|
const date = this.currentMonth();
|
|
153
|
-
return date.toLocaleDateString(
|
|
184
|
+
return date.toLocaleDateString(this.locale(), { month: 'long', year: 'numeric' });
|
|
154
185
|
});
|
|
155
186
|
protected readonly calendarWeeks = computed(() => {
|
|
156
187
|
const date = this.currentMonth();
|
|
@@ -184,7 +215,12 @@ export class Calendar {
|
|
|
184
215
|
const dayDate = new Date(current);
|
|
185
216
|
const isOutside = dayDate.getMonth() !== month;
|
|
186
217
|
const disabledFn = this.disabled();
|
|
187
|
-
const
|
|
218
|
+
const min = this.minDate();
|
|
219
|
+
const max = this.maxDate();
|
|
220
|
+
const isDisabled =
|
|
221
|
+
(disabledFn ? disabledFn(dayDate) : false) ||
|
|
222
|
+
(min != null && this.isBeforeDay(dayDate, min)) ||
|
|
223
|
+
(max != null && this.isAfterDay(dayDate, max));
|
|
188
224
|
|
|
189
225
|
week.push({
|
|
190
226
|
date: dayDate,
|
|
@@ -224,7 +260,7 @@ export class Calendar {
|
|
|
224
260
|
day: 'numeric',
|
|
225
261
|
year: 'numeric',
|
|
226
262
|
};
|
|
227
|
-
const label = date.toLocaleDateString(
|
|
263
|
+
const label = date.toLocaleDateString(this.locale(), options);
|
|
228
264
|
|
|
229
265
|
if (this.isToday(date)) {
|
|
230
266
|
return `${label}, today`;
|
|
@@ -242,13 +278,13 @@ export class Calendar {
|
|
|
242
278
|
protected getPreviousMonthLabel(): string {
|
|
243
279
|
const current = this.currentMonth();
|
|
244
280
|
const prev = new Date(current.getFullYear(), current.getMonth() - 1, 1);
|
|
245
|
-
return prev.toLocaleDateString(
|
|
281
|
+
return prev.toLocaleDateString(this.locale(), { month: 'long', year: 'numeric' });
|
|
246
282
|
}
|
|
247
283
|
/** Get label for next month button */
|
|
248
284
|
protected getNextMonthLabel(): string {
|
|
249
285
|
const current = this.currentMonth();
|
|
250
286
|
const next = new Date(current.getFullYear(), current.getMonth() + 1, 1);
|
|
251
|
-
return next.toLocaleDateString(
|
|
287
|
+
return next.toLocaleDateString(this.locale(), { month: 'long', year: 'numeric' });
|
|
252
288
|
}
|
|
253
289
|
/** Get tabindex for day button (roving tabindex) */
|
|
254
290
|
protected getDayTabIndex(day: { date: Date; isOutside: boolean; disabled: boolean }): number {
|
|
@@ -298,13 +334,21 @@ export class Calendar {
|
|
|
298
334
|
if (newDate.getMonth() !== this.currentMonth().getMonth()) {
|
|
299
335
|
this.currentMonth.set(new Date(newDate.getFullYear(), newDate.getMonth(), 1));
|
|
300
336
|
}
|
|
301
|
-
// Focus the new date after DOM update
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
337
|
+
// Focus the new date after DOM update (browser only)
|
|
338
|
+
if (typeof document !== 'undefined') {
|
|
339
|
+
const targetDate = newDate;
|
|
340
|
+
setTimeout(() => {
|
|
341
|
+
const root: HTMLElement = this._elementRef.nativeElement;
|
|
342
|
+
const buttons = root.querySelectorAll<HTMLElement>('button[data-date]');
|
|
343
|
+
const targetStr = `${targetDate.getFullYear()}-${targetDate.getMonth()}-${targetDate.getDate()}`;
|
|
344
|
+
for (const btn of Array.from(buttons)) {
|
|
345
|
+
if (btn.dataset['date'] === targetStr) {
|
|
346
|
+
btn.focus();
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}, 0);
|
|
351
|
+
}
|
|
308
352
|
}
|
|
309
353
|
}
|
|
310
354
|
protected getDayClass(day: { date: Date; isOutside: boolean; disabled: boolean }): string {
|
|
@@ -353,4 +397,17 @@ export class Calendar {
|
|
|
353
397
|
date1.getDate() === date2.getDate()
|
|
354
398
|
);
|
|
355
399
|
}
|
|
400
|
+
private isBeforeDay(date: Date, ref: Date): boolean {
|
|
401
|
+
if (date.getFullYear() !== ref.getFullYear()) return date.getFullYear() < ref.getFullYear();
|
|
402
|
+
if (date.getMonth() !== ref.getMonth()) return date.getMonth() < ref.getMonth();
|
|
403
|
+
return date.getDate() < ref.getDate();
|
|
404
|
+
}
|
|
405
|
+
private isAfterDay(date: Date, ref: Date): boolean {
|
|
406
|
+
if (date.getFullYear() !== ref.getFullYear()) return date.getFullYear() > ref.getFullYear();
|
|
407
|
+
if (date.getMonth() !== ref.getMonth()) return date.getMonth() > ref.getMonth();
|
|
408
|
+
return date.getDate() > ref.getDate();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// TODO: range and multiple modes - requires multi-select state
|
|
356
412
|
}
|
|
413
|
+
|
|
@@ -24,6 +24,7 @@ import { CHART_CONTEXT, type ChartConfig, type ChartContext } from './chart-cont
|
|
|
24
24
|
selector: 'ChartContainer',
|
|
25
25
|
template: `<ng-content />`,
|
|
26
26
|
host: {
|
|
27
|
+
'attr.data-slot': '"chart-container"',
|
|
27
28
|
'[class]': 'computedClass()',
|
|
28
29
|
'[style]': 'chartStyles()',
|
|
29
30
|
'data-chart': '',
|
|
@@ -6,11 +6,11 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
6
6
|
*/
|
|
7
7
|
@Component({
|
|
8
8
|
selector: 'ChartLegend',
|
|
9
|
-
template:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
template: `<ng-content />`,
|
|
10
|
+
host: {
|
|
11
|
+
'attr.data-slot': '"chart-legend"',
|
|
12
|
+
'[class]': 'computedClass()',
|
|
13
|
+
},
|
|
14
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
15
|
})
|
|
16
16
|
export class ChartLegend {
|
|
@@ -6,11 +6,11 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
6
6
|
*/
|
|
7
7
|
@Component({
|
|
8
8
|
selector: 'ChartTooltipContent',
|
|
9
|
-
template:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
template: `<ng-content />`,
|
|
10
|
+
host: {
|
|
11
|
+
'attr.data-slot': '"chart-tooltip-content"',
|
|
12
|
+
'[class]': 'computedClass()',
|
|
13
|
+
},
|
|
14
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
15
|
})
|
|
16
16
|
export class ChartTooltipContent {
|
|
@@ -6,11 +6,11 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
6
6
|
*/
|
|
7
7
|
@Component({
|
|
8
8
|
selector: 'ChartTooltip',
|
|
9
|
-
template:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
template: `<ng-content />`,
|
|
10
|
+
host: {
|
|
11
|
+
'attr.data-slot': '"chart-tooltip"',
|
|
12
|
+
'[class]': 'computedClass()',
|
|
13
|
+
},
|
|
14
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
15
|
})
|
|
16
16
|
export class ChartTooltip {
|
|
@@ -22,7 +22,8 @@ import { COLLAPSIBLE_CONTEXT } from './collapsible-context';
|
|
|
22
22
|
'[class]': 'computedClass()',
|
|
23
23
|
'[attr.data-state]': 'collapsible.isOpen() ? "open" : "closed"',
|
|
24
24
|
'[attr.data-disabled]': 'collapsible.disabled() ? "" : null',
|
|
25
|
-
'[attr.
|
|
25
|
+
'[attr.id]': 'collapsible.contentId',
|
|
26
|
+
'[attr.inert]': '!collapsible.isOpen() || null',
|
|
26
27
|
},
|
|
27
28
|
styles: [
|
|
28
29
|
`
|
|
@@ -17,6 +17,7 @@ import { COLLAPSIBLE_CONTEXT } from './collapsible-context';
|
|
|
17
17
|
'[attr.data-state]': 'collapsible.isOpen() ? "open" : "closed"',
|
|
18
18
|
'[attr.data-disabled]': 'collapsible.disabled() ? "" : null',
|
|
19
19
|
'[attr.aria-expanded]': 'collapsible.isOpen()',
|
|
20
|
+
'[attr.aria-controls]': 'collapsible.contentId',
|
|
20
21
|
'[attr.disabled]': 'collapsible.disabled() ? true : null',
|
|
21
22
|
'(click)': 'onClick()',
|
|
22
23
|
'(keydown.enter)': 'onClick()',
|
|
@@ -77,6 +77,9 @@ export class Collapsible implements CollapsibleContext {
|
|
|
77
77
|
|
|
78
78
|
protected readonly computedClass = computed(() => cn('', this.class()));
|
|
79
79
|
|
|
80
|
+
/** Stable ID linking the trigger (aria-controls) to the content (id) */
|
|
81
|
+
readonly contentId = `collapsible-content-${Math.random().toString(36).slice(2)}`;
|
|
82
|
+
|
|
80
83
|
/** Internal state for open/closed */
|
|
81
84
|
private readonly _isOpen = signal<boolean>(false);
|
|
82
85
|
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
effect,
|
|
8
8
|
ElementRef,
|
|
9
9
|
inject,
|
|
10
|
+
Injector,
|
|
10
11
|
input,
|
|
11
12
|
OnDestroy,
|
|
13
|
+
signal,
|
|
12
14
|
} from '@angular/core';
|
|
13
15
|
import { CONTEXT_MENU_CONTEXT } from './context-menu-context';
|
|
14
16
|
|
|
@@ -26,8 +28,8 @@ import { CONTEXT_MENU_CONTEXT } from './context-menu-context';
|
|
|
26
28
|
[class]="computedClass()"
|
|
27
29
|
[attr.data-state]="context.open() ? 'open' : 'closed'"
|
|
28
30
|
[style.position]="'fixed'"
|
|
29
|
-
[style.left.px]="
|
|
30
|
-
[style.top.px]="
|
|
31
|
+
[style.left.px]="displayPosition().x"
|
|
32
|
+
[style.top.px]="displayPosition().y"
|
|
31
33
|
role="menu"
|
|
32
34
|
aria-orientation="vertical"
|
|
33
35
|
tabindex="-1"
|
|
@@ -40,7 +42,7 @@ import { CONTEXT_MENU_CONTEXT } from './context-menu-context';
|
|
|
40
42
|
host: {
|
|
41
43
|
'attr.data-slot': '"context-menu-content"',
|
|
42
44
|
class: 'contents',
|
|
43
|
-
'(document:click)': 'onDocumentClick()',
|
|
45
|
+
'(document:click)': 'onDocumentClick($event)',
|
|
44
46
|
'(document:keydown.escape)': 'onEscapeKey()',
|
|
45
47
|
'(document:contextmenu)': 'onAnotherContextMenu()',
|
|
46
48
|
},
|
|
@@ -48,19 +50,28 @@ import { CONTEXT_MENU_CONTEXT } from './context-menu-context';
|
|
|
48
50
|
})
|
|
49
51
|
export class ContextMenuContent implements OnDestroy {
|
|
50
52
|
constructor() {
|
|
51
|
-
//
|
|
53
|
+
// Clamp position and focus first item when menu opens
|
|
52
54
|
effect(() => {
|
|
53
55
|
if (this.context.open()) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
this.isPositioned.set(false);
|
|
57
|
+
afterNextRender(
|
|
58
|
+
() => {
|
|
59
|
+
this.clampPosition();
|
|
60
|
+
this.isPositioned.set(true);
|
|
61
|
+
this.updateMenuItems();
|
|
62
|
+
const focusedIdx = this.context.focusedIndex();
|
|
63
|
+
if (focusedIdx >= 0 && this.menuItems[focusedIdx]) {
|
|
64
|
+
this.menuItems[focusedIdx].focus();
|
|
65
|
+
} else if (this.menuItems.length > 0) {
|
|
66
|
+
this.menuItems[0].focus();
|
|
67
|
+
this.context.focusedIndex.set(0);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{ injector: this._injector },
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
this.isPositioned.set(false);
|
|
74
|
+
this.clampedPos.set(null);
|
|
64
75
|
}
|
|
65
76
|
});
|
|
66
77
|
|
|
@@ -74,15 +85,21 @@ export class ContextMenuContent implements OnDestroy {
|
|
|
74
85
|
readonly class = input<string>('');
|
|
75
86
|
|
|
76
87
|
private readonly _elementRef = inject(ElementRef);
|
|
88
|
+
private readonly _injector = inject(Injector);
|
|
77
89
|
|
|
78
90
|
protected readonly context = inject(CONTEXT_MENU_CONTEXT);
|
|
79
91
|
|
|
92
|
+
protected readonly isPositioned = signal(false);
|
|
93
|
+
protected readonly clampedPos = signal<{ x: number; y: number } | null>(null);
|
|
94
|
+
protected readonly displayPosition = computed(() => this.clampedPos() ?? this.context.position());
|
|
95
|
+
|
|
80
96
|
protected readonly computedClass = computed(() =>
|
|
81
97
|
cn(
|
|
82
98
|
'z-50 min-w-[12rem] overflow-hidden rounded-xl border bg-popover p-2 text-popover-foreground shadow-lg',
|
|
83
99
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
84
100
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
85
101
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
102
|
+
!this.isPositioned() && 'pointer-events-none opacity-0',
|
|
86
103
|
this.class(),
|
|
87
104
|
),
|
|
88
105
|
);
|
|
@@ -97,12 +114,23 @@ export class ContextMenuContent implements OnDestroy {
|
|
|
97
114
|
}
|
|
98
115
|
}
|
|
99
116
|
|
|
117
|
+
private clampPosition(): void {
|
|
118
|
+
if (typeof window === 'undefined') return;
|
|
119
|
+
const menu = this._elementRef.nativeElement.querySelector('[role="menu"]') as HTMLElement;
|
|
120
|
+
if (!menu) return;
|
|
121
|
+
const pos = this.context.position();
|
|
122
|
+
const rect = menu.getBoundingClientRect();
|
|
123
|
+
const padding = 8;
|
|
124
|
+
const x = Math.max(padding, Math.min(pos.x, window.innerWidth - rect.width - padding));
|
|
125
|
+
const y = Math.max(padding, Math.min(pos.y, window.innerHeight - rect.height - padding));
|
|
126
|
+
this.clampedPos.set({ x, y });
|
|
127
|
+
}
|
|
100
128
|
private updateMenuItems(): void {
|
|
101
129
|
const content = this._elementRef.nativeElement.querySelector('[role="menu"]');
|
|
102
130
|
if (content) {
|
|
103
131
|
this.menuItems = Array.from(
|
|
104
132
|
content.querySelectorAll(
|
|
105
|
-
'[role="menuitem"]:not([aria-disabled="true"]):not([data-disabled])',
|
|
133
|
+
':is([role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]):not([aria-disabled="true"]):not([data-disabled=""])',
|
|
106
134
|
),
|
|
107
135
|
);
|
|
108
136
|
}
|
|
@@ -194,8 +222,12 @@ export class ContextMenuContent implements OnDestroy {
|
|
|
194
222
|
triggerEl.focus();
|
|
195
223
|
}
|
|
196
224
|
}
|
|
197
|
-
protected onDocumentClick(): void {
|
|
198
|
-
|
|
225
|
+
protected onDocumentClick(event: MouseEvent): void {
|
|
226
|
+
const target = event.target as HTMLElement;
|
|
227
|
+
const host = this._elementRef.nativeElement;
|
|
228
|
+
if (!host.contains(target)) {
|
|
229
|
+
this.close();
|
|
230
|
+
}
|
|
199
231
|
}
|
|
200
232
|
protected onEscapeKey(): void {
|
|
201
233
|
this.close();
|