@ng-cn/core 1.0.18 → 1.0.21

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 (50) hide show
  1. package/package.json +17 -17
  2. package/schematics/tsconfig.json +1 -0
  3. package/src/app/lib/components/ui/alert-dialog/alert-dialog-content.component.ts +1 -1
  4. package/src/app/lib/components/ui/calendar/calendar.component.ts +65 -12
  5. package/src/app/lib/components/ui/chart/chart-context.ts +8 -6
  6. package/src/app/lib/components/ui/collapsible/collapsible.component.ts +0 -5
  7. package/src/app/lib/components/ui/context-menu/context-menu-content.component.ts +1 -0
  8. package/src/app/lib/components/ui/country-selector/country-data.ts +63 -0
  9. package/src/app/lib/components/ui/country-selector/country-selector.component.ts +199 -0
  10. package/src/app/lib/components/ui/country-selector/index.ts +2 -0
  11. package/src/app/lib/components/ui/date-picker/date-picker.component.ts +47 -5
  12. package/src/app/lib/components/ui/dialog/dialog-content.component.ts +1 -1
  13. package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-content.component.ts +23 -21
  14. package/src/app/lib/components/ui/field/field-content.component.ts +34 -0
  15. package/src/app/lib/components/ui/field/field-description.component.ts +35 -0
  16. package/src/app/lib/components/ui/field/field-error.component.ts +48 -0
  17. package/src/app/lib/components/ui/field/field-group.component.ts +34 -0
  18. package/src/app/lib/components/ui/field/field-label.component.ts +46 -0
  19. package/src/app/lib/components/ui/field/field-legend.component.ts +41 -0
  20. package/src/app/lib/components/ui/field/field-separator.component.ts +49 -0
  21. package/src/app/lib/components/ui/field/field-set.component.ts +37 -0
  22. package/src/app/lib/components/ui/field/field-title.component.ts +30 -0
  23. package/src/app/lib/components/ui/field/field.component.ts +66 -0
  24. package/src/app/lib/components/ui/field/index.ts +15 -0
  25. package/src/app/lib/components/ui/input/input.component.ts +3 -4
  26. package/src/app/lib/components/ui/item/index.ts +21 -0
  27. package/src/app/lib/components/ui/item/item-actions.component.ts +29 -0
  28. package/src/app/lib/components/ui/item/item-content.component.ts +31 -0
  29. package/src/app/lib/components/ui/item/item-description.component.ts +30 -0
  30. package/src/app/lib/components/ui/item/item-footer.component.ts +30 -0
  31. package/src/app/lib/components/ui/item/item-group.component.ts +32 -0
  32. package/src/app/lib/components/ui/item/item-header.component.ts +30 -0
  33. package/src/app/lib/components/ui/item/item-media.component.ts +63 -0
  34. package/src/app/lib/components/ui/item/item-separator.component.ts +33 -0
  35. package/src/app/lib/components/ui/item/item-title.component.ts +27 -0
  36. package/src/app/lib/components/ui/item/item.component.ts +77 -0
  37. package/src/app/lib/components/ui/phone-input/index.ts +1 -0
  38. package/src/app/lib/components/ui/phone-input/phone-input.component.ts +169 -0
  39. package/src/app/lib/components/ui/radio-group/radio-group.component.ts +0 -5
  40. package/src/app/lib/components/ui/resizable/resizable-handle.component.ts +2 -2
  41. package/src/app/lib/components/ui/select/select.component.ts +0 -8
  42. package/src/app/lib/components/ui/sheet/sheet-content.component.ts +1 -1
  43. package/src/app/lib/components/ui/sidebar/sidebar-provider.component.ts +0 -5
  44. package/src/app/lib/components/ui/slider/slider.component.ts +0 -4
  45. package/src/app/lib/components/ui/switch/switch.component.ts +0 -5
  46. package/src/app/lib/components/ui/tabs/tabs-list.component.ts +3 -1
  47. package/src/app/lib/components/ui/textarea/textarea.component.ts +110 -10
  48. package/src/app/lib/components/ui/toast/toast.service.ts +1 -1
  49. package/src/app/lib/components/ui/toggle/toggle.component.ts +0 -5
  50. package/src/app/lib/components/ui/toggle-group/toggle-group.component.ts +0 -5
