@ng-cn/core 1.0.18 → 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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/app/lib/components/ui/alert-dialog/alert-dialog-content.component.ts +1 -1
  3. package/src/app/lib/components/ui/calendar/calendar.component.ts +65 -12
  4. package/src/app/lib/components/ui/context-menu/context-menu-content.component.ts +1 -0
  5. package/src/app/lib/components/ui/country-selector/country-data.ts +63 -0
  6. package/src/app/lib/components/ui/country-selector/country-selector.component.ts +199 -0
  7. package/src/app/lib/components/ui/country-selector/index.ts +2 -0
  8. package/src/app/lib/components/ui/date-picker/date-picker.component.ts +47 -5
  9. package/src/app/lib/components/ui/dialog/dialog-content.component.ts +1 -1
  10. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-content.component.ts +23 -21
  11. package/src/app/lib/components/ui/field/field-content.component.ts +34 -0
  12. package/src/app/lib/components/ui/field/field-description.component.ts +35 -0
  13. package/src/app/lib/components/ui/field/field-error.component.ts +48 -0
  14. package/src/app/lib/components/ui/field/field-group.component.ts +34 -0
  15. package/src/app/lib/components/ui/field/field-label.component.ts +46 -0
  16. package/src/app/lib/components/ui/field/field-legend.component.ts +41 -0
  17. package/src/app/lib/components/ui/field/field-separator.component.ts +49 -0
  18. package/src/app/lib/components/ui/field/field-set.component.ts +37 -0
  19. package/src/app/lib/components/ui/field/field-title.component.ts +30 -0
  20. package/src/app/lib/components/ui/field/field.component.ts +66 -0
  21. package/src/app/lib/components/ui/field/index.ts +15 -0
  22. package/src/app/lib/components/ui/item/index.ts +21 -0
  23. package/src/app/lib/components/ui/item/item-actions.component.ts +29 -0
  24. package/src/app/lib/components/ui/item/item-content.component.ts +31 -0
  25. package/src/app/lib/components/ui/item/item-description.component.ts +30 -0
  26. package/src/app/lib/components/ui/item/item-footer.component.ts +30 -0
  27. package/src/app/lib/components/ui/item/item-group.component.ts +32 -0
  28. package/src/app/lib/components/ui/item/item-header.component.ts +30 -0
  29. package/src/app/lib/components/ui/item/item-media.component.ts +63 -0
  30. package/src/app/lib/components/ui/item/item-separator.component.ts +33 -0
  31. package/src/app/lib/components/ui/item/item-title.component.ts +27 -0
  32. package/src/app/lib/components/ui/item/item.component.ts +77 -0
  33. package/src/app/lib/components/ui/phone-input/index.ts +1 -0
  34. package/src/app/lib/components/ui/phone-input/phone-input.component.ts +169 -0
  35. package/src/app/lib/components/ui/resizable/resizable-handle.component.ts +2 -2
  36. package/src/app/lib/components/ui/sheet/sheet-content.component.ts +1 -1
  37. package/src/app/lib/components/ui/tabs/tabs-list.component.ts +3 -1
  38. package/src/app/lib/components/ui/textarea/textarea.component.ts +110 -10
  39. package/src/app/lib/components/ui/toast/toast.service.ts +1 -1
