@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
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Injector,
|
|
12
12
|
input,
|
|
13
13
|
OnDestroy,
|
|
14
|
+
signal,
|
|
14
15
|
viewChild,
|
|
15
16
|
} from '@angular/core';
|
|
16
17
|
import { DRAWER_CONTEXT } from './drawer-context';
|
|
@@ -35,6 +36,7 @@ import { DRAWER_CONTEXT } from './drawer-context';
|
|
|
35
36
|
<div
|
|
36
37
|
#contentEl
|
|
37
38
|
[class]="computedClass()"
|
|
39
|
+
[style]="swipeStyle()"
|
|
38
40
|
[attr.data-state]="context.open() ? 'open' : 'closed'"
|
|
39
41
|
role="dialog"
|
|
40
42
|
aria-modal="true"
|
|
@@ -44,6 +46,9 @@ import { DRAWER_CONTEXT } from './drawer-context';
|
|
|
44
46
|
hlmFocusTrap
|
|
45
47
|
[trapFocus]="context.open()"
|
|
46
48
|
(keydown.escape)="onEscapeKey()"
|
|
49
|
+
(touchstart)="onTouchStart($event)"
|
|
50
|
+
(touchmove)="onTouchMove($event)"
|
|
51
|
+
(touchend)="onTouchEnd()"
|
|
47
52
|
>
|
|
48
53
|
<!-- Handle -->
|
|
49
54
|
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted"></div>
|
|
@@ -105,6 +110,17 @@ export class DrawerContent implements OnDestroy {
|
|
|
105
110
|
);
|
|
106
111
|
});
|
|
107
112
|
|
|
113
|
+
protected readonly swipeDelta = signal(0);
|
|
114
|
+
private touchStartCoord = 0;
|
|
115
|
+
|
|
116
|
+
protected readonly swipeStyle = computed(() => {
|
|
117
|
+
const delta = this.swipeDelta();
|
|
118
|
+
if (delta === 0) return '';
|
|
119
|
+
const dir = this.context.direction;
|
|
120
|
+
if (dir === 'bottom' || dir === 'top') return { transform: `translateY(${delta}px)` };
|
|
121
|
+
return { transform: `translateX(${delta}px)` };
|
|
122
|
+
});
|
|
123
|
+
|
|
108
124
|
/** Previous body overflow style for restoration */
|
|
109
125
|
private previousBodyOverflow = '';
|
|
110
126
|
|
|
@@ -119,6 +135,34 @@ export class DrawerContent implements OnDestroy {
|
|
|
119
135
|
onEscapeKey(): void {
|
|
120
136
|
this.context.setOpen(false);
|
|
121
137
|
}
|
|
138
|
+
onTouchStart(event: TouchEvent): void {
|
|
139
|
+
const touch = event.touches[0];
|
|
140
|
+
const dir = this.context.direction;
|
|
141
|
+
this.touchStartCoord = dir === 'bottom' || dir === 'top' ? touch.clientY : touch.clientX;
|
|
142
|
+
}
|
|
143
|
+
onTouchMove(event: TouchEvent): void {
|
|
144
|
+
const touch = event.touches[0];
|
|
145
|
+
const dir = this.context.direction;
|
|
146
|
+
const coord = dir === 'bottom' || dir === 'top' ? touch.clientY : touch.clientX;
|
|
147
|
+
const rawDelta = coord - this.touchStartCoord;
|
|
148
|
+
// Only allow dragging in the "away" direction
|
|
149
|
+
const isAway =
|
|
150
|
+
(dir === 'bottom' && rawDelta > 0) ||
|
|
151
|
+
(dir === 'top' && rawDelta < 0) ||
|
|
152
|
+
(dir === 'right' && rawDelta > 0) ||
|
|
153
|
+
(dir === 'left' && rawDelta < 0);
|
|
154
|
+
this.swipeDelta.set(isAway ? rawDelta : 0);
|
|
155
|
+
}
|
|
156
|
+
onTouchEnd(): void {
|
|
157
|
+
const threshold = 80;
|
|
158
|
+
if (Math.abs(this.swipeDelta()) >= threshold) {
|
|
159
|
+
this.swipeDelta.set(0);
|
|
160
|
+
this.context.setOpen(false);
|
|
161
|
+
} else {
|
|
162
|
+
this.swipeDelta.set(0);
|
|
163
|
+
}
|
|
164
|
+
this.touchStartCoord = 0;
|
|
165
|
+
}
|
|
122
166
|
|
|
123
167
|
private focusFirstElement(): void {
|
|
124
168
|
setTimeout(() => {
|
|
@@ -55,29 +55,31 @@ export class DropdownMenuContent implements OnDestroy {
|
|
|
55
55
|
effect(() => {
|
|
56
56
|
if (this.context.open()) {
|
|
57
57
|
if (this.strategy() === 'fixed') {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
if (typeof window !== 'undefined') {
|
|
59
|
+
const trigger = this.context.triggerElement();
|
|
60
|
+
if (trigger) {
|
|
61
|
+
const rect = trigger.getBoundingClientRect();
|
|
62
|
+
const side = this.side();
|
|
63
|
+
const offset = this.sideOffset();
|
|
64
|
+
let top = rect.top;
|
|
65
|
+
let left = rect.left;
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
if (side === 'bottom') {
|
|
68
|
+
top = rect.bottom + offset;
|
|
69
|
+
left = rect.left;
|
|
70
|
+
} else if (side === 'top') {
|
|
71
|
+
top = rect.top - offset;
|
|
72
|
+
left = rect.left;
|
|
73
|
+
} else if (side === 'right') {
|
|
74
|
+
top = rect.top;
|
|
75
|
+
left = rect.right + offset;
|
|
76
|
+
} else if (side === 'left') {
|
|
77
|
+
top = rect.top;
|
|
78
|
+
left = rect.left - offset;
|
|
79
|
+
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
this.fixedPos.set({ top, left });
|
|
82
|
+
}
|
|
81
83
|
}
|
|
82
84
|
} else {
|
|
83
85
|
this.fixedPos.set(null);
|
|
@@ -111,7 +113,7 @@ export class DropdownMenuContent implements OnDestroy {
|
|
|
111
113
|
/** Additional CSS classes */
|
|
112
114
|
readonly class = input<string>('');
|
|
113
115
|
/** Positioning strategy: 'absolute' stays within parent, 'fixed' escapes overflow containers */
|
|
114
|
-
readonly strategy = input<'absolute' | 'fixed'>('
|
|
116
|
+
readonly strategy = input<'absolute' | 'fixed'>('fixed');
|
|
115
117
|
|
|
116
118
|
private readonly _elementRef = inject(ElementRef);
|
|
117
119
|
|
|
@@ -198,7 +200,7 @@ export class DropdownMenuContent implements OnDestroy {
|
|
|
198
200
|
if (content) {
|
|
199
201
|
this.menuItems = Array.from(
|
|
200
202
|
content.querySelectorAll(
|
|
201
|
-
'[role="menuitem"]:not([aria-disabled="true"]):not([data-disabled])',
|
|
203
|
+
'[role="menuitem"]:not([aria-disabled="true"]):not([data-disabled=""])',
|
|
202
204
|
),
|
|
203
205
|
);
|
|
204
206
|
}
|
|
@@ -43,9 +43,11 @@ export class DropdownMenuSubContent {
|
|
|
43
43
|
);
|
|
44
44
|
|
|
45
45
|
protected onMouseEnter(): void {
|
|
46
|
+
this.subContext.isMouseInSubContent.set(true);
|
|
46
47
|
this.subContext.open.set(true);
|
|
47
48
|
}
|
|
48
49
|
protected onMouseLeave(): void {
|
|
50
|
+
this.subContext.isMouseInSubContent.set(false);
|
|
49
51
|
this.subContext.open.set(false);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -19,7 +19,12 @@ import { DROPDOWN_MENU_SUB_CONTEXT } from './dropdown-menu-sub.component';
|
|
|
19
19
|
'[class]': 'computedClass()',
|
|
20
20
|
'(mouseenter)': 'onMouseEnter()',
|
|
21
21
|
'(mouseleave)': 'onMouseLeave()',
|
|
22
|
+
'(keydown)': 'onKeyDown($event)',
|
|
22
23
|
'[attr.data-state]': 'subContext.open() ? "open" : "closed"',
|
|
24
|
+
'role': 'menuitem',
|
|
25
|
+
'[attr.aria-haspopup]': '"menu"',
|
|
26
|
+
'[attr.aria-expanded]': 'subContext.open()',
|
|
27
|
+
'[attr.tabindex]': '"-1"',
|
|
23
28
|
},
|
|
24
29
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
25
30
|
})
|
|
@@ -46,9 +51,30 @@ export class DropdownMenuSubTrigger {
|
|
|
46
51
|
this.subContext.open.set(true);
|
|
47
52
|
}
|
|
48
53
|
protected onMouseLeave(): void {
|
|
49
|
-
// Delay closing to allow mouse to move to sub-content
|
|
50
54
|
setTimeout(() => {
|
|
51
|
-
|
|
55
|
+
if (!this.subContext.isMouseInSubContent()) {
|
|
56
|
+
this.subContext.open.set(false);
|
|
57
|
+
}
|
|
52
58
|
}, 100);
|
|
53
59
|
}
|
|
60
|
+
protected onKeyDown(event: KeyboardEvent): void {
|
|
61
|
+
if (event.key === 'ArrowRight' || event.key === 'Enter' || event.key === ' ') {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
this.subContext.open.set(true);
|
|
65
|
+
// Focus first focusable item in sub-content after it renders
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
const subContent = (event.target as HTMLElement)
|
|
68
|
+
.closest('DropdownMenuSub')
|
|
69
|
+
?.querySelector<HTMLElement>('[role="menu"] [role="menuitem"]:not([aria-disabled="true"]):not([data-disabled=""])');
|
|
70
|
+
if (subContent) {
|
|
71
|
+
subContent.focus();
|
|
72
|
+
}
|
|
73
|
+
}, 10);
|
|
74
|
+
}
|
|
75
|
+
if (event.key === 'ArrowLeft') {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
this.subContext.open.set(false);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
54
80
|
}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export interface DropdownMenuSubContext {
|
|
10
10
|
open: WritableSignal<boolean>;
|
|
11
|
+
/** True while the mouse is hovering over the sub-content panel */
|
|
12
|
+
isMouseInSubContent: WritableSignal<boolean>;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export const DROPDOWN_MENU_SUB_CONTEXT = new InjectionToken<DropdownMenuSubContext>(
|
|
@@ -26,6 +28,7 @@ export const DROPDOWN_MENU_SUB_CONTEXT = new InjectionToken<DropdownMenuSubConte
|
|
|
26
28
|
provide: DROPDOWN_MENU_SUB_CONTEXT,
|
|
27
29
|
useFactory: (): DropdownMenuSubContext => ({
|
|
28
30
|
open: signal(false),
|
|
31
|
+
isMouseInSubContent: signal(false),
|
|
29
32
|
}),
|
|
30
33
|
},
|
|
31
34
|
],
|
|
@@ -36,6 +36,31 @@ export class DropdownMenuTrigger {
|
|
|
36
36
|
this.context.open.set(true);
|
|
37
37
|
this.context.focusedIndex.set(0);
|
|
38
38
|
break;
|
|
39
|
+
case 'ArrowUp':
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
this.context.triggerElement.set(this._elementRef.nativeElement);
|
|
42
|
+
this.context.open.set(true);
|
|
43
|
+
// Set focusedIndex to -1 so the content effect opens normally,
|
|
44
|
+
// then after it renders and focuses the first item we move to last.
|
|
45
|
+
this.context.focusedIndex.set(-1);
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
// After the content's own setTimeout(0) has run and focused item[0],
|
|
48
|
+
// query the menu items and focus the last one.
|
|
49
|
+
const menu = document.querySelector('[data-slot="dropdown-menu-content"] [role="menu"]');
|
|
50
|
+
if (menu) {
|
|
51
|
+
const items = Array.from(
|
|
52
|
+
menu.querySelectorAll<HTMLElement>(
|
|
53
|
+
'[role="menuitem"]:not([aria-disabled="true"]):not([data-disabled=""])',
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
if (items.length > 0) {
|
|
57
|
+
const lastIndex = items.length - 1;
|
|
58
|
+
items[lastIndex].focus();
|
|
59
|
+
this.context.focusedIndex.set(lastIndex);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}, 10);
|
|
63
|
+
break;
|
|
39
64
|
case 'Enter':
|
|
40
65
|
case ' ':
|
|
41
66
|
event.preventDefault();
|
|
@@ -8,6 +8,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
8
8
|
selector: 'EmptyAction',
|
|
9
9
|
template: `<ng-content />`,
|
|
10
10
|
host: {
|
|
11
|
+
'attr.data-slot': '"empty-action"',
|
|
11
12
|
'[class]': 'computedClass()',
|
|
12
13
|
},
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -8,6 +8,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
8
8
|
selector: 'EmptyDescription',
|
|
9
9
|
template: `<ng-content />`,
|
|
10
10
|
host: {
|
|
11
|
+
'attr.data-slot': '"empty-description"',
|
|
11
12
|
'[class]': 'computedClass()',
|
|
12
13
|
},
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -8,6 +8,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
8
8
|
selector: 'EmptyIcon',
|
|
9
9
|
template: `<ng-content />`,
|
|
10
10
|
host: {
|
|
11
|
+
'attr.data-slot': '"empty-icon"',
|
|
11
12
|
'[class]': 'computedClass()',
|
|
12
13
|
},
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -19,7 +20,7 @@ export class EmptyIcon {
|
|
|
19
20
|
/** Computed class combining base styles and custom classes */
|
|
20
21
|
protected readonly computedClass = computed(() =>
|
|
21
22
|
cn(
|
|
22
|
-
'mx-auto flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground [&>svg]:size-6',
|
|
23
|
+
'mx-auto flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground [&>svg]:size-6 [&>lucide-icon]:size-6',
|
|
23
24
|
this.class(),
|
|
24
25
|
),
|
|
25
26
|
);
|
|
@@ -8,6 +8,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
8
8
|
selector: 'EmptyTitle',
|
|
9
9
|
template: `<ng-content />`,
|
|
10
10
|
host: {
|
|
11
|
+
'attr.data-slot': '"empty-title"',
|
|
11
12
|
'[class]': 'computedClass()',
|
|
12
13
|
},
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -30,6 +30,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
30
30
|
selector: 'Empty',
|
|
31
31
|
template: `<ng-content />`,
|
|
32
32
|
host: {
|
|
33
|
+
'attr.data-slot': '"empty"',
|
|
33
34
|
'[class]': 'computedClass()',
|
|
34
35
|
},
|
|
35
36
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FieldContent component - groups a field's title/label and description,
|
|
6
|
+
* useful in horizontal orientation next to a control.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <Field orientation="horizontal">
|
|
10
|
+
* <FieldContent>
|
|
11
|
+
* <FieldTitle>Enable notifications</FieldTitle>
|
|
12
|
+
* <FieldDescription>Receive updates about your account.</FieldDescription>
|
|
13
|
+
* </FieldContent>
|
|
14
|
+
* <Switch />
|
|
15
|
+
* </Field>
|
|
16
|
+
*/
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'FieldContent',
|
|
19
|
+
template: `<ng-content />`,
|
|
20
|
+
host: {
|
|
21
|
+
'attr.data-slot': '"field-content"',
|
|
22
|
+
'[class]': 'computedClass()',
|
|
23
|
+
},
|
|
24
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
25
|
+
})
|
|
26
|
+
export class FieldContent {
|
|
27
|
+
/** Additional CSS classes to apply */
|
|
28
|
+
readonly class = input<string>('');
|
|
29
|
+
|
|
30
|
+
/** Computed class combining base styles and custom classes */
|
|
31
|
+
protected readonly computedClass = computed(() =>
|
|
32
|
+
cn('group/field-content flex flex-1 flex-col gap-1.5 leading-snug', this.class()),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FieldDescription component - helper text for a Field.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <Field>
|
|
9
|
+
* <FieldLabel htmlFor="email">Email</FieldLabel>
|
|
10
|
+
* <input Input id="email" type="email" />
|
|
11
|
+
* <FieldDescription>We will never share your email.</FieldDescription>
|
|
12
|
+
* </Field>
|
|
13
|
+
*/
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'FieldDescription',
|
|
16
|
+
template: `<ng-content />`,
|
|
17
|
+
host: {
|
|
18
|
+
'attr.data-slot': '"field-description"',
|
|
19
|
+
'[class]': 'computedClass()',
|
|
20
|
+
},
|
|
21
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
22
|
+
})
|
|
23
|
+
export class FieldDescription {
|
|
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(
|
|
30
|
+
'text-muted-foreground text-sm leading-normal font-normal',
|
|
31
|
+
'group-has-[[data-orientation=horizontal]]/field:text-balance',
|
|
32
|
+
this.class(),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FieldError component - validation error message(s) for a Field.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <!-- With projected content -->
|
|
9
|
+
* <FieldError>This field is required.</FieldError>
|
|
10
|
+
*
|
|
11
|
+
* <!-- With an errors array -->
|
|
12
|
+
* <FieldError [errors]="['Too short.', 'Must include a number.']" />
|
|
13
|
+
*/
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'FieldError',
|
|
16
|
+
template: `
|
|
17
|
+
@if (errors() && errors()!.length > 0) {
|
|
18
|
+
@if (errors()!.length === 1) {
|
|
19
|
+
{{ errors()![0] }}
|
|
20
|
+
} @else {
|
|
21
|
+
<ul class="ml-4 flex list-disc flex-col gap-1">
|
|
22
|
+
@for (error of errors(); track error) {
|
|
23
|
+
<li>{{ error }}</li>
|
|
24
|
+
}
|
|
25
|
+
</ul>
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
<ng-content />
|
|
29
|
+
`,
|
|
30
|
+
host: {
|
|
31
|
+
'attr.data-slot': '"field-error"',
|
|
32
|
+
role: 'alert',
|
|
33
|
+
'[class]': 'computedClass()',
|
|
34
|
+
},
|
|
35
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
36
|
+
})
|
|
37
|
+
export class FieldError {
|
|
38
|
+
/** List of error messages to render. When omitted, projected content is shown. */
|
|
39
|
+
readonly errors = input<string[] | undefined>(undefined);
|
|
40
|
+
|
|
41
|
+
/** Additional CSS classes to apply */
|
|
42
|
+
readonly class = input<string>('');
|
|
43
|
+
|
|
44
|
+
/** Computed class combining base styles and custom classes */
|
|
45
|
+
protected readonly computedClass = computed(() =>
|
|
46
|
+
cn('text-destructive text-sm font-normal', this.class()),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FieldGroup component - container that stacks multiple Field components.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <FieldGroup>
|
|
9
|
+
* <Field>...</Field>
|
|
10
|
+
* <FieldSeparator />
|
|
11
|
+
* <Field>...</Field>
|
|
12
|
+
* </FieldGroup>
|
|
13
|
+
*/
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'FieldGroup',
|
|
16
|
+
template: `<ng-content />`,
|
|
17
|
+
host: {
|
|
18
|
+
'attr.data-slot': '"field-group"',
|
|
19
|
+
'[class]': 'computedClass()',
|
|
20
|
+
},
|
|
21
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
22
|
+
})
|
|
23
|
+
export class FieldGroup {
|
|
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(
|
|
30
|
+
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3',
|
|
31
|
+
this.class(),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FieldLabel component - label for a form control inside a Field.
|
|
6
|
+
* Renders a native label element for accessibility.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <FieldLabel htmlFor="username">Username</FieldLabel>
|
|
10
|
+
* <input Input id="username" />
|
|
11
|
+
*/
|
|
12
|
+
@Component({
|
|
13
|
+
selector: 'FieldLabel',
|
|
14
|
+
template: `
|
|
15
|
+
<label class="contents" [attr.for]="forId()">
|
|
16
|
+
<ng-content />
|
|
17
|
+
</label>
|
|
18
|
+
`,
|
|
19
|
+
host: {
|
|
20
|
+
'attr.data-slot': '"field-label"',
|
|
21
|
+
'[class]': 'computedClass()',
|
|
22
|
+
},
|
|
23
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
24
|
+
})
|
|
25
|
+
export class FieldLabel {
|
|
26
|
+
/** ID of the form element this label is for */
|
|
27
|
+
readonly for = input<string>();
|
|
28
|
+
|
|
29
|
+
/** Alternative binding for 'for' attribute (React-style) */
|
|
30
|
+
readonly htmlFor = input<string>();
|
|
31
|
+
|
|
32
|
+
/** Additional CSS classes to apply */
|
|
33
|
+
readonly class = input<string>('');
|
|
34
|
+
|
|
35
|
+
/** Computed ID - prefers 'for' over 'htmlFor' */
|
|
36
|
+
protected readonly forId = computed(() => this.for() || this.htmlFor());
|
|
37
|
+
|
|
38
|
+
/** Computed class combining base styles and custom classes */
|
|
39
|
+
protected readonly computedClass = computed(() =>
|
|
40
|
+
cn(
|
|
41
|
+
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
|
42
|
+
'text-sm font-medium',
|
|
43
|
+
this.class(),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
export type FieldLegendVariant = 'legend' | 'label';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* FieldLegend component - legend/title for a FieldSet.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* <FieldLegend>Address Information</FieldLegend>
|
|
11
|
+
*
|
|
12
|
+
* <!-- Label-sized legend -->
|
|
13
|
+
* <FieldLegend variant="label">Notifications</FieldLegend>
|
|
14
|
+
*/
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'FieldLegend',
|
|
17
|
+
template: `<ng-content />`,
|
|
18
|
+
host: {
|
|
19
|
+
'attr.data-slot': '"field-legend"',
|
|
20
|
+
'[attr.data-variant]': 'variant()',
|
|
21
|
+
'[class]': 'computedClass()',
|
|
22
|
+
},
|
|
23
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
24
|
+
})
|
|
25
|
+
export class FieldLegend {
|
|
26
|
+
/** The visual variant of the legend */
|
|
27
|
+
readonly variant = input<FieldLegendVariant>('legend');
|
|
28
|
+
|
|
29
|
+
/** Additional CSS classes to apply */
|
|
30
|
+
readonly class = input<string>('');
|
|
31
|
+
|
|
32
|
+
/** Computed class combining base styles and custom classes */
|
|
33
|
+
protected readonly computedClass = computed(() =>
|
|
34
|
+
cn(
|
|
35
|
+
'mb-3 font-medium',
|
|
36
|
+
'data-[variant=legend]:text-base',
|
|
37
|
+
'data-[variant=label]:text-sm',
|
|
38
|
+
this.class(),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FieldSeparator component - visual divider between fields,
|
|
6
|
+
* with optional centered content text (e.g. "Or continue with").
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <!-- Plain separator -->
|
|
10
|
+
* <FieldSeparator />
|
|
11
|
+
*
|
|
12
|
+
* <!-- With centered content -->
|
|
13
|
+
* <FieldSeparator content="Or continue with" />
|
|
14
|
+
*/
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'FieldSeparator',
|
|
17
|
+
template: `
|
|
18
|
+
<div class="bg-border absolute inset-0 top-1/2 h-px w-full" aria-hidden="true"></div>
|
|
19
|
+
@if (content()) {
|
|
20
|
+
<span
|
|
21
|
+
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
|
22
|
+
data-slot="field-separator-content"
|
|
23
|
+
>
|
|
24
|
+
{{ content() }}
|
|
25
|
+
</span>
|
|
26
|
+
}
|
|
27
|
+
`,
|
|
28
|
+
host: {
|
|
29
|
+
'attr.data-slot': '"field-separator"',
|
|
30
|
+
'[attr.data-content]': 'hasContent()',
|
|
31
|
+
'[class]': 'computedClass()',
|
|
32
|
+
},
|
|
33
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
34
|
+
})
|
|
35
|
+
export class FieldSeparator {
|
|
36
|
+
/** Optional content text rendered centered on the separator line */
|
|
37
|
+
readonly content = input<string>('');
|
|
38
|
+
|
|
39
|
+
/** Additional CSS classes to apply */
|
|
40
|
+
readonly class = input<string>('');
|
|
41
|
+
|
|
42
|
+
/** Whether content text is present */
|
|
43
|
+
protected readonly hasContent = computed(() => !!this.content());
|
|
44
|
+
|
|
45
|
+
/** Computed class combining base styles and custom classes */
|
|
46
|
+
protected readonly computedClass = computed(() =>
|
|
47
|
+
cn('relative -my-2 h-5 text-sm', this.class()),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FieldSet component - groups related fields together with fieldset semantics.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <FieldSet>
|
|
9
|
+
* <FieldLegend>Profile</FieldLegend>
|
|
10
|
+
* <FieldGroup>
|
|
11
|
+
* <Field>...</Field>
|
|
12
|
+
* </FieldGroup>
|
|
13
|
+
* </FieldSet>
|
|
14
|
+
*/
|
|
15
|
+
@Component({
|
|
16
|
+
selector: 'FieldSet',
|
|
17
|
+
template: `<ng-content />`,
|
|
18
|
+
host: {
|
|
19
|
+
'attr.data-slot': '"field-set"',
|
|
20
|
+
role: 'group',
|
|
21
|
+
'[class]': 'computedClass()',
|
|
22
|
+
},
|
|
23
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
24
|
+
})
|
|
25
|
+
export class FieldSet {
|
|
26
|
+
/** Additional CSS classes to apply */
|
|
27
|
+
readonly class = input<string>('');
|
|
28
|
+
|
|
29
|
+
/** Computed class combining base styles and custom classes */
|
|
30
|
+
protected readonly computedClass = computed(() =>
|
|
31
|
+
cn(
|
|
32
|
+
'flex flex-col gap-6',
|
|
33
|
+
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
|
|
34
|
+
this.class(),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
}
|