@@ -3,11 +3,14 @@ import {
3
3
  ChangeDetectionStrategy,
4
4
  Component,
5
5
  computed,
6
+ forwardRef,
6
7
  input,
7
8
  model,
8
9
  output,
10
+ signal,
9
11
  viewChild,
10
12
  } from '@angular/core';
13
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
11
14
  import { CalendarIcon, LucideAngularModule } from 'lucide-angular';
12
15
  import { buttonVariants } from '../button';
13
16
  import { Calendar } from '../calendar';
@@ -16,6 +19,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '../popover';
16
19
  /**
17
20
  * DatePicker component - date selection with calendar popover.
18
21
  * Matches shadcn/ui React DatePicker exactly.
22
+ * Implements ControlValueAccessor for Angular Forms integration.
19
23
  */
20
24
  @Component({
21
25
  selector: 'DatePicker',
@@ -38,6 +42,9 @@ import { Popover, PopoverContent, PopoverTrigger } from '../popover';
38
42
  [selected]="date()"
39
43
  (onSelect)="onDateSelect($event)"
40
44
  [disabled]="disabledDates()"
45
+ [minDate]="minDate()"
46
+ [maxDate]="maxDate()"
47
+ [locale]="locale()"
41
48
  class="w-full"
42
49
  />
43
50
  </PopoverContent>
@@ -47,25 +54,43 @@ import { Popover, PopoverContent, PopoverTrigger } from '../popover';
47
54
  'attr.data-slot': '"date-picker"',
48
55
  class: 'contents',
49
56
  },
57
+ providers: [
58
+ {
59
+ provide: NG_VALUE_ACCESSOR,
60
+ useExisting: forwardRef(() => DatePicker),
61
+ multi: true,
62
+ },
63
+ ],
50
64
  changeDetection: ChangeDetectionStrategy.OnPush,
51
65
  })
