@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.
- package/package.json +17 -17
- package/schematics/tsconfig.json +1 -0
- package/src/app/lib/components/ui/alert-dialog/alert-dialog-content.component.ts +1 -1
- package/src/app/lib/components/ui/calendar/calendar.component.ts +65 -12
- package/src/app/lib/components/ui/chart/chart-context.ts +8 -6
- package/src/app/lib/components/ui/collapsible/collapsible.component.ts +0 -5
- package/src/app/lib/components/ui/context-menu/context-menu-content.component.ts +1 -0
- package/src/app/lib/components/ui/country-selector/country-data.ts +63 -0
- package/src/app/lib/components/ui/country-selector/country-selector.component.ts +199 -0
- package/src/app/lib/components/ui/country-selector/index.ts +2 -0
- package/src/app/lib/components/ui/date-picker/date-picker.component.ts +47 -5
- package/src/app/lib/components/ui/dialog/dialog-content.component.ts +1 -1
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-content.component.ts +23 -21
- package/src/app/lib/components/ui/field/field-content.component.ts +34 -0
- package/src/app/lib/components/ui/field/field-description.component.ts +35 -0
- package/src/app/lib/components/ui/field/field-error.component.ts +48 -0
- package/src/app/lib/components/ui/field/field-group.component.ts +34 -0
- package/src/app/lib/components/ui/field/field-label.component.ts +46 -0
- package/src/app/lib/components/ui/field/field-legend.component.ts +41 -0
- package/src/app/lib/components/ui/field/field-separator.component.ts +49 -0
- package/src/app/lib/components/ui/field/field-set.component.ts +37 -0
- package/src/app/lib/components/ui/field/field-title.component.ts +30 -0
- package/src/app/lib/components/ui/field/field.component.ts +66 -0
- package/src/app/lib/components/ui/field/index.ts +15 -0
- package/src/app/lib/components/ui/input/input.component.ts +3 -4
- package/src/app/lib/components/ui/item/index.ts +21 -0
- package/src/app/lib/components/ui/item/item-actions.component.ts +29 -0
- package/src/app/lib/components/ui/item/item-content.component.ts +31 -0
- package/src/app/lib/components/ui/item/item-description.component.ts +30 -0
- package/src/app/lib/components/ui/item/item-footer.component.ts +30 -0
- package/src/app/lib/components/ui/item/item-group.component.ts +32 -0
- package/src/app/lib/components/ui/item/item-header.component.ts +30 -0
- package/src/app/lib/components/ui/item/item-media.component.ts +63 -0
- package/src/app/lib/components/ui/item/item-separator.component.ts +33 -0
- package/src/app/lib/components/ui/item/item-title.component.ts +27 -0
- package/src/app/lib/components/ui/item/item.component.ts +77 -0
- package/src/app/lib/components/ui/phone-input/index.ts +1 -0
- package/src/app/lib/components/ui/phone-input/phone-input.component.ts +169 -0
- package/src/app/lib/components/ui/radio-group/radio-group.component.ts +0 -5
- package/src/app/lib/components/ui/resizable/resizable-handle.component.ts +2 -2
- package/src/app/lib/components/ui/select/select.component.ts +0 -8
- package/src/app/lib/components/ui/sheet/sheet-content.component.ts +1 -1
- package/src/app/lib/components/ui/sidebar/sidebar-provider.component.ts +0 -5
- package/src/app/lib/components/ui/slider/slider.component.ts +0 -4
- package/src/app/lib/components/ui/switch/switch.component.ts +0 -5
- package/src/app/lib/components/ui/tabs/tabs-list.component.ts +3 -1
- package/src/app/lib/components/ui/textarea/textarea.component.ts +110 -10
- package/src/app/lib/components/ui/toast/toast.service.ts +1 -1
- package/src/app/lib/components/ui/toggle/toggle.component.ts +0 -5
- 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(
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
if (typeof window !== 'undefined') {
|
|
59
|
+
const trigger = this.context.triggerElement();
|
|
60
|
+
if (trigger) {
|
|
61
|
+
const rect = trigger.getBoundingClientRect();
|
|
62
|
+
const side = this.side();
|
|
63
|
+
const offset = this.sideOffset();
|
|
64
|
+
let top = rect.top;
|
|
65
|
+
let left = rect.left;
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
if (side === 'bottom') {
|
|
68
|
+
top = rect.bottom + offset;
|
|
69
|
+
left = rect.left;
|
|
70
|
+
} else if (side === 'top') {
|
|
71
|
+
top = rect.top - offset;
|
|
72
|
+
left = rect.left;
|
|
73
|
+
} else if (side === 'right') {
|
|
74
|
+
top = rect.top;
|
|
75
|
+
left = rect.right + offset;
|
|
76
|
+
} else if (side === 'left') {
|
|
77
|
+
top = rect.top;
|
|
78
|
+
left = rect.left - offset;
|
|
79
|
+
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
this.fixedPos.set({ top, left });
|
|
82
|
+
}
|
|
81
83
|
}
|
|
82
84
|
} else {
|
|
83
85
|
this.fixedPos.set(null);
|
|
@@ -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-
|
|
102
|
-
'
|
|
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';
|