@ng-cn/core 1.0.17 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/package.json +6 -5
  2. package/src/app/lib/components/ui/alert-dialog/alert-dialog-content.component.ts +22 -21
  3. package/src/app/lib/components/ui/avatar/ui-avatar.component.ts +4 -0
  4. package/src/app/lib/components/ui/calendar/calendar.component.ts +70 -13
  5. package/src/app/lib/components/ui/carousel/carousel-content.component.ts +1 -0
  6. package/src/app/lib/components/ui/carousel/carousel-item.component.ts +1 -0
  7. package/src/app/lib/components/ui/carousel/carousel-next.component.ts +1 -0
  8. package/src/app/lib/components/ui/carousel/carousel-previous.component.ts +1 -0
  9. package/src/app/lib/components/ui/carousel/carousel.component.ts +1 -0
  10. package/src/app/lib/components/ui/chart/chart-container.component.ts +1 -0
  11. package/src/app/lib/components/ui/chart/chart-legend-content.component.ts +1 -0
  12. package/src/app/lib/components/ui/chart/chart-legend.component.ts +5 -5
  13. package/src/app/lib/components/ui/chart/chart-tooltip-content.component.ts +5 -5
  14. package/src/app/lib/components/ui/chart/chart-tooltip.component.ts +5 -5
  15. package/src/app/lib/components/ui/chart/chart.component.ts +1 -0
  16. package/src/app/lib/components/ui/checkbox/checkbox.component.ts +1 -1
  17. package/src/app/lib/components/ui/collapsible/collapsible-content.component.ts +2 -1
  18. package/src/app/lib/components/ui/collapsible/collapsible-context.ts +1 -0
  19. package/src/app/lib/components/ui/collapsible/collapsible-trigger.component.ts +1 -0
  20. package/src/app/lib/components/ui/collapsible/collapsible.component.ts +3 -0
  21. package/src/app/lib/components/ui/context-menu/context-menu-content.component.ts +49 -17
  22. package/src/app/lib/components/ui/context-menu/context-menu-sub-content.component.ts +2 -0
  23. package/src/app/lib/components/ui/context-menu/context-menu-sub-trigger.component.ts +30 -1
  24. package/src/app/lib/components/ui/context-menu/context-menu-sub.component.ts +3 -0
  25. package/src/app/lib/components/ui/country-selector/country-data.ts +63 -0
  26. package/src/app/lib/components/ui/country-selector/country-selector.component.ts +199 -0
  27. package/src/app/lib/components/ui/country-selector/index.ts +2 -0
  28. package/src/app/lib/components/ui/date-picker/date-picker.component.ts +48 -5
  29. package/src/app/lib/components/ui/dialog/dialog-content.component.ts +27 -20
  30. package/src/app/lib/components/ui/direction/direction-context.ts +9 -0
  31. package/src/app/lib/components/ui/direction/direction.component.ts +48 -0
  32. package/src/app/lib/components/ui/direction/index.ts +2 -0
  33. package/src/app/lib/components/ui/drawer/drawer-content.component.ts +44 -0
  34. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-content.component.ts +25 -23
  35. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.component.ts +1 -0
  36. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.component.ts +2 -0
  37. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.component.ts +28 -2
  38. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub.component.ts +3 -0
  39. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-trigger.component.ts +25 -0
  40. package/src/app/lib/components/ui/empty/empty-action.component.ts +1 -0
  41. package/src/app/lib/components/ui/empty/empty-description.component.ts +1 -0
  42. package/src/app/lib/components/ui/empty/empty-icon.component.ts +2 -1
  43. package/src/app/lib/components/ui/empty/empty-title.component.ts +1 -0
  44. package/src/app/lib/components/ui/empty/empty.component.ts +1 -0
  45. package/src/app/lib/components/ui/field/field-content.component.ts +34 -0
  46. package/src/app/lib/components/ui/field/field-description.component.ts +35 -0
  47. package/src/app/lib/components/ui/field/field-error.component.ts +48 -0
  48. package/src/app/lib/components/ui/field/field-group.component.ts +34 -0
  49. package/src/app/lib/components/ui/field/field-label.component.ts +46 -0
  50. package/src/app/lib/components/ui/field/field-legend.component.ts +41 -0
  51. package/src/app/lib/components/ui/field/field-separator.component.ts +49 -0
  52. package/src/app/lib/components/ui/field/field-set.component.ts +37 -0
  53. package/src/app/lib/components/ui/field/field-title.component.ts +30 -0
  54. package/src/app/lib/components/ui/field/field.component.ts +66 -0
  55. package/src/app/lib/components/ui/field/index.ts +15 -0
  56. package/src/app/lib/components/ui/form/form-description.component.ts +2 -2
  57. package/src/app/lib/components/ui/hover-card/hover-card-content.component.ts +108 -60
  58. package/src/app/lib/components/ui/hover-card/hover-card-context.ts +4 -2
  59. package/src/app/lib/components/ui/hover-card/hover-card-trigger.component.ts +5 -3
  60. package/src/app/lib/components/ui/hover-card/hover-card.component.ts +8 -3
  61. package/src/app/lib/components/ui/input-group/input-group-addon.component.ts +1 -0
  62. package/src/app/lib/components/ui/input-group/input-group-input.component.ts +1 -0
  63. package/src/app/lib/components/ui/input-group/input-group.component.ts +1 -0
  64. package/src/app/lib/components/ui/item/index.ts +21 -0
  65. package/src/app/lib/components/ui/item/item-actions.component.ts +29 -0
  66. package/src/app/lib/components/ui/item/item-content.component.ts +31 -0
  67. package/src/app/lib/components/ui/item/item-description.component.ts +30 -0
  68. package/src/app/lib/components/ui/item/item-footer.component.ts +30 -0
  69. package/src/app/lib/components/ui/item/item-group.component.ts +32 -0
  70. package/src/app/lib/components/ui/item/item-header.component.ts +30 -0
  71. package/src/app/lib/components/ui/item/item-media.component.ts +63 -0
  72. package/src/app/lib/components/ui/item/item-separator.component.ts +33 -0
  73. package/src/app/lib/components/ui/item/item-title.component.ts +27 -0
  74. package/src/app/lib/components/ui/item/item.component.ts +77 -0
  75. package/src/app/lib/components/ui/menubar/menubar-content.component.ts +1 -1
  76. package/src/app/lib/components/ui/navigation-menu/navigation-menu-content.component.ts +7 -1
  77. package/src/app/lib/components/ui/navigation-menu/navigation-menu-context.ts +14 -0
  78. package/src/app/lib/components/ui/navigation-menu/navigation-menu-item.component.ts +9 -4
  79. package/src/app/lib/components/ui/navigation-menu/navigation-menu-trigger.component.ts +69 -2
  80. package/src/app/lib/components/ui/navigation-menu/navigation-menu.component.ts +32 -4
  81. package/src/app/lib/components/ui/pagination/pagination.component.ts +3 -1
  82. package/src/app/lib/components/ui/phone-input/index.ts +1 -0
  83. package/src/app/lib/components/ui/phone-input/phone-input.component.ts +169 -0
  84. package/src/app/lib/components/ui/popover/popover-content.component.ts +11 -0
  85. package/src/app/lib/components/ui/popover/popover-context.ts +2 -0
  86. package/src/app/lib/components/ui/popover/popover.component.ts +4 -0
  87. package/src/app/lib/components/ui/progress/progress.component.ts +1 -2
  88. package/src/app/lib/components/ui/resizable/resizable-handle.component.ts +2 -2
  89. package/src/app/lib/components/ui/scroll-area/scroll-area.component.ts +7 -6
  90. package/src/app/lib/components/ui/segmented/segmented-item.component.ts +1 -0
  91. package/src/app/lib/components/ui/segmented/segmented.component.ts +1 -0
  92. package/src/app/lib/components/ui/select/select-content.component.ts +35 -15
  93. package/src/app/lib/components/ui/select/select-context.ts +10 -0
  94. package/src/app/lib/components/ui/select/select-item.component.ts +25 -7
  95. package/src/app/lib/components/ui/select/select-trigger.component.ts +6 -13
  96. package/src/app/lib/components/ui/select/select.component.ts +46 -0
  97. package/src/app/lib/components/ui/sheet/sheet-content.component.ts +23 -6
  98. package/src/app/lib/components/ui/slider/slider.component.ts +2 -2
  99. package/src/app/lib/components/ui/sonner/index.ts +2 -0
  100. package/src/app/lib/components/ui/sonner/sonner.component.ts +70 -0
  101. package/src/app/lib/components/ui/switch/switch.component.ts +1 -14
  102. package/src/app/lib/components/ui/tabs/tabs-list.component.ts +20 -0
  103. package/src/app/lib/components/ui/tabs/tabs-trigger.component.ts +0 -1
  104. package/src/app/lib/components/ui/textarea/textarea.component.ts +110 -10
  105. package/src/app/lib/components/ui/toast/toast.service.ts +1 -1
  106. package/src/app/lib/components/ui/toggle/toggle.component.ts +12 -6
  107. package/src/app/lib/components/ui/tooltip/tooltip-content.component.ts +141 -17
  108. package/src/app/lib/components/ui/tooltip/tooltip-context.ts +3 -1
  109. package/src/app/lib/components/ui/tooltip/tooltip-provider.component.ts +1 -1
  110. package/src/app/lib/components/ui/tooltip/tooltip-trigger.component.ts +5 -2
  111. package/src/app/lib/components/ui/tooltip/tooltip.component.ts +3 -1
@@ -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
- const trigger = this.context.triggerElement();
59
- if (trigger) {
60
- const rect = trigger.getBoundingClientRect();
61
- const side = this.side();
62
- const offset = this.sideOffset();
63
- let top = rect.top;
64
- let left = rect.left;
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
- if (side === 'bottom') {
67
- top = rect.bottom + offset;
68
- left = rect.left;
69
- } else if (side === 'top') {
70
- top = rect.top - offset;
71
- left = rect.left;
72
- } else if (side === 'right') {
73
- top = rect.top;
74
- left = rect.right + offset;
75
- } else if (side === 'left') {
76
- top = rect.top;
77
- left = rect.left - offset;
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
- this.fixedPos.set({ top, left });
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'>('absolute');
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
  }
@@ -67,5 +67,6 @@ export class DropdownMenuRadioItem {
67
67
  }
68
68
  this._radioGroupContext.setValue(this.value());
69
69
  this.onSelect.emit();
70
+ this._context.open.set(false);
70
71
  }
71
72
  }
@@ -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
- // Check if mouse is still outside both trigger and content
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
+ }