@@ -0,0 +1,63 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ /**
6
+ * ItemMedia variants using class-variance-authority.
7
+ * Matches shadcn/ui React item-media exactly.
8
+ */
9
+ export const itemMediaVariants = cva(
10
+ 'flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5',
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: 'bg-transparent',
15
+ icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
16
+ image: 'size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover',
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: 'default',
21
+ },
22
+ },
23
+ );
24
+
25
+ export type ItemMediaVariants = VariantProps<typeof itemMediaVariants>;
26
+ export type ItemMediaVariant = 'default' | 'icon' | 'image';
27
+
28
+ /**
29
+ * ItemMedia component - leading media (icon, image or avatar) for an Item.
30
+ *
31
+ * @example
32
+ * <!-- Icon -->
33
+ * <ItemMedia variant="icon">
34
+ * <lucide-icon [img]="Music" />
35
+ * </ItemMedia>
36
+ *
37
+ * <!-- Image -->
38
+ * <ItemMedia variant="image">
39
+ * <img src="..." alt="..." />
40
+ * </ItemMedia>
41
+ */
42
+ @Component({
43
+ selector: 'ItemMedia',
44
+ template: `<ng-content />`,
45
+ host: {
46
+ 'attr.data-slot': '"item-media"',
47
+ '[attr.data-variant]': 'variant()',
48
+ '[class]': 'computedClass()',
49
+ },
50
+ changeDetection: ChangeDetectionStrategy.OnPush,
51
+ })
52
+ export class ItemMedia {
53
+ /** The visual style variant of the media container */
54
+ readonly variant = input<ItemMediaVariant>('default');
55
+
56
+ /** Additional CSS classes to apply */
57
+ readonly class = input<string>('');
58
+
59
+ /** Computed class combining base styles, variants and custom classes */
60
+ protected readonly computedClass = computed(() =>
61
+ cn(itemMediaVariants({ variant: this.variant() }), this.class()),
62
+ );
63
+ }
@@ -0,0 +1,33 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+
4
+ /**
5
+ * ItemSeparator component - horizontal divider between items in an ItemGroup.
6
+ *
7
+ * @example
8
+ * <ItemGroup>
9
+ * <Item>...</Item>
10
+ * <ItemSeparator />
11
+ * <Item>...</Item>
12
+ * </ItemGroup>
13
+ */
14
+ @Component({
15
+ selector: 'ItemSeparator',
16
+ template: ``,
17
+ host: {
18
+ 'attr.data-slot': '"item-separator"',
19
+ role: 'none',
20
+ 'attr.data-orientation': '"horizontal"',
21
+ '[class]': 'computedClass()',
22
+ },
23
+ changeDetection: ChangeDetectionStrategy.OnPush,
24
+ })
25
+ export class ItemSeparator {
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('bg-border shrink-0 h-px w-full my-0', this.class()),
32
+ );
33
+ }
@@ -0,0 +1,27 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+
4
+ /**
5
+ * ItemTitle component - title text for an Item.
6
+ *
7
+ * @example
8
+ * <ItemTitle>Basic Item</ItemTitle>
9
+ */
10
+ @Component({
11
+ selector: 'ItemTitle',
12
+ template: `<ng-content />`,
13
+ host: {
14
+ 'attr.data-slot': '"item-title"',
15
+ '[class]': 'computedClass()',
16
+ },
17
+ changeDetection: ChangeDetectionStrategy.OnPush,
18
+ })
19
+ export class ItemTitle {
20
+ /** Additional CSS classes to apply */
21
+ readonly class = input<string>('');
22
+
23
+ /** Computed class combining base styles and custom classes */
24
+ protected readonly computedClass = computed(() =>
25
+ cn('flex w-fit items-center gap-2 text-sm leading-snug font-medium', this.class()),
26
+ );
27
+ }
@@ -0,0 +1,77 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ /**
6
+ * Item variants using class-variance-authority.
7
+ * Matches shadcn/ui React item exactly.
8
+ */
9
+ export const itemVariants = cva(
10
+ 'group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: 'bg-transparent',
15
+ outline: 'border-border',
16
+ muted: 'bg-muted/50',
17
+ },
18
+ size: {
19
+ default: 'p-4 gap-4',
20
+ sm: 'py-3 px-4 gap-2.5',
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: 'default',
25
+ size: 'default',
26
+ },
27
+ },
28
+ );
29
+
30
+ export type ItemVariants = VariantProps<typeof itemVariants>;
31
+ export type ItemVariant = 'default' | 'outline' | 'muted';
32
+ export type ItemSize = 'default' | 'sm';
33
+
34
+ /**
35
+ * Item component - a flexible row for displaying content with
36
+ * media, title, description and actions.
37
+ *
38
+ * @example
39
+ * <Item variant="outline">
40
+ * <ItemMedia variant="icon">
41
+ * <lucide-icon [img]="User" />
42
+ * </ItemMedia>
43
+ * <ItemContent>
44
+ * <ItemTitle>Profile</ItemTitle>
45
+ * <ItemDescription>Manage your profile settings.</ItemDescription>
46
+ * </ItemContent>
47
+ * <ItemActions>
48
+ * <Button size="sm">Edit</Button>
49
+ * </ItemActions>
50
+ * </Item>
51
+ */
52
+ @Component({
53
+ selector: 'Item',
54
+ template: `<ng-content />`,
55
+ host: {
56
+ 'attr.data-slot': '"item"',
57
+ '[attr.data-variant]': 'variant()',
58
+ '[attr.data-size]': 'size()',
59
+ '[class]': 'computedClass()',
60
+ },
61
+ changeDetection: ChangeDetectionStrategy.OnPush,
62
+ })
63
+ export class Item {
64
+ /** The visual style variant of the item */
65
+ readonly variant = input<ItemVariant>('default');
66
+
67
+ /** The size of the item */
68
+ readonly size = input<ItemSize>('default');
69
+
70
+ /** Additional CSS classes to apply */
71
+ readonly class = input<string>('');
72
+
73
+ /** Computed class combining base styles, variants and custom classes */
74
+ protected readonly computedClass = computed(() =>
75
+ cn(itemVariants({ variant: this.variant(), size: this.size() }), this.class()),
76
+ );
77
+ }
@@ -0,0 +1 @@
1
+ export { PhoneInput } from './phone-input.component';
@@ -0,0 +1,169 @@
1
+ import { cn } from '@/lib/utils';
2
+ import {
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ computed,
6
+ forwardRef,
7
+ input,
8
+ signal,
9
+ } from '@angular/core';
10
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
11
+ import { CountrySelector } from '../country-selector/country-selector.component';
12
+ import { COUNTRIES, type Country, getCountryByCode } from '../country-selector/country-data';
13
+
14
+ /**
15
+ * PhoneInput component
16
+ *
17
+ * Combines a CountrySelector (dial code) with a number input.
18
+ * Implements ControlValueAccessor — value is the full phone string (e.g. "+1 555-123-4567").
19
+ *
20
+ * @example
21
+ * ```html
22
+ * <PhoneInput [(ngModel)]="phone" placeholder="Enter phone number" />
23
+ * ```
24
+ */
25
+ @Component({
26
+ selector: 'PhoneInput',
27
+ imports: [CountrySelector],
28
+ template: `
29
+ <div [class]="containerClass()">
30
+ <CountrySelector
31
+ [disabled]="isDisabled()"
32
+ [class]="'rounded-r-none border-r-0 shrink-0 w-auto min-w-[120px]'"
33
+ [placeholder]="'Country'"
34
+ [value]="selectedCountryCode()"
35
+ (countryChange)="onCountryChange($event)"
36
+ />
37
+ <input
38
+ type="tel"
39
+ [placeholder]="placeholder()"
40
+ [disabled]="isDisabled()"
41
+ [value]="localNumber()"
42
+ (input)="onNumberInput($event)"
43
+ (blur)="onTouched()"
44
+ [class]="inputClass()"
45
+ />
46
+ </div>
47
+ `,
48
+ host: {
49
+ 'attr.data-slot': '"phone-input"',
50
+ '[class]': 'hostClass()',
51
+ },
52
+ providers: [
53
+ {
54
+ provide: NG_VALUE_ACCESSOR,
55
+ useExisting: forwardRef(() => PhoneInput),
56
+ multi: true,
57
+ },
58
+ ],
59
+ changeDetection: ChangeDetectionStrategy.OnPush,
60
+ })
61
+ export class PhoneInput implements ControlValueAccessor {
62
+ /** Placeholder for the number input */
63
+ readonly placeholder = input<string>('Phone number');
64
+ /** Whether the input is disabled */
65
+ readonly disabled = input<boolean>(false);
66
+ /** Additional CSS classes */
67
+ readonly class = input<string>('');
68
+
69
+ /** Internal country code (ISO 2-letter) */
70
+ protected readonly selectedCountryCode = signal<string>('US');
71
+ /** Internal local number (without dial code) */
72
+ protected readonly localNumber = signal<string>('');
73
+ /** Internal disabled state from CVA */
74
+ private readonly _isDisabledFromCVA = signal<boolean>(false);
75
+
76
+ protected readonly isDisabled = computed(() => this.disabled() || this._isDisabledFromCVA());
77
+
78
+ protected readonly hostClass = computed(() => cn('block', this.class()));
79
+
80
+ protected readonly containerClass = computed(() =>
81
+ cn(
82
+ 'flex items-stretch w-full',
83
+ this.isDisabled() && 'pointer-events-none opacity-50',
84
+ ),
85
+ );
86
+
87
+ protected readonly inputClass = computed(() =>
88
+ cn(
89
+ 'flex-1 min-w-0 h-10 rounded-xl rounded-l-none border px-3 py-2 text-sm',
90
+ 'bg-zinc-50 dark:bg-zinc-800/50 border-zinc-300 dark:border-zinc-700/50',
91
+ 'text-zinc-900 dark:text-zinc-50 placeholder:text-zinc-500',
92
+ 'shadow-xs transition-[color,box-shadow] outline-none',
93
+ 'focus-visible:border-primary/30 dark:focus-visible:border-white/30 focus-visible:ring-primary/20 dark:focus-visible:ring-white/20 focus-visible:ring-2',
94
+ 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
95
+ ),
96
+ );
97
+
98
+ /** ControlValueAccessor callbacks */
99
+ private onChange: (value: string) => void = () => {};
100
+ protected onTouched: () => void = () => {};
101
+
102
+ writeValue(value: string): void {
103
+ if (!value) {
104
+ this.localNumber.set('');
105
+ return;
106
+ }
107
+ // Parse: if value starts with a known dial code, split it
108
+ const country = getCountryByCode(this.selectedCountryCode());
109
+ const dialCode = country?.dialCode ?? '';
110
+ if (dialCode && value.startsWith(dialCode)) {
111
+ this.localNumber.set(value.slice(dialCode.length).trimStart());
112
+ } else if (value.startsWith('+')) {
113
+ // Try to find a matching dial code
114
+ const matched = this.findCountryByDialCode(value);
115
+ if (matched) {
116
+ this.selectedCountryCode.set(matched.code);
117
+ this.localNumber.set(value.slice(matched.dialCode.length).trimStart());
118
+ } else {
119
+ this.localNumber.set(value);
120
+ }
121
+ } else {
122
+ this.localNumber.set(value);
123
+ }
124
+ }
125
+
126
+ registerOnChange(fn: (value: string) => void): void {
127
+ this.onChange = fn;
128
+ }
129
+
130
+ registerOnTouched(fn: () => void): void {
131
+ this.onTouched = fn;
132
+ }
133
+
134
+ setDisabledState(isDisabled: boolean): void {
135
+ this._isDisabledFromCVA.set(isDisabled);
136
+ }
137
+
138
+ protected onCountryChange(country: Country): void {
139
+ this.selectedCountryCode.set(country.code);
140
+ this.emitValue();
141
+ this.onTouched();
142
+ }
143
+
144
+ protected onNumberInput(event: Event): void {
145
+ const target = event.target as HTMLInputElement;
146
+ this.localNumber.set(target.value);
147
+ this.emitValue();
148
+ }
149
+
150
+ private emitValue(): void {
151
+ const country = getCountryByCode(this.selectedCountryCode());
152
+ const dialCode = country?.dialCode ?? '';
153
+ const number = this.localNumber().trim();
154
+ const fullValue = number ? `${dialCode} ${number}` : dialCode;
155
+ this.onChange(fullValue);
156
+ }
157
+
158
+ private findCountryByDialCode(phone: string): Country | undefined {
159
+ let best: Country | undefined;
160
+ let bestLen = 0;
161
+ for (const c of COUNTRIES) {
162
+ if (phone.startsWith(c.dialCode) && c.dialCode.length > bestLen) {
163
+ best = c;
164
+ bestLen = c.dialCode.length;
165
+ }
166
+ }
167
+ return best;
168
+ }
169
+ }
@@ -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
  };
