@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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ItemActions component - trailing actions (buttons, icons) for an Item.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <ItemActions>
|
|
9
|
+
* <Button variant="outline" size="sm">Open</Button>
|
|
10
|
+
* </ItemActions>
|
|
11
|
+
*/
|
|
12
|
+
@Component({
|
|
13
|
+
selector: 'ItemActions',
|
|
14
|
+
template: `<ng-content />`,
|
|
15
|
+
host: {
|
|
16
|
+
'attr.data-slot': '"item-actions"',
|
|
17
|
+
'[class]': 'computedClass()',
|
|
18
|
+
},
|
|
19
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
20
|
+
})
|
|
21
|
+
export class ItemActions {
|
|
22
|
+
/** Additional CSS classes to apply */
|
|
23
|
+
readonly class = input<string>('');
|
|
24
|
+
|
|
25
|
+
/** Computed class combining base styles and custom classes */
|
|
26
|
+
protected readonly computedClass = computed(() =>
|
|
27
|
+
cn('flex items-center gap-2', this.class()),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ItemContent component - main content area of an Item, holding
|
|
6
|
+
* the title and description.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <ItemContent>
|
|
10
|
+
* <ItemTitle>Title</ItemTitle>
|
|
11
|
+
* <ItemDescription>Description</ItemDescription>
|
|
12
|
+
* </ItemContent>
|
|
13
|
+
*/
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'ItemContent',
|
|
16
|
+
template: `<ng-content />`,
|
|
17
|
+
host: {
|
|
18
|
+
'attr.data-slot': '"item-content"',
|
|
19
|
+
'[class]': 'computedClass()',
|
|
20
|
+
},
|
|
21
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
22
|
+
})
|
|
23
|
+
export class ItemContent {
|
|
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('flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none', this.class()),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ItemDescription component - secondary description text for an Item.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <ItemDescription>A short summary of the item.</ItemDescription>
|
|
9
|
+
*/
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'ItemDescription',
|
|
12
|
+
template: `<ng-content />`,
|
|
13
|
+
host: {
|
|
14
|
+
'attr.data-slot': '"item-description"',
|
|
15
|
+
'[class]': 'computedClass()',
|
|
16
|
+
},
|
|
17
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18
|
+
})
|
|
19
|
+
export class ItemDescription {
|
|
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(
|
|
26
|
+
'text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance',
|
|
27
|
+
this.class(),
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ItemFooter component - full-width footer row inside an Item.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <Item>
|
|
9
|
+
* <ItemContent>...</ItemContent>
|
|
10
|
+
* <ItemFooter>Footer content</ItemFooter>
|
|
11
|
+
* </Item>
|
|
12
|
+
*/
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'ItemFooter',
|
|
15
|
+
template: `<ng-content />`,
|
|
16
|
+
host: {
|
|
17
|
+
'attr.data-slot': '"item-footer"',
|
|
18
|
+
'[class]': 'computedClass()',
|
|
19
|
+
},
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
})
|
|
22
|
+
export class ItemFooter {
|
|
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 basis-full items-center justify-between gap-2', this.class()),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ItemGroup component - container that stacks multiple Item components.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <ItemGroup>
|
|
9
|
+
* <Item>...</Item>
|
|
10
|
+
* <ItemSeparator />
|
|
11
|
+
* <Item>...</Item>
|
|
12
|
+
* </ItemGroup>
|
|
13
|
+
*/
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'ItemGroup',
|
|
16
|
+
template: `<ng-content />`,
|
|
17
|
+
host: {
|
|
18
|
+
'attr.data-slot': '"item-group"',
|
|
19
|
+
role: 'list',
|
|
20
|
+
'[class]': 'computedClass()',
|
|
21
|
+
},
|
|
22
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
23
|
+
})
|
|
24
|
+
export class ItemGroup {
|
|
25
|
+
/** Additional CSS classes to apply */
|
|
26
|
+
readonly class = input<string>('');
|
|
27
|
+
|
|
28
|
+
/** Computed class combining base styles and custom classes */
|
|
29
|
+
protected readonly computedClass = computed(() =>
|
|
30
|
+
cn('group/item-group flex flex-col', this.class()),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ItemHeader component - full-width header row inside an Item.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <Item>
|
|
9
|
+
* <ItemHeader>Header content</ItemHeader>
|
|
10
|
+
* <ItemContent>...</ItemContent>
|
|
11
|
+
* </Item>
|
|
12
|
+
*/
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'ItemHeader',
|
|
15
|
+
template: `<ng-content />`,
|
|
16
|
+
host: {
|
|
17
|
+
'attr.data-slot': '"item-header"',
|
|
18
|
+
'[class]': 'computedClass()',
|
|
19
|
+
},
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
})
|
|
22
|
+
export class ItemHeader {
|
|
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 basis-full items-center justify-between gap-2', this.class()),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
inject,
|
|
9
9
|
input,
|
|
10
10
|
model,
|
|
11
|
-
output,
|
|
12
11
|
signal,
|
|
13
12
|
} from '@angular/core';
|
|
14
13
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
@@ -121,9 +120,6 @@ export type RadioGroupProps = {
|
|
|
121
120
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
122
121
|
})
|
|
123
122
|
export class RadioGroup implements ControlValueAccessor {
|
|
124
|
-
/** Emitted when value changes */
|
|
125
|
-
readonly valueChange = output<string>();
|
|
126
|
-
|
|
127
123
|
/** The current selected value */
|
|
128
124
|
readonly value = model<string>('');
|
|
129
125
|
|
|
@@ -169,7 +165,6 @@ export class RadioGroup implements ControlValueAccessor {
|
|
|
169
165
|
this.context.value.set(value);
|
|
170
166
|
this.onChange(value);
|
|
171
167
|
this.onTouched();
|
|
172
|
-
this.valueChange.emit(value);
|
|
173
168
|
}
|
|
174
169
|
},
|
|
175
170
|
focusNext: (currentValue: string) => this.focusItem(currentValue, 1),
|
|
@@ -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
|
};
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
inject,
|
|
11
11
|
input,
|
|
12
12
|
model,
|
|
13
|
-
output,
|
|
14
13
|
signal,
|
|
15
14
|
} from '@angular/core';
|
|
16
15
|
import { SELECT_CONTEXT, type SelectContext } from './select-context';
|
|
@@ -168,11 +167,6 @@ export class Select {
|
|
|
168
167
|
});
|
|
169
168
|
}
|
|
170
169
|
|
|
171
|
-
/** Event handler called when the value changes */
|
|
172
|
-
readonly valueChange = output<string>();
|
|
173
|
-
/** Event handler called when the open state changes */
|
|
174
|
-
readonly openChange = output<boolean>();
|
|
175
|
-
|
|
176
170
|
/** The controlled value of the select */
|
|
177
171
|
readonly value = model<string>('');
|
|
178
172
|
/** The controlled open state of the select */
|
|
@@ -211,7 +205,6 @@ export class Select {
|
|
|
211
205
|
setOpen: (open: boolean) => {
|
|
212
206
|
this._isOpen.set(open);
|
|
213
207
|
this.open.set(open);
|
|
214
|
-
this.openChange.emit(open);
|
|
215
208
|
},
|
|
216
209
|
disabled: this._isDisabled,
|
|
217
210
|
placeholder: signal(''),
|
|
@@ -225,7 +218,6 @@ export class Select {
|
|
|
225
218
|
setValue: (value: string, label?: string) => {
|
|
226
219
|
this._value.set(value);
|
|
227
220
|
this.value.set(value);
|
|
228
|
-
this.valueChange.emit(value);
|
|
229
221
|
if (label) {
|
|
230
222
|
this.context.selectedLabel.set(label);
|
|
231
223
|
}
|
|
@@ -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;
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
HostListener,
|
|
11
11
|
input,
|
|
12
12
|
model,
|
|
13
|
-
output,
|
|
14
13
|
signal,
|
|
15
14
|
} from '@angular/core';
|
|
16
15
|
import {
|
|
@@ -71,9 +70,6 @@ export class SidebarProvider {
|
|
|
71
70
|
});
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
/** Open state change event */
|
|
75
|
-
readonly openChange = output<boolean>();
|
|
76
|
-
|
|
77
73
|
/** Controlled open state */
|
|
78
74
|
readonly open = model<boolean>(true);
|
|
79
75
|
|
|
@@ -101,7 +97,6 @@ export class SidebarProvider {
|
|
|
101
97
|
this.context.open.set(value);
|
|
102
98
|
this.context.state.set(value ? 'expanded' : 'collapsed');
|
|
103
99
|
this.open.set(value);
|
|
104
|
-
this.openChange.emit(value);
|
|
105
100
|
},
|
|
106
101
|
openMobile: signal(false),
|
|
107
102
|
setOpenMobile: (value: boolean) => {
|