52
- export class DatePicker {
66
+ export class DatePicker implements ControlValueAccessor {
53
67
  private readonly popover = viewChild(Popover);
54
68
 
55
69
  /** Date select event */
56
70
  readonly onSelect = output<Date | undefined>();
57
71
 
58
- /** Selected date */
72
+ /** Selected date (model input for two-way binding) */
59
73
  readonly date = model<Date | undefined>(undefined);
60
74
 
61
75
  /** Additional CSS classes */
62
76
  readonly class = input<string>('');
63
77
  /** Placeholder text */
64
78
  readonly placeholder = input<string>('Pick a date');
65
- /** Date format */
66
- readonly dateFormat = input<string>('PPP');
67
79
  /** Disabled dates function */
68
80
  readonly disabledDates = input<((date: Date) => boolean) | undefined>(undefined);
81
+ /** Minimum selectable date — passed to Calendar */
82
+ readonly minDate = input<Date | undefined>(undefined);
83
+ /** Maximum selectable date — passed to Calendar */
84
+ readonly maxDate = input<Date | undefined>(undefined);
85
+ /** Locale used for date formatting (e.g. 'en-US', 'fr-FR') */
86
+ readonly locale = input<string>('en-US');
87
+
88
+ /** Internal disabled state set by ControlValueAccessor */
89
+ protected readonly isDisabled = signal<boolean>(false);
90
+
91
+ /** ControlValueAccessor callbacks */
92
+ private onChange: (value: Date | undefined) => void = () => {};
93
+ private onTouched: () => void = () => {};
69
94
 
70
95
  protected readonly computedButtonClass = computed(() =>
71
96
  cn(
@@ -74,6 +99,7 @@ export class DatePicker {
74
99
  this.popover()?.isOpen() &&
75
100
  'border-primary/30 ring-primary/20 ring-2 dark:border-white/30 dark:ring-white/20',
76
101
  !this.date() && 'text-muted-foreground',
102
+ this.isDisabled() && 'pointer-events-none cursor-not-allowed opacity-50',
77
103
  this.class(),
78
104
  ),
79
105
  );
@@ -81,7 +107,7 @@ export class DatePicker {
81
107
  protected readonly CalendarIconRef = CalendarIcon;
82
108
 
83
109
  protected formatDate(date: Date): string {
84
- return date.toLocaleDateString('en-US', {
110
+ return date.toLocaleDateString(this.locale(), {
85
111
  year: 'numeric',
86
112
  month: 'long',
87
113
  day: 'numeric',
@@ -90,6 +116,22 @@ export class DatePicker {
90
116
  protected onDateSelect(date: Date | undefined): void {
91
117
  this.date.set(date);
92
118
  this.onSelect.emit(date);
119
+ this.onChange(date);
120
+ this.onTouched();
93
121
  this.popover()?.setOpen(false);
94
122
  }
123
+
124
+ // ControlValueAccessor implementation
125
+ writeValue(value: Date | undefined): void {
126
+ this.date.set(value ?? undefined);
127
+ }
128
+ registerOnChange(fn: (value: Date | undefined) => void): void {
129
+ this.onChange = fn;
130
+ }
131
+ registerOnTouched(fn: () => void): void {
132
+ this.onTouched = fn;
133
+ }
134
+ setDisabledState(isDisabled: boolean): void {
135
+ this.isDisabled.set(isDisabled);
136
+ }
95
137
  }
@@ -155,7 +155,7 @@ export class DialogContent {
155
155
  }
156
156
 
157
157
  private lockBodyScroll(): void {
158
- if (typeof document !== 'undefined') {
158
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
159
159
  const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
160
160
  this.previousBodyOverflow = document.body.style.overflow;
161
161
  this.previousBodyPaddingRight = document.body.style.paddingRight;
@@ -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);
@@ -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
+ }
@@ -0,0 +1,30 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
3
+
4
+ /**
5
+ * FieldTitle component - title text inside FieldContent.
6
+ *
7
+ * @example
8
+ * <FieldContent>
9
+ * <FieldTitle>Two-factor authentication</FieldTitle>
10
+ * <FieldDescription>Add an extra layer of security.</FieldDescription>
11
+ * </FieldContent>
12
+ */
13
+ @Component({
14
+ selector: 'FieldTitle',
15
+ template: `<ng-content />`,
16
+ host: {
17
+ 'attr.data-slot': '"field-title"',
18
+ '[class]': 'computedClass()',
19
+ },
20
+ changeDetection: ChangeDetectionStrategy.OnPush,
21
+ })
22
+ export class FieldTitle {
23
+ /** Additional CSS classes to apply */
24
+ readonly class = input<string>('');
25
+
26
+ /** Computed class combining base styles and custom classes */
27
+ protected readonly computedClass = computed(() =>
28
+ cn('flex w-fit items-center gap-2 text-sm leading-snug font-medium', this.class()),
29
+ );
30
+ }
@@ -0,0 +1,66 @@
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
+ * Field variants using class-variance-authority.
7
+ * Matches shadcn/ui React field exactly.
8
+ */
9
+ export const fieldVariants = cva('group/field flex w-full gap-3 data-[invalid=true]:text-destructive', {
10
+ variants: {
11
+ orientation: {
12
+ vertical: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto',
13
+ horizontal: 'flex-row items-center [&>[data-slot=field-label]]:flex-auto',
14
+ responsive:
15
+ 'flex-col [&>*]:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ orientation: 'vertical',
20
+ },
21
+ });
22
+
23
+ export type FieldVariants = VariantProps<typeof fieldVariants>;
24
+ export type FieldOrientation = 'vertical' | 'horizontal' | 'responsive';
25
+
26
+ /**
27
+ * Field component - wraps a single form field with its label,
28
+ * control, description and error message.
29
+ *
30
+ * @example
31
+ * <!-- Vertical (default) -->
32
+ * <Field>
33
+ * <FieldLabel htmlFor="email">Email</FieldLabel>
34
+ * <input Input id="email" type="email" />
35
+ * <FieldDescription>We never share your email.</FieldDescription>
36
+ * </Field>
37
+ *
38
+ * <!-- Horizontal -->
39
+ * <Field orientation="horizontal">
40
+ * <FieldLabel htmlFor="newsletter">Subscribe to newsletter</FieldLabel>
41
+ * <Switch id="newsletter" />
42
+ * </Field>
43
+ */
44
+ @Component({
45
+ selector: 'Field',
46
+ template: `<ng-content />`,
47
+ host: {
48
+ 'attr.data-slot': '"field"',
49
+ role: 'group',
50
+ '[attr.data-orientation]': 'orientation()',
51
+ '[class]': 'computedClass()',
52
+ },
53
+ changeDetection: ChangeDetectionStrategy.OnPush,
54
+ })
55
+ export class Field {
56
+ /** Layout orientation of the field */
57
+ readonly orientation = input<FieldOrientation>('vertical');
58
+
59
+ /** Additional CSS classes to apply */
60
+ readonly class = input<string>('');
61
+
62
+ /** Computed class combining base styles, variants and custom classes */
63
+ protected readonly computedClass = computed(() =>
64
+ cn(fieldVariants({ orientation: this.orientation() }), this.class()),
65
+ );
66
+ }
@@ -0,0 +1,15 @@
1
+ export { FieldContent } from './field-content.component';
2
+ export { FieldDescription } from './field-description.component';
3
+ export { FieldError } from './field-error.component';
4
+ export { FieldGroup } from './field-group.component';
5
+ export { FieldLabel } from './field-label.component';
6
+ export { FieldLegend, type FieldLegendVariant } from './field-legend.component';
7
+ export { FieldSeparator } from './field-separator.component';
8
+ export { FieldSet } from './field-set.component';
9
+ export { FieldTitle } from './field-title.component';
10
+ export {
11
+ Field,
12
+ fieldVariants,
13
+ type FieldOrientation,
14
+ type FieldVariants,
15
+ } from './field.component';
@@ -30,7 +30,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
30
30
  * <Input type="email" formControlName="email" placeholder="Enter email" />
31
31
  */
32
32
  @Component({
33
- selector: 'input[Input]',
33
+ selector: 'input[Input], Input',
34
34
  template: '',
35
35
  host: {
36
36
  '[type]': 'type()',
@@ -98,9 +98,8 @@ export class Input implements ControlValueAccessor, AfterViewInit {
98
98
  /** Classes applied to the input element */
99
99
  protected readonly inputClass = computed(() =>
100
100
  cn(
101
- 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input w-full min-w-0 rounded-xl border px-4 py-1 text-base text-left shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
102
- 'bg-zinc-50 dark:bg-zinc-800/50 border-zinc-300 dark:border-zinc-700/50 !text-zinc-900 dark:!text-zinc-50 placeholder:text-zinc-500',
103
- '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',
101
+ 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
102
+ 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
104
103
  'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
105
104
  this.class(),
106
105
  ),
@@ -0,0 +1,21 @@
1
+ export { ItemActions } from './item-actions.component';
2
+ export { ItemContent } from './item-content.component';
3
+ export { ItemDescription } from './item-description.component';
4
+ export { ItemFooter } from './item-footer.component';
5
+ export { ItemGroup } from './item-group.component';
6
+ export { ItemHeader } from './item-header.component';
7
+ export {
8
+ ItemMedia,
9
+ itemMediaVariants,
10
+ type ItemMediaVariant,
11
+ type ItemMediaVariants,
12
+ } from './item-media.component';
13
+ export { ItemSeparator } from './item-separator.component';
14
+ export { ItemTitle } from './item-title.component';
15
+ export {
16
+ Item,
17
+ itemVariants,
18
+ type ItemSize,
19
+ type ItemVariant,
20
+ type ItemVariants,
21
+ } from './item.component';