@@ -139,7 +139,7 @@ export class SheetContent implements OnDestroy {
139
139
  }
140
140
 
141
141
  private lockBodyScroll(): void {
142
- if (typeof document !== 'undefined') {
142
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
143
143
  const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
144
144
  this.previousBodyOverflow = document.body.style.overflow;
145
145
  this.previousBodyPaddingRight = document.body.style.paddingRight;
@@ -187,6 +187,7 @@ export class TabsList {
187
187
 
188
188
  if (handled) {
189
189
  event.preventDefault();
190
+ if (typeof document === 'undefined') return;
190
191
 
191
192
  // Skip disabled tabs — scan in the movement direction
192
193
  const direction =
@@ -212,7 +213,7 @@ export class TabsList {
212
213
  let index = startIndex;
213
214
  for (let i = 0; i < length; i++) {
214
215
  const tabId = this.tabs.getTabId(tabValues[index]);
215
- const el = document.getElementById(tabId);
216
+ const el = typeof document !== 'undefined' ? document.getElementById(tabId) : null;
216
217
  if (!el?.hasAttribute('data-disabled')) return index;
217
218
  index = ((index + direction) + length) % length;
218
219
  }
@@ -220,6 +221,7 @@ export class TabsList {
220
221
  }
221
222
 
222
223
  private updateIndicator(): void {
224
+ if (typeof document === 'undefined') return;
223
225
  const activeValue = this.tabs.value();
224
226
  if (!activeValue) return;
225
227
 
@@ -1,31 +1,91 @@
1
1
  import { cn } from '@/lib/utils';
2
- import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import {
3
+ AfterViewInit,
4
+ ChangeDetectionStrategy,
5
+ Component,
6
+ computed,
7
+ ElementRef,
8
+ forwardRef,
9
+ inject,
10
+ input,
11
+ signal,
12
+ } from '@angular/core';
13
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
3
14
 
4
15
  /**
5
16
  * Textarea component that applies shadcn textarea styles.
17
+ * Implements ControlValueAccessor for Angular Forms integration.
18
+ * Applied as an attribute on a native <textarea> element.
6
19
  *
7
20
  * @example
8
21
  * <!-- Basic textarea -->
9
- * <Textarea placeholder="Enter your message"></Textarea>
10
- *
11
- * <!-- With rows -->
12
- * <Textarea rows="5" placeholder="Description"></Textarea>
22
+ * <textarea Textarea placeholder="Type your message here."></textarea>
13
23
  *
14
24
  * <!-- Disabled -->
15
- * <Textarea disabled placeholder="Disabled"></Textarea>
25
+ * <textarea Textarea disabled placeholder="Disabled"></textarea>
26
+ *
27
+ * <!-- Auto-resize -->
28
+ * <textarea Textarea [autoResize]="true" placeholder="Grows as you type..."></textarea>
29
+ *
30
+ * <!-- With reactive forms -->
31
+ * <textarea Textarea formControlName="message" placeholder="Enter message"></textarea>
16
32
  */
17
33
  @Component({
18
- selector: 'Textarea',
19
- template: `<ng-content />`,
34
+ selector: 'textarea[Textarea]',
35
+ template: '',
20
36
  host: {
21
37
  '[class]': 'computedClass()',
22
- 'data-slot': 'textarea',
38
+ '[disabled]': 'isDisabled()',
39
+ '[id]': 'id()',
40
+ '[name]': 'name()',
41
+ '[placeholder]': 'placeholder()',
42
+ '[rows]': 'rows()',
43
+ '[attr.data-slot]': '"textarea"',
44
+ '[attr.data-auto-resize]': 'autoResize() || null',
45
+ '[attr.aria-invalid]': 'ariaInvalid() || null',
46
+ '[attr.aria-describedby]': 'ariaDescribedBy() || null',
47
+ '(input)': 'onInput($event)',
48
+ '(blur)': 'onTouched()',
23
49
  },
50
+ providers: [
51
+ {
52
+ provide: NG_VALUE_ACCESSOR,
53
+ useExisting: forwardRef(() => Textarea),
54
+ multi: true,
55
+ },
56
+ ],
24
57
  changeDetection: ChangeDetectionStrategy.OnPush,
25
58
  })
26
- export class Textarea {
59
+ export class Textarea implements ControlValueAccessor, AfterViewInit {
60
+ /** Placeholder text */
61
+ readonly placeholder = input<string>('');
62
+ /** Textarea id attribute */
63
+ readonly id = input<string>('');
64
+ /** Textarea name attribute */
65
+ readonly name = input<string>('');
66
+ /** Number of visible text rows */
67
+ readonly rows = input<number>(3);
68
+ /** Whether the textarea is disabled */
69
+ readonly disabled = input(false, {
70
+ transform: (value: boolean | string) => value === '' || value === true || value === 'true',
71
+ });
72
+ /** Aria-invalid state for error display */
73
+ readonly ariaInvalid = input<boolean | undefined>(undefined, { alias: 'aria-invalid' });
74
+ /** Aria-describedby for accessibility */
75
+ readonly ariaDescribedBy = input<string | undefined>(undefined, { alias: 'aria-describedby' });
27
76
  /** Additional CSS classes to apply */
28
77
  readonly class = input<string>('');
78
+ /** When true, the textarea height adjusts automatically as the user types */
79
+ readonly autoResize = input<boolean>(false);
80
+
81
+ private readonly _elementRef = inject(ElementRef<HTMLTextAreaElement>);
82
+
83
+ /** Internal disabled state (set by ControlValueAccessor) */
84
+ protected readonly isDisabled = signal<boolean>(false);
85
+
86
+ /** ControlValueAccessor callbacks */
87
+ private onChange: (value: string) => void = () => {};
88
+ protected onTouched: () => void = () => {};
29
89
 
30
90
  /** Computed class combining base styles and custom classes */
31
91
  protected readonly computedClass = computed(() =>
@@ -33,7 +93,47 @@ export class Textarea {
33
93
  'placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
34
94
  'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
35
95
  'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
96
+ 'resize-none data-[auto-resize=true]:resize-none',
36
97
  this.class(),
37
98
  ),
38
99
  );
100
+
101
+ ngAfterViewInit(): void {
102
+ // Ensure element is available after hydration
103
+ }
104
+
105
+ // ControlValueAccessor implementation
106
+ writeValue(value: string): void {
107
+ const el = this._elementRef.nativeElement;
108
+ el.value = value ?? '';
109
+ if (this.autoResize()) {
110
+ el.style.height = 'auto';
111
+ el.style.height = el.scrollHeight + 'px';
112
+ }
113
+ }
114
+ registerOnChange(fn: (value: string) => void): void {
115
+ this.onChange = fn;
116
+ }
117
+ registerOnTouched(fn: () => void): void {
118
+ this.onTouched = fn;
119
+ }
120
+ setDisabledState(isDisabled: boolean): void {
121
+ this.isDisabled.set(isDisabled);
122
+ }
123
+
124
+ /** Focus the textarea element */
125
+ focus(): void {
126
+ this._elementRef.nativeElement.focus();
127
+ }
128
+
129
+ /** Handle input events */
130
+ protected onInput(event: Event): void {
131
+ const target = event.target as HTMLTextAreaElement;
132
+ this.onChange(target.value);
133
+ if (this.autoResize()) {
134
+ const el = this._elementRef.nativeElement;
135
+ el.style.height = 'auto';
136
+ el.style.height = el.scrollHeight + 'px';
137
+ }
138
+ }
39
139
  }
@@ -178,7 +178,7 @@ export class ToastService {
178
178
  this._toasts.update((toasts) => [...toasts, newToast]);
179
179
 
180
180
  const duration = toast.duration ?? 4000;
181
- if (duration > 0) {
181
+ if (duration > 0 && typeof window !== 'undefined') {
182
182
  setTimeout(() => this.dismiss(id), duration);
183
183
  }
184
184