@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
@@ -16,6 +16,8 @@ export interface PopoverContextValue {
16
16
  triggerRef?: Signal<HTMLElement | null>;
17
17
  /** Set the trigger element reference */
18
18
  setTriggerRef?: (element: HTMLElement | null) => void;
19
+ /** Unique ID for aria-controls relationship */
20
+ contentId: string;
19
21
  }
20
22
 
21
23
  export const POPOVER_CONTEXT = new InjectionToken<PopoverContextValue>('POPOVER_CONTEXT');
@@ -10,6 +10,8 @@ import { POPOVER_CONTEXT, type PopoverContextValue } from './popover-context';
10
10
 
11
11
  export type PopoverState = 'open' | 'closed';
12
12
 
13
+ let idCounter = 0;
14
+
13
15
  /**
14
16
  * Props for the Popover component
15
17
  */
@@ -128,6 +130,8 @@ export class Popover implements PopoverContextValue {
128
130
  readonly controlledOpen = input<boolean | undefined>(undefined, { alias: 'open' });
129
131
 
130
132
  readonly open = signal(false);
133
+ /** Unique ID for aria-controls relationship */
134
+ readonly contentId = `popover-content-${++idCounter}`;
131
135
  /** Reference to the trigger element for positioning */
132
136
  readonly triggerRef = signal<HTMLElement | null>(null);
133
137
 
@@ -102,9 +102,8 @@ export type ProgressProps = {
102
102
  '[attr.aria-label]': 'ariaLabel()',
103
103
  '[attr.aria-valuemin]': '0',
104
104
  '[attr.aria-valuemax]': 'max()',
105
- '[attr.aria-valuenow]': 'value()',
105
+ '[attr.aria-valuenow]': 'value() !== null ? value() : null',
106
106
  '[attr.aria-valuetext]': 'computedValueText()',
107
- '[attr.aria-live]': '"polite"',
108
107
  '[attr.data-state]': 'state()',
109
108
  '[attr.data-value]': 'value()',
110
109
  '[attr.data-max]': 'max()',
@@ -165,7 +165,7 @@ export class ResizableHandle {
165
165
  if (!this.isDragging()) return;
166
166
 
167
167
  const currentPosition = this.context.direction() === 'horizontal' ? e.clientX : e.clientY;
168
- const delta = ((currentPosition - this.startPosition) / window.innerWidth) * 100;
168
+ const delta = ((currentPosition - this.startPosition) / (typeof window !== 'undefined' ? window.innerWidth : 1000)) * 100;
169
169
  this.context.onResize(delta);
170
170
  this.startPosition = currentPosition;
171
171
  };
@@ -194,7 +194,7 @@ export class ResizableHandle {
194
194
  const currentTouch = e.touches[0];
195
195
  const currentPosition =
196
196
  this.context.direction() === 'horizontal' ? currentTouch.clientX : currentTouch.clientY;
197
- const delta = ((currentPosition - this.startPosition) / window.innerWidth) * 100;
197
+ const delta = ((currentPosition - this.startPosition) / (typeof window !== 'undefined' ? window.innerWidth : 1000)) * 100;
198
198
  this.context.onResize(delta);
199
199
  this.startPosition = currentPosition;
200
200
  };
@@ -98,15 +98,16 @@ export class ScrollArea {
98
98
 
99
99
  protected readonly computedClass = computed(() => cn('relative overflow-hidden', this.class()));
100
100
 
101
- protected readonly viewportClass = computed(() =>
102
- cn(
101
+ protected readonly viewportClass = computed(() => {
102
+ const type = this.type();
103
+ return cn(
103
104
  'h-full w-full rounded-[inherit]',
104
105
  '[&>div]:!block',
105
- // Hide native scrollbar
106
+ // Hide native scrollbar (custom ScrollBar component provides visual feedback)
106
107
  '[&::-webkit-scrollbar]:hidden',
107
108
  '[-ms-overflow-style:none]',
108
109
  '[scrollbar-width:none]',
109
- 'overflow-auto',
110
- ),
111
- );
110
+ type === 'always' ? 'overflow-scroll' : 'overflow-auto',
111
+ );
112
+ });
112
113
  }
@@ -13,6 +13,7 @@ import { segmentedItemVariants } from './segmented-variants';
13
13
  selector: 'SegmentedItem',
14
14
  template: `<ng-content />`,
15
15
  host: {
16
+ 'attr.data-slot': '"segmented-item"',
16
17
  '[class]': 'computedClass()',
17
18
  '[attr.role]': 'itemRole()',
18
19
  '[attr.aria-selected]': 'itemRole() === "tab" ? isSelected() : null',
@@ -33,6 +33,7 @@ import { segmentedVariants, type SegmentedVariants } from './segmented-variants'
33
33
  selector: 'Segmented',
34
34
  template: `<ng-content />`,
35
35
  host: {
36
+ 'attr.data-slot': '"segmented"',
36
37
  '[class]': 'computedClass()',
37
38
  role: 'tablist',
38
39
  '[attr.aria-orientation]': '"horizontal"',
@@ -1,4 +1,4 @@
1
- import { cn } from '@/lib/utils';
1
+ import { cn, Presence } from '@/lib/utils';
2
2
  import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
3
3
  import { SELECT_CONTEXT } from './select-context';
4
4
 
@@ -9,25 +9,29 @@ import { SELECT_CONTEXT } from './select-context';
9
9
  */
10
10
  @Component({
11
11
  selector: 'SelectContent',
12
+ imports: [Presence],
12
13
  template: `
13
- <div
14
- [class]="dropdownClass()"
15
- [attr.id]="context?.contentId"
16
- [attr.data-state]="context?.open() ? 'open' : 'closed'"
17
- [attr.data-side]="side()"
18
- [attr.data-align]="align()"
19
- role="listbox"
20
- aria-label="Options"
21
- (keydown.escape)="onEscape()"
22
- >
23
- <div [class]="viewportClass()">
24
- <ng-content />
14
+ <Presence [present]="context?.open() ?? false">
15
+ <div
16
+ [class]="dropdownClass()"
17
+ [attr.id]="context?.contentId"
18
+ [attr.data-state]="context?.open() ? 'open' : 'closed'"
19
+ [attr.data-side]="side()"
20
+ [attr.data-align]="align()"
21
+ [attr.aria-activedescendant]="focusedItemId()"
22
+ role="listbox"
23
+ (keydown.escape)="onEscape()"
24
+ (keydown)="onKeydown($event)"
25
+ >
26
+ <div [class]="viewportClass()">
27
+ <ng-content />
28
+ </div>
25
29
  </div>
26
- </div>
30
+ </Presence>
27
31
  `,
28
32
  host: {
29
33
  class: 'contents',
30
- 'data-slot': 'select-content',
34
+ 'attr.data-slot': '"select-content"',
31
35
  },
32
36
  changeDetection: ChangeDetectionStrategy.OnPush,
33
37
  })
@@ -59,6 +63,15 @@ export class SelectContent {
59
63
  /** Viewport class */
60
64
  protected readonly viewportClass = computed(() => cn('max-h-60 overflow-y-auto p-1'));
61
65
 
66
+ /** ID of the currently focused item for aria-activedescendant */
67
+ protected readonly focusedItemId = computed(() => {
68
+ if (!this.context) return null;
69
+ const values = this.context.itemValues();
70
+ const focusedIndex = this.context.focusedIndex();
71
+ const focusedValue = values[focusedIndex];
72
+ return focusedValue ? `select-item-${focusedValue}` : null;
73
+ });
74
+
62
75
  protected onEscape(): void {
63
76
  this.context?.setOpen(false);
64
77
  const trigger = this.context?.triggerElement();
@@ -66,4 +79,11 @@ export class SelectContent {
66
79
  setTimeout(() => trigger.focus());
67
80
  }
68
81
  }
82
+
83
+ /** Handle printable character keys for typeahead search */
84
+ onKeydown(event: KeyboardEvent): void {
85
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
86
+ this.context?.handleTypeahead(event.key);
87
+ }
88
+ }
69
89
  }
@@ -31,6 +31,16 @@ export interface SelectContext {
31
31
  required?: () => boolean;
32
32
  /** Name for form submission */
33
33
  name?: () => string;
34
+ /** Map of item value → display label for typeahead */
35
+ itemLabels: WritableSignal<Map<string, string>>;
36
+ /** Accumulated typeahead characters */
37
+ typeaheadBuffer: WritableSignal<string>;
38
+ /** Timeout handle for clearing the typeahead buffer */
39
+ typeaheadTimeout: WritableSignal<ReturnType<typeof setTimeout> | null>;
40
+ /** Handle a typed character for typeahead search */
41
+ handleTypeahead: (char: string) => void;
42
+ /** Focus the first item whose label starts with query (case-insensitive) */
43
+ focusMatchingItem: (query: string) => void;
34
44
  }
35
45
 
36
46
  export interface SelectGroupContext {
@@ -47,6 +47,7 @@ import { SELECT_CONTEXT } from './select-context';
47
47
  host: {
48
48
  '[class]': 'computedClass()',
49
49
  role: 'option',
50
+ '[attr.id]': 'itemId()',
50
51
  '[attr.aria-selected]': 'isSelected()',
51
52
  '[attr.data-state]': 'isSelected() ? "checked" : "unchecked"',
52
53
  '[attr.data-disabled]': 'disabled() ? "" : null',
@@ -55,7 +56,7 @@ import { SELECT_CONTEXT } from './select-context';
55
56
  '[attr.tabindex]': 'disabled() ? -1 : 0',
56
57
  '(click)': 'select()',
57
58
  '(keydown)': 'onKeyDown($event)',
58
- 'data-slot': 'select-item',
59
+ 'attr.data-slot': '"select-item"',
59
60
  },
60
61
  changeDetection: ChangeDetectionStrategy.OnPush,
61
62
  })
@@ -72,6 +73,9 @@ export class SelectItem implements OnInit, OnDestroy {
72
73
 
73
74
  private readonly _context = inject(SELECT_CONTEXT, { optional: true });
74
75
 
76
+ /** Stable DOM id for aria-activedescendant */
77
+ readonly itemId = computed(() => `select-item-${this.value()}`);
78
+
75
79
  /** Whether this item is selected */
76
80
  protected readonly isSelected = computed(() => {
77
81
  return this._context?.value() === this.value();
@@ -94,17 +98,28 @@ export class SelectItem implements OnInit, OnDestroy {
94
98
  });
95
99
 
96
100
  ngOnInit(): void {
97
- // Register this item
101
+ // Register this item's value
98
102
  this._context?.itemValues.update((values) => {
99
103
  if (!values.includes(this.value())) {
100
104
  return [...values, this.value()];
101
105
  }
102
106
  return values;
103
107
  });
108
+ // Register this item's label for typeahead
109
+ this._context?.itemLabels.update((m) => {
110
+ const label = this.textContent()?.nativeElement?.textContent?.trim() || this.value();
111
+ m.set(this.value(), label);
112
+ return m;
113
+ });
104
114
  }
105
115
  ngOnDestroy(): void {
106
- // Unregister this item
116
+ // Unregister this item's value
107
117
  this._context?.itemValues.update((values) => values.filter((v) => v !== this.value()));
118
+ // Unregister this item's label
119
+ this._context?.itemLabels.update((m) => {
120
+ m.delete(this.value());
121
+ return m;
122
+ });
108
123
  }
109
124
 
110
125
  /** Handle keyboard navigation */
@@ -117,17 +132,20 @@ export class SelectItem implements OnInit, OnDestroy {
117
132
  event.preventDefault();
118
133
  this.select();
119
134
  break;
120
- case 'ArrowDown':
135
+ case 'ArrowDown': {
121
136
  event.preventDefault();
122
137
  const currentIndex = this._context.focusedIndex();
123
138
  const itemCount = this._context.itemValues().length;
124
- this._context.focusItem(Math.min(currentIndex + 1, itemCount - 1));
139
+ this._context.focusItem((currentIndex + 1) % itemCount);
125
140
  break;
126
- case 'ArrowUp':
141
+ }
142
+ case 'ArrowUp': {
127
143
  event.preventDefault();
128
144
  const idx = this._context.focusedIndex();
129
- this._context.focusItem(Math.max(idx - 1, 0));
145
+ const count = this._context.itemValues().length;
146
+ this._context.focusItem(idx > 0 ? idx - 1 : count - 1);
130
147
  break;
148
+ }
131
149
  case 'Home':
132
150
  event.preventDefault();
133
151
  this._context.focusItem(0);
@@ -57,7 +57,7 @@ import { SELECT_CONTEXT } from './select-context';
57
57
  `,
58
58
  host: {
59
59
  class: 'contents',
60
- 'data-slot': 'select-trigger',
60
+ 'attr.data-slot': '"select-trigger"',
61
61
  },
62
62
  changeDetection: ChangeDetectionStrategy.OnPush,
63
63
  })
@@ -114,35 +114,28 @@ export class SelectTrigger {
114
114
  case 'ArrowDown':
115
115
  event.preventDefault();
116
116
  if (!this.context.open()) {
117
- // Save trigger element
118
117
  const button = this.triggerButton()?.nativeElement;
119
- if (button) {
120
- this.context.triggerElement.set(button);
121
- }
118
+ if (button) this.context.triggerElement.set(button);
122
119
  this.context.setOpen(true);
123
120
  setTimeout(() => this.context?.focusItem(0));
124
121
  } else {
125
- // Move to next item
126
122
  const currentIndex = this.context.focusedIndex();
127
123
  const itemCount = this.context.itemValues().length;
128
- this.context.focusItem(Math.min(currentIndex + 1, itemCount - 1));
124
+ this.context.focusItem((currentIndex + 1) % itemCount);
129
125
  }
130
126
  break;
131
127
  case 'ArrowUp':
132
128
  event.preventDefault();
133
129
  if (!this.context.open()) {
134
- // Save trigger element
135
130
  const button = this.triggerButton()?.nativeElement;
136
- if (button) {
137
- this.context.triggerElement.set(button);
138
- }
131
+ if (button) this.context.triggerElement.set(button);
139
132
  this.context.setOpen(true);
140
133
  const lastIndex = this.context.itemValues().length - 1;
141
134
  setTimeout(() => this.context?.focusItem(lastIndex));
142
135
  } else {
143
- // Move to previous item
144
136
  const currentIndex = this.context.focusedIndex();
145
- this.context.focusItem(Math.max(currentIndex - 1, 0));
137
+ const itemCount = this.context.itemValues().length;
138
+ this.context.focusItem(currentIndex > 0 ? currentIndex - 1 : itemCount - 1);
146
139
  }
147
140
  break;
148
141
  case 'Escape':
@@ -199,6 +199,11 @@ export class Select {
199
199
  private readonly _isDisabled = signal<boolean>(false);
200
200
 
201
201
  private readonly ariaIds = this._ariaIdService.generateMenuIds('select');
202
+ /** Internal typeahead signals */
203
+ private readonly _itemLabels = signal<Map<string, string>>(new Map());
204
+ private readonly _typeaheadBuffer = signal<string>('');
205
+ private readonly _typeaheadTimeout = signal<ReturnType<typeof setTimeout> | null>(null);
206
+
202
207
  /** Context for child components */
203
208
  readonly context: SelectContext = {
204
209
  value: this._value,
@@ -232,6 +237,11 @@ export class Select {
232
237
  }
233
238
  },
234
239
  focusItem: (index: number) => this.focusItemByIndex(index),
240
+ itemLabels: this._itemLabels,
241
+ typeaheadBuffer: this._typeaheadBuffer,
242
+ typeaheadTimeout: this._typeaheadTimeout,
243
+ handleTypeahead: (char: string) => this.handleTypeahead(char),
244
+ focusMatchingItem: (query: string) => this.focusMatchingItem(query),
235
245
  };
236
246
 
237
247
  /** Focus an item by index */
@@ -248,4 +258,40 @@ export class Select {
248
258
  selectItem.focus();
249
259
  }
250
260
  }
261
+
262
+ /** Handle typeahead: accumulate char, clear after 500 ms, then focus matching item */
263
+ private handleTypeahead(char: string): void {
264
+ // Clear existing timeout
265
+ const existingTimeout = this._typeaheadTimeout();
266
+ if (existingTimeout !== null) {
267
+ clearTimeout(existingTimeout);
268
+ }
269
+
270
+ const newBuffer = this._typeaheadBuffer() + char;
271
+ this._typeaheadBuffer.set(newBuffer);
272
+
273
+ const timeout = setTimeout(() => {
274
+ this._typeaheadBuffer.set('');
275
+ this._typeaheadTimeout.set(null);
276
+ }, 500);
277
+ this._typeaheadTimeout.set(timeout);
278
+
279
+ this.focusMatchingItem(newBuffer);
280
+ }
281
+
282
+ /** Focus the first item whose label starts with query (case-insensitive) */
283
+ private focusMatchingItem(query: string): void {
284
+ const values = this.context.itemValues();
285
+ const labels = this._itemLabels();
286
+ const lowerQuery = query.toLowerCase();
287
+
288
+ const matchIndex = values.findIndex((v) => {
289
+ const label = labels.get(v) ?? v;
290
+ return label.toLowerCase().startsWith(lowerQuery);
291
+ });
292
+
293
+ if (matchIndex !== -1) {
294
+ this.focusItemByIndex(matchIndex);
295
+ }
296
+ }
251
297
  }
@@ -83,13 +83,21 @@ import { sheetVariants, type SheetVariants } from './sheet-variants';
83
83
  })
84
84
  export class SheetContent implements OnDestroy {
85
85
  constructor() {
86
- // Handle body scroll lock based on open state (browser-only via effect + afterNextRender)
86
+ let wasOpen = false;
87
+
88
+ // Handle body scroll lock and focus restoration based on open state
87
89
  effect(() => {
88
90
  const isOpen = this.context.open();
89
91
  if (isOpen) {
92
+ wasOpen = true;
90
93
  this.lockBodyScroll();
91
94
  } else {
92
95
  this.unlockBodyScroll();
96
+ // Restore focus whenever sheet closes (covers close button, overlay, Escape, programmatic)
97
+ if (wasOpen) {
98
+ this.restoreFocus();
99
+ }
100
+ wasOpen = false;
93
101
  }
94
102
  });
95
103
  }
@@ -105,13 +113,14 @@ export class SheetContent implements OnDestroy {
105
113
  cn(sheetVariants({ side: this.side() }), this.class()),
106
114
  );
107
115
 
108
- /** Previous body overflow for restoration */
116
+ /** Previous body overflow/padding for restoration */
109
117
  private previousBodyOverflow = '';
118
+ private previousBodyPaddingRight = '';
110
119
 
111
120
  ngOnDestroy(): void {
112
121
  // Restore body scroll
113
122
  this.unlockBodyScroll();
114
- // Restore focus to trigger element
123
+ // Restore focus to trigger element (fallback if not already called via effect)
115
124
  this.restoreFocus();
116
125
  }
117
126
 
@@ -120,25 +129,33 @@ export class SheetContent implements OnDestroy {
120
129
  this.close();
121
130
  }
122
131
  onEscapeKey(): void {
123
- this.close();
132
+ // Guard: only close when the sheet is actually open (not during exit animation)
133
+ if (this.context.open()) {
134
+ this.close();
135
+ }
124
136
  }
125
137
  onClose(): void {
126
138
  this.close();
127
139
  }
128
140
 
129
141
  private lockBodyScroll(): void {
130
- if (typeof document !== 'undefined') {
142
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
143
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
131
144
  this.previousBodyOverflow = document.body.style.overflow;
145
+ this.previousBodyPaddingRight = document.body.style.paddingRight;
132
146
  document.body.style.overflow = 'hidden';
147
+ if (scrollbarWidth > 0) {
148
+ document.body.style.paddingRight = scrollbarWidth + 'px';
149
+ }
133
150
  }
134
151
  }
135
152
  private unlockBodyScroll(): void {
136
153
  if (typeof document !== 'undefined') {
137
154
  document.body.style.overflow = this.previousBodyOverflow;
155
+ document.body.style.paddingRight = this.previousBodyPaddingRight;
138
156
  }
139
157
  }
140
158
  private close(): void {
141
- this.restoreFocus();
142
159
  this.context.setOpen(false);
143
160
  }
144
161
  private restoreFocus(): void {
@@ -346,10 +346,10 @@ export class Slider implements ControlValueAccessor {
346
346
  newValue -= stepValue * increment;
347
347
  break;
348
348
  case 'ArrowUp':
349
- newValue += stepValue;
349
+ newValue += stepValue * (this.inverted() ? -1 : 1);
350
350
  break;
351
351
  case 'ArrowDown':
352
- newValue -= stepValue;
352
+ newValue -= stepValue * (this.inverted() ? -1 : 1);
353
353
  break;
354
354
  case 'PageUp':
355
355
  newValue += largeStep;
@@ -0,0 +1,2 @@
1
+ export { toast } from 'ngx-sonner';
2
+ export { Sonner } from './sonner.component';
@@ -0,0 +1,70 @@
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
+ import { NgxSonnerToaster } from 'ngx-sonner';
3
+
4
+ export { toast } from 'ngx-sonner';
5
+
6
+ /**
7
+ * Sonner component - toast notification toaster.
8
+ * Wraps ngx-sonner's Toaster with shadcn/ui default styling.
9
+ * Place once near the root of your app.
10
+ *
11
+ * @example
12
+ * <!-- In app root template -->
13
+ * <Sonner />
14
+ *
15
+ * <!-- With custom position -->
16
+ * <Sonner position="top-right" />
17
+ *
18
+ * <!-- Then trigger toasts anywhere -->
19
+ * import { toast } from '@/lib/components/ui/sonner';
20
+ * toast('Hello world');
21
+ * toast.success('Saved!');
22
+ * toast.error('Something went wrong');
23
+ */
24
+ @Component({
25
+ selector: 'Sonner',
26
+ imports: [NgxSonnerToaster],
27
+ changeDetection: ChangeDetectionStrategy.OnPush,
28
+ host: {
29
+ 'attr.data-slot': '"sonner"',
30
+ style: 'display: contents',
31
+ },
32
+ template: `
33
+ <ngx-sonner-toaster
34
+ [theme]="theme()"
35
+ [position]="position()"
36
+ [richColors]="richColors()"
37
+ [expand]="expand()"
38
+ [duration]="duration()"
39
+ [visibleToasts]="visibleToasts()"
40
+ [closeButton]="closeButton()"
41
+ [offset]="offset()"
42
+ [dir]="dir()"
43
+ [class]="toasterClass()"
44
+ />
45
+ `,
46
+ })
47
+ export class Sonner {
48
+ /** Visual theme */
49
+ readonly theme = input<'light' | 'dark' | 'system'>('system');
50
+ /** Position of the toaster */
51
+ readonly position = input<
52
+ 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
53
+ >('bottom-right');
54
+ /** Use rich colors for success/error/warning/info */
55
+ readonly richColors = input<boolean>(false);
56
+ /** Expand toasts by default */
57
+ readonly expand = input<boolean>(false);
58
+ /** Default duration in ms */
59
+ readonly duration = input<number>(4000);
60
+ /** Max visible toasts */
61
+ readonly visibleToasts = input<number>(3);
62
+ /** Show close button on each toast */
63
+ readonly closeButton = input<boolean>(false);
64
+ /** Offset from edge */
65
+ readonly offset = input<string | number | null>(null);
66
+ /** Text direction */
67
+ readonly dir = input<'ltr' | 'rtl' | 'auto'>('auto');
68
+ /** Additional CSS classes passed to the toaster */
69
+ readonly toasterClass = input<string>('toaster group');
70
+ }
@@ -103,9 +103,7 @@ export type SwitchProps = {
103
103
  [attr.data-disabled]="isDisabled() ? '' : null"
104
104
  [disabled]="isDisabled()"
105
105
  [class]="trackClass()"
106
- [style.backgroundColor]="checked() ? checkedBgColor() : 'var(--color-input)'"
107
106
  (click)="toggle()"
108
- (keydown)="onKeyDown($event)"
109
107
  >
110
108
  <span data-slot="switch-thumb" [class]="thumbClass()" [attr.data-state]="state()"></span>
111
109
  </button>
@@ -125,7 +123,7 @@ export type SwitchProps = {
125
123
  `,
126
124
  host: {
127
125
  '[class]': 'computedClass()',
128
- 'data-slot': 'switch',
126
+ 'attr.data-slot': '"switch"',
129
127
  },
130
128
  providers: [
131
129
  {
@@ -168,9 +166,6 @@ export class Switch implements ControlValueAccessor {
168
166
  readonly class = input<string>('');
169
167
  /** Additional CSS classes to apply to the inner track button */
170
168
  readonly buttonClass = input<string>('');
171
- /** Background color for checked state (hex, rgb, or CSS color name) */
172
- readonly checkedBgColor = input<string>('rgb(59, 130, 246)');
173
-
174
169
  /** Current state for data attribute */
175
170
  protected readonly state = computed(
176
171
  (): SwitchState => (this.checked() ? 'checked' : 'unchecked'),
@@ -246,12 +241,4 @@ export class Switch implements ControlValueAccessor {
246
241
  }
247
242
  }
248
243
 
249
- /** Handle keyboard events */
250
- protected onKeyDown(event: KeyboardEvent): void {
251
- // Switch should only respond to Space (Enter is handled by button default)
252
- if (event.key === ' ') {
253
- // Let the click handler deal with it
254
- return;
255
- }
256
- }
257
244
  }
@@ -187,6 +187,13 @@ export class TabsList {
187
187
 
188
188
  if (handled) {
189
189
  event.preventDefault();
190
+ if (typeof document === 'undefined') return;
191
+
192
+ // Skip disabled tabs — scan in the movement direction
193
+ const direction =
194
+ event.key === 'ArrowLeft' || event.key === 'ArrowUp' || event.key === 'End' ? -1 : 1;
195
+ newIndex = this.findEnabledIndex(tabValues, newIndex, direction);
196
+
190
197
  const newValue = tabValues[newIndex];
191
198
 
192
199
  // In automatic mode, activate on focus; in manual mode, just focus
@@ -201,7 +208,20 @@ export class TabsList {
201
208
  }
202
209
  }
203
210
 
211
+ private findEnabledIndex(tabValues: string[], startIndex: number, direction: 1 | -1): number {
212
+ const length = tabValues.length;
213
+ let index = startIndex;
214
+ for (let i = 0; i < length; i++) {
215
+ const tabId = this.tabs.getTabId(tabValues[index]);
216
+ const el = typeof document !== 'undefined' ? document.getElementById(tabId) : null;
217
+ if (!el?.hasAttribute('data-disabled')) return index;
218
+ index = ((index + direction) + length) % length;
219
+ }
220
+ return startIndex;
221
+ }
222
+
204
223
  private updateIndicator(): void {
224
+ if (typeof document === 'undefined') return;
205
225
  const activeValue = this.tabs.value();
206
226
  if (!activeValue) return;
207
227
 
@@ -83,7 +83,6 @@ export interface TabsTriggerProps {
83
83
  '[attr.aria-controls]': 'panelId()',
84
84
  '[attr.aria-disabled]': 'disabled() || null',
85
85
  '[attr.tabindex]': 'isActive() ? 0 : -1',
86
- '[attr.disabled]': 'disabled() ? "" : null',
87
86
  '(click)': 'onClick()',
88
87
  '(keydown.enter)': 'onClick()',
89
88
  '(keydown.space)': 'onSpace($event)',