pdm-ui-kit 0.1.0
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/FIGMA_COMPONENT_AUDIT.md +154 -0
- package/README.md +72 -0
- package/ng-package.json +7 -0
- package/package.json +29 -0
- package/src/lib/components/accordion/accordion.component.html +34 -0
- package/src/lib/components/accordion/accordion.component.ts +38 -0
- package/src/lib/components/alert/alert.component.html +52 -0
- package/src/lib/components/alert/alert.component.ts +25 -0
- package/src/lib/components/alert-dialog/alert-dialog.component.html +41 -0
- package/src/lib/components/alert-dialog/alert-dialog.component.ts +45 -0
- package/src/lib/components/aspect-ratio/aspect-ratio.component.html +11 -0
- package/src/lib/components/aspect-ratio/aspect-ratio.component.ts +18 -0
- package/src/lib/components/avatar/avatar.component.html +21 -0
- package/src/lib/components/avatar/avatar.component.ts +32 -0
- package/src/lib/components/badge/badge.component.html +28 -0
- package/src/lib/components/badge/badge.component.ts +23 -0
- package/src/lib/components/breadcrumb/breadcrumb.component.html +39 -0
- package/src/lib/components/breadcrumb/breadcrumb.component.ts +26 -0
- package/src/lib/components/button/button.component.html +15 -0
- package/src/lib/components/button/button.component.ts +84 -0
- package/src/lib/components/button-group/button-group.component.html +39 -0
- package/src/lib/components/button-group/button-group.component.ts +15 -0
- package/src/lib/components/calendar/calendar.component.html +73 -0
- package/src/lib/components/calendar/calendar.component.ts +78 -0
- package/src/lib/components/card/card.component.html +77 -0
- package/src/lib/components/card/card.component.ts +39 -0
- package/src/lib/components/carousel/carousel.component.html +86 -0
- package/src/lib/components/carousel/carousel.component.ts +100 -0
- package/src/lib/components/chart/chart.component.html +143 -0
- package/src/lib/components/chart/chart.component.ts +147 -0
- package/src/lib/components/checkbox/checkbox.component.html +38 -0
- package/src/lib/components/checkbox/checkbox.component.ts +32 -0
- package/src/lib/components/collapsible/collapsible.component.html +26 -0
- package/src/lib/components/collapsible/collapsible.component.ts +29 -0
- package/src/lib/components/combobox/combobox.component.html +42 -0
- package/src/lib/components/combobox/combobox.component.ts +32 -0
- package/src/lib/components/command/command.component.html +55 -0
- package/src/lib/components/command/command.component.ts +67 -0
- package/src/lib/components/context-menu/context-menu.component.html +47 -0
- package/src/lib/components/context-menu/context-menu.component.ts +67 -0
- package/src/lib/components/data-table/data-table.component.html +63 -0
- package/src/lib/components/data-table/data-table.component.ts +78 -0
- package/src/lib/components/date-picker/date-picker.component.html +38 -0
- package/src/lib/components/date-picker/date-picker.component.ts +34 -0
- package/src/lib/components/dialog/dialog.component.html +78 -0
- package/src/lib/components/dialog/dialog.component.ts +55 -0
- package/src/lib/components/drawer/drawer.component.html +56 -0
- package/src/lib/components/drawer/drawer.component.ts +43 -0
- package/src/lib/components/dropdown-menu/dropdown-menu.component.html +56 -0
- package/src/lib/components/dropdown-menu/dropdown-menu.component.ts +126 -0
- package/src/lib/components/empty/empty.component.html +29 -0
- package/src/lib/components/empty/empty.component.ts +35 -0
- package/src/lib/components/field/field.component.html +22 -0
- package/src/lib/components/field/field.component.ts +28 -0
- package/src/lib/components/hover-card/hover-card.component.html +24 -0
- package/src/lib/components/hover-card/hover-card.component.ts +36 -0
- package/src/lib/components/icon/icon.component.html +286 -0
- package/src/lib/components/icon/icon.component.ts +133 -0
- package/src/lib/components/input/input.component.html +22 -0
- package/src/lib/components/input/input.component.ts +33 -0
- package/src/lib/components/input-group/input-group.component.html +31 -0
- package/src/lib/components/input-group/input-group.component.ts +26 -0
- package/src/lib/components/input-otp/input-otp.component.html +25 -0
- package/src/lib/components/input-otp/input-otp.component.ts +146 -0
- package/src/lib/components/input-password/input-password.component.html +64 -0
- package/src/lib/components/input-password/input-password.component.ts +46 -0
- package/src/lib/components/item/item.component.html +10 -0
- package/src/lib/components/item/item.component.ts +12 -0
- package/src/lib/components/kbd/kbd.component.html +3 -0
- package/src/lib/components/kbd/kbd.component.ts +10 -0
- package/src/lib/components/label/label.component.html +7 -0
- package/src/lib/components/label/label.component.ts +12 -0
- package/src/lib/components/menubar/menubar.component.html +16 -0
- package/src/lib/components/menubar/menubar.component.ts +29 -0
- package/src/lib/components/native-select/native-select.component.html +17 -0
- package/src/lib/components/native-select/native-select.component.ts +28 -0
- package/src/lib/components/navigation-menu/navigation-menu.component.html +15 -0
- package/src/lib/components/navigation-menu/navigation-menu.component.ts +17 -0
- package/src/lib/components/pagination/pagination.component.html +30 -0
- package/src/lib/components/pagination/pagination.component.ts +37 -0
- package/src/lib/components/popover/popover.component.html +6 -0
- package/src/lib/components/popover/popover.component.ts +40 -0
- package/src/lib/components/progress/progress.component.html +9 -0
- package/src/lib/components/progress/progress.component.ts +20 -0
- package/src/lib/components/radio-group/radio-group.component.html +25 -0
- package/src/lib/components/radio-group/radio-group.component.ts +30 -0
- package/src/lib/components/scroll-area/scroll-area.component.html +5 -0
- package/src/lib/components/scroll-area/scroll-area.component.ts +11 -0
- package/src/lib/components/select/select.component.html +14 -0
- package/src/lib/components/select/select.component.ts +27 -0
- package/src/lib/components/separator/separator.component.html +5 -0
- package/src/lib/components/separator/separator.component.ts +16 -0
- package/src/lib/components/sheet/sheet.component.html +10 -0
- package/src/lib/components/sheet/sheet.component.ts +28 -0
- package/src/lib/components/sidebar/sidebar.component.html +3 -0
- package/src/lib/components/sidebar/sidebar.component.ts +11 -0
- package/src/lib/components/skeleton/skeleton.component.html +1 -0
- package/src/lib/components/skeleton/skeleton.component.ts +10 -0
- package/src/lib/components/slider/slider.component.html +15 -0
- package/src/lib/components/slider/slider.component.ts +31 -0
- package/src/lib/components/sonner/sonner.component.html +10 -0
- package/src/lib/components/sonner/sonner.component.ts +25 -0
- package/src/lib/components/spinner/spinner.component.html +6 -0
- package/src/lib/components/spinner/spinner.component.ts +11 -0
- package/src/lib/components/switch/switch.component.html +14 -0
- package/src/lib/components/switch/switch.component.ts +20 -0
- package/src/lib/components/table/table.component.html +5 -0
- package/src/lib/components/table/table.component.ts +10 -0
- package/src/lib/components/tabs/tabs.component.html +21 -0
- package/src/lib/components/tabs/tabs.component.ts +26 -0
- package/src/lib/components/textarea/textarea.component.html +21 -0
- package/src/lib/components/textarea/textarea.component.ts +28 -0
- package/src/lib/components/toggle/toggle.component.html +16 -0
- package/src/lib/components/toggle/toggle.component.ts +29 -0
- package/src/lib/components/toggle-group/toggle-group.component.html +17 -0
- package/src/lib/components/toggle-group/toggle-group.component.ts +26 -0
- package/src/lib/components/tooltip/tooltip.component.html +6 -0
- package/src/lib/components/tooltip/tooltip.component.ts +20 -0
- package/src/lib/pdm-ui-kit.module.ts +126 -0
- package/src/public-api.ts +58 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +9 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'pdm-input-otp',
|
|
5
|
+
templateUrl: './input-otp.component.html',
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
7
|
+
})
|
|
8
|
+
export class PdmInputOtpComponent {
|
|
9
|
+
@Input() length = 6;
|
|
10
|
+
@Input() groupSize = 3;
|
|
11
|
+
@Input() disabled = false;
|
|
12
|
+
@Input() invalid = false;
|
|
13
|
+
@Input() className = '';
|
|
14
|
+
|
|
15
|
+
@Output() valueChange = new EventEmitter<string>();
|
|
16
|
+
@Output() completed = new EventEmitter<string>();
|
|
17
|
+
|
|
18
|
+
@ViewChildren('otpInput') private readonly inputs?: QueryList<ElementRef<HTMLInputElement>>;
|
|
19
|
+
|
|
20
|
+
values: string[] = Array.from({ length: this.length }, () => '');
|
|
21
|
+
|
|
22
|
+
ngOnChanges(): void {
|
|
23
|
+
if (this.values.length !== this.length) {
|
|
24
|
+
this.values = Array.from({ length: this.length }, (_, index) => this.values[index] ?? '');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
trackByIndex(index: number): number {
|
|
29
|
+
return index;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onInput(index: number, event: Event): void {
|
|
33
|
+
const input = event.target as HTMLInputElement;
|
|
34
|
+
const char = (input.value || '').replace(/\D+/g, '').slice(-1);
|
|
35
|
+
|
|
36
|
+
this.values[index] = char;
|
|
37
|
+
input.value = char;
|
|
38
|
+
this.emit();
|
|
39
|
+
|
|
40
|
+
if (char) {
|
|
41
|
+
this.focusInput(index + 1, true);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onKeyDown(index: number, event: KeyboardEvent): void {
|
|
46
|
+
if (event.key === 'Backspace' && !this.values[index]) {
|
|
47
|
+
this.focusInput(index - 1);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (event.key === 'ArrowLeft') {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
this.focusInput(index - 1);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (event.key === 'ArrowRight') {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
this.focusInput(index + 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onPaste(event: ClipboardEvent): void {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
|
|
66
|
+
const pastedText = (event.clipboardData?.getData('text') ?? '').replace(/\D+/g, '').slice(0, this.length);
|
|
67
|
+
|
|
68
|
+
if (!pastedText) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.values = Array.from({ length: this.length }, (_, index) => pastedText[index] ?? '');
|
|
73
|
+
this.emit();
|
|
74
|
+
|
|
75
|
+
const nextIndex = Math.min(pastedText.length, this.length - 1);
|
|
76
|
+
this.focusInput(nextIndex);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getInputClasses(index: number): string {
|
|
80
|
+
const classes = [
|
|
81
|
+
'h-9 w-9 appearance-none border bg-background text-center text-sm font-normal text-foreground outline-none transition focus:outline-none focus-visible:outline-none',
|
|
82
|
+
'shadow-[0_1px_2px_0_rgba(0,0,0,0.1)]',
|
|
83
|
+
'focus:border-input focus:ring-1 focus:ring-primary/30',
|
|
84
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
if (this.invalid) {
|
|
88
|
+
classes.push('border-destructive focus:border-destructive focus:ring-destructive');
|
|
89
|
+
} else {
|
|
90
|
+
classes.push('border-input');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.isGroupStart(index)) {
|
|
94
|
+
classes.push('rounded-l-md border-l');
|
|
95
|
+
} else {
|
|
96
|
+
classes.push('border-l-0');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (this.isGroupEnd(index)) {
|
|
100
|
+
classes.push('rounded-r-md');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return classes.join(' ');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
shouldShowSeparator(index: number): boolean {
|
|
107
|
+
return this.groupSize > 0 && (index + 1) % this.groupSize === 0 && index < this.length - 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private isGroupStart(index: number): boolean {
|
|
111
|
+
return this.groupSize <= 0 || index % this.groupSize === 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private isGroupEnd(index: number): boolean {
|
|
115
|
+
if (this.groupSize <= 0) {
|
|
116
|
+
return index === this.length - 1;
|
|
117
|
+
}
|
|
118
|
+
return (index + 1) % this.groupSize === 0 || index === this.length - 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private emit(): void {
|
|
122
|
+
const value = this.values.join('');
|
|
123
|
+
this.valueChange.emit(value);
|
|
124
|
+
|
|
125
|
+
if (value.length === this.length && !this.values.includes('')) {
|
|
126
|
+
this.completed.emit(value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private focusInput(index: number, deferred = false): void {
|
|
131
|
+
if (deferred) {
|
|
132
|
+
requestAnimationFrame(() => this.focusInput(index, false));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!this.inputs || index < 0 || index >= this.length) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const input = this.inputs.get(index)?.nativeElement;
|
|
141
|
+
if (input) {
|
|
142
|
+
input.focus();
|
|
143
|
+
input.select();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<div [ngClass]="['grid w-full gap-2', className]">
|
|
2
|
+
<label *ngIf="label" [attr.for]="id" class="text-sm font-medium leading-5 text-foreground">{{ label }}</label>
|
|
3
|
+
<div class="relative">
|
|
4
|
+
<input
|
|
5
|
+
[id]="id"
|
|
6
|
+
[type]="inputType"
|
|
7
|
+
[value]="value"
|
|
8
|
+
[placeholder]="placeholder"
|
|
9
|
+
[disabled]="disabled"
|
|
10
|
+
[readonly]="readonly"
|
|
11
|
+
[required]="required"
|
|
12
|
+
[attr.aria-invalid]="invalid"
|
|
13
|
+
[ngClass]="[
|
|
14
|
+
'flex h-9 w-full rounded-[8px] border bg-background px-3 py-2 pr-10 text-sm leading-5 text-foreground shadow-[0_1px_2px_rgba(0,0,0,0.1)] outline-none ring-offset-background placeholder:text-muted-foreground focus:outline-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50',
|
|
15
|
+
invalid ? 'border-destructive' : 'border-input',
|
|
16
|
+
inputClassName
|
|
17
|
+
]"
|
|
18
|
+
(input)="onInput($event)"
|
|
19
|
+
(blur)="onBlur($event)"
|
|
20
|
+
/>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
class="absolute right-2 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded border-0 bg-transparent p-0 text-muted-foreground outline-none transition-colors hover:text-foreground focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
|
24
|
+
[disabled]="disabled"
|
|
25
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'"
|
|
26
|
+
(click)="toggleVisibility()"
|
|
27
|
+
>
|
|
28
|
+
<svg
|
|
29
|
+
*ngIf="!showPassword"
|
|
30
|
+
aria-hidden="true"
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
stroke-width="2"
|
|
36
|
+
stroke-linecap="round"
|
|
37
|
+
stroke-linejoin="round"
|
|
38
|
+
class="h-4 w-4"
|
|
39
|
+
>
|
|
40
|
+
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"></path>
|
|
41
|
+
<circle cx="12" cy="12" r="3"></circle>
|
|
42
|
+
</svg>
|
|
43
|
+
<svg
|
|
44
|
+
*ngIf="showPassword"
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
47
|
+
viewBox="0 0 24 24"
|
|
48
|
+
fill="none"
|
|
49
|
+
stroke="currentColor"
|
|
50
|
+
stroke-width="2"
|
|
51
|
+
stroke-linecap="round"
|
|
52
|
+
stroke-linejoin="round"
|
|
53
|
+
class="h-4 w-4"
|
|
54
|
+
>
|
|
55
|
+
<path d="M3 3l18 18"></path>
|
|
56
|
+
<path d="M10.58 10.58a2 2 0 1 0 2.83 2.83"></path>
|
|
57
|
+
<path d="M9.88 5.09A10.94 10.94 0 0 1 12 4.91c5.05 0 9.27 3.11 10.6 7.09a1 1 0 0 1 0 .64 11.9 11.9 0 0 1-1.84 3.2"></path>
|
|
58
|
+
<path d="M6.61 6.61A11.81 11.81 0 0 0 1.4 12a1 1 0 0 0 0 .64 11.83 11.83 0 0 0 8.79 7.54"></path>
|
|
59
|
+
</svg>
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
<p *ngIf="!invalid && helperText" class="text-sm leading-5 text-muted-foreground">{{ helperText }}</p>
|
|
63
|
+
<p *ngIf="invalid && errorText" class="text-sm leading-5 text-destructive">{{ errorText }}</p>
|
|
64
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'pdm-input-password',
|
|
5
|
+
templateUrl: './input-password.component.html',
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
7
|
+
})
|
|
8
|
+
export class PdmInputPasswordComponent {
|
|
9
|
+
@Input() id = '';
|
|
10
|
+
@Input() value = '';
|
|
11
|
+
@Input() placeholder = '';
|
|
12
|
+
@Input() disabled = false;
|
|
13
|
+
@Input() readonly = false;
|
|
14
|
+
@Input() required = false;
|
|
15
|
+
@Input() invalid = false;
|
|
16
|
+
@Input() className = '';
|
|
17
|
+
@Input() inputClassName = '';
|
|
18
|
+
@Input() label = '';
|
|
19
|
+
@Input() helperText = '';
|
|
20
|
+
@Input() errorText = '';
|
|
21
|
+
|
|
22
|
+
@Output() valueChange = new EventEmitter<string>();
|
|
23
|
+
@Output() blurred = new EventEmitter<FocusEvent>();
|
|
24
|
+
|
|
25
|
+
showPassword = false;
|
|
26
|
+
|
|
27
|
+
get inputType(): 'text' | 'password' {
|
|
28
|
+
return this.showPassword ? 'text' : 'password';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onInput(event: Event): void {
|
|
32
|
+
this.valueChange.emit((event.target as HTMLInputElement).value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onBlur(event: FocusEvent): void {
|
|
36
|
+
this.blurred.emit(event);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
toggleVisibility(): void {
|
|
40
|
+
if (this.disabled) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.showPassword = !this.showPassword;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div
|
|
2
|
+
[ngClass]="[
|
|
3
|
+
'relative flex w-full select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',
|
|
4
|
+
selected ? 'bg-[hsl(var(--accent))] text-[hsl(var(--accent-foreground))]' : 'text-[hsl(var(--foreground))]',
|
|
5
|
+
disabled ? 'pointer-events-none opacity-50' : 'hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--accent-foreground))]',
|
|
6
|
+
className
|
|
7
|
+
]"
|
|
8
|
+
>
|
|
9
|
+
<ng-content></ng-content>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'pdm-item',
|
|
5
|
+
templateUrl: './item.component.html',
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
7
|
+
})
|
|
8
|
+
export class PdmItemComponent {
|
|
9
|
+
@Input() className = '';
|
|
10
|
+
@Input() disabled = false;
|
|
11
|
+
@Input() selected = false;
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'pdm-kbd',
|
|
5
|
+
templateUrl: './kbd.component.html',
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
7
|
+
})
|
|
8
|
+
export class PdmKbdComponent {
|
|
9
|
+
@Input() className = '';
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<label
|
|
2
|
+
[attr.for]="forId"
|
|
3
|
+
[ngClass]="['text-sm font-medium leading-5 text-[hsl(var(--foreground))] peer-disabled:cursor-not-allowed peer-disabled:opacity-70', className]"
|
|
4
|
+
>
|
|
5
|
+
<ng-content></ng-content>
|
|
6
|
+
<span *ngIf="required" class="ml-1 text-[hsl(var(--destructive))]">*</span>
|
|
7
|
+
</label>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'pdm-label',
|
|
5
|
+
templateUrl: './label.component.html',
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
7
|
+
})
|
|
8
|
+
export class PdmLabelComponent {
|
|
9
|
+
@Input() forId = '';
|
|
10
|
+
@Input() required = false;
|
|
11
|
+
@Input() className = '';
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<nav role="menubar" [ngClass]="['inline-flex h-9 items-center gap-0.5 rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--background))] p-1 shadow-[0_1px_2px_rgba(0,0,0,0.1)]', className]">
|
|
2
|
+
<div *ngFor="let menu of menus; let i = index" class="relative">
|
|
3
|
+
<button type="button" class="inline-flex h-7 items-center rounded-sm px-3 text-sm leading-5 text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent))]" (click)="toggle(i)">{{ menu.label }}</button>
|
|
4
|
+
<div *ngIf="openIndex === i" class="absolute left-0 top-full z-50 mt-1 min-w-[12rem] rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--popover))] p-1 text-[hsl(var(--popover-foreground))] shadow-[0_4px_6px_rgba(0,0,0,0.09)]">
|
|
5
|
+
<button
|
|
6
|
+
*ngFor="let item of menu.items"
|
|
7
|
+
type="button"
|
|
8
|
+
[disabled]="item.disabled"
|
|
9
|
+
class="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm leading-5 outline-none hover:bg-[hsl(var(--accent))] disabled:pointer-events-none disabled:opacity-50"
|
|
10
|
+
(click)="select(item.value)"
|
|
11
|
+
>
|
|
12
|
+
{{ item.label }}
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</nav>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
|
2
|
+
import { PdmMenuItem } from '../dropdown-menu/dropdown-menu.component';
|
|
3
|
+
|
|
4
|
+
export interface PdmMenubarItem {
|
|
5
|
+
label: string;
|
|
6
|
+
items: PdmMenuItem[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
selector: 'pdm-menubar',
|
|
11
|
+
templateUrl: './menubar.component.html',
|
|
12
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
13
|
+
})
|
|
14
|
+
export class PdmMenubarComponent {
|
|
15
|
+
@Input() menus: PdmMenubarItem[] = [];
|
|
16
|
+
@Input() className = '';
|
|
17
|
+
@Output() itemSelect = new EventEmitter<string>();
|
|
18
|
+
|
|
19
|
+
openIndex = -1;
|
|
20
|
+
|
|
21
|
+
toggle(index: number): void {
|
|
22
|
+
this.openIndex = this.openIndex === index ? -1 : index;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
select(value: string): void {
|
|
26
|
+
this.itemSelect.emit(value);
|
|
27
|
+
this.openIndex = -1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div class="relative" [ngClass]="className">
|
|
2
|
+
<select
|
|
3
|
+
[id]="id"
|
|
4
|
+
[value]="value"
|
|
5
|
+
[disabled]="disabled"
|
|
6
|
+
[attr.aria-invalid]="invalid"
|
|
7
|
+
(change)="onChange($event)"
|
|
8
|
+
[ngClass]="[
|
|
9
|
+
'flex h-9 w-full appearance-none rounded-[8px] border bg-[hsl(var(--background))] px-3 py-2 pr-9 text-sm leading-5 shadow-[0_1px_2px_rgba(0,0,0,0.1)] ring-offset-[hsl(var(--background))] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[hsl(var(--foreground))] disabled:cursor-not-allowed disabled:opacity-50',
|
|
10
|
+
invalid ? 'border-[hsl(var(--destructive))]' : 'border-[hsl(var(--input))]'
|
|
11
|
+
]"
|
|
12
|
+
>
|
|
13
|
+
<option value="" disabled>{{ placeholder }}</option>
|
|
14
|
+
<option *ngFor="let option of options" [value]="option.value" [disabled]="option.disabled">{{ option.label }}</option>
|
|
15
|
+
</select>
|
|
16
|
+
<pdm-icon className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[hsl(var(--muted-foreground))]" name="chevron-down" [size]="16"></pdm-icon>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export interface PdmNativeSelectOption {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
selector: 'pdm-native-select',
|
|
11
|
+
templateUrl: './native-select.component.html',
|
|
12
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
13
|
+
})
|
|
14
|
+
export class PdmNativeSelectComponent {
|
|
15
|
+
@Input() id = '';
|
|
16
|
+
@Input() value = '';
|
|
17
|
+
@Input() disabled = false;
|
|
18
|
+
@Input() invalid = false;
|
|
19
|
+
@Input() options: PdmNativeSelectOption[] = [];
|
|
20
|
+
@Input() placeholder = 'Select an option';
|
|
21
|
+
@Input() className = '';
|
|
22
|
+
|
|
23
|
+
@Output() valueChange = new EventEmitter<string>();
|
|
24
|
+
|
|
25
|
+
onChange(event: Event): void {
|
|
26
|
+
this.valueChange.emit((event.target as HTMLSelectElement).value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<nav [ngClass]="['relative z-10 flex max-w-max flex-1 items-center justify-center', className]">
|
|
2
|
+
<ul class="group flex flex-1 list-none items-center justify-center space-x-1">
|
|
3
|
+
<li *ngFor="let item of items">
|
|
4
|
+
<a
|
|
5
|
+
[href]="item.href || '#'"
|
|
6
|
+
[ngClass]="[
|
|
7
|
+
'group inline-flex h-9 w-max items-center justify-center rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--accent-foreground))]',
|
|
8
|
+
item.active ? 'bg-[hsl(var(--accent))] text-[hsl(var(--accent-foreground))]' : 'text-[hsl(var(--foreground))]'
|
|
9
|
+
]"
|
|
10
|
+
>
|
|
11
|
+
{{ item.label }}
|
|
12
|
+
</a>
|
|
13
|
+
</li>
|
|
14
|
+
</ul>
|
|
15
|
+
</nav>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export interface PdmNavigationItem {
|
|
4
|
+
label: string;
|
|
5
|
+
href?: string;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
selector: 'pdm-navigation-menu',
|
|
11
|
+
templateUrl: './navigation-menu.component.html',
|
|
12
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
13
|
+
})
|
|
14
|
+
export class PdmNavigationMenuComponent {
|
|
15
|
+
@Input() items: PdmNavigationItem[] = [];
|
|
16
|
+
@Input() className = '';
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<nav aria-label="Pagination" [ngClass]="['mx-auto flex w-full justify-center', className]">
|
|
2
|
+
<ul class="flex items-center gap-1">
|
|
3
|
+
<li>
|
|
4
|
+
<button type="button" class="inline-flex h-8 items-center justify-center gap-1 rounded-md px-2 text-sm text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent))] disabled:opacity-50" [disabled]="page <= 1" (click)="setPage(page - 1)">
|
|
5
|
+
<pdm-icon name="chevron-left" [size]="14"></pdm-icon>
|
|
6
|
+
Previous
|
|
7
|
+
</button>
|
|
8
|
+
</li>
|
|
9
|
+
<li *ngFor="let pageNumber of visiblePages">
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
[ngClass]="[
|
|
13
|
+
'inline-flex h-6 min-w-[24px] items-center justify-center rounded-md px-2 text-sm',
|
|
14
|
+
pageNumber === page
|
|
15
|
+
? 'border border-[hsl(var(--border))] bg-[hsl(var(--muted))] text-[hsl(var(--foreground))] shadow-[0_1px_2px_rgba(0,0,0,0.1)]'
|
|
16
|
+
: 'text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--accent-foreground))]'
|
|
17
|
+
]"
|
|
18
|
+
(click)="setPage(pageNumber)"
|
|
19
|
+
>
|
|
20
|
+
{{ pageNumber }}
|
|
21
|
+
</button>
|
|
22
|
+
</li>
|
|
23
|
+
<li>
|
|
24
|
+
<button type="button" class="inline-flex h-8 items-center justify-center gap-1 rounded-md px-2 text-sm text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent))] disabled:opacity-50" [disabled]="page >= pageCount" (click)="setPage(page + 1)">
|
|
25
|
+
Next
|
|
26
|
+
<pdm-icon name="chevron-right" [size]="14"></pdm-icon>
|
|
27
|
+
</button>
|
|
28
|
+
</li>
|
|
29
|
+
</ul>
|
|
30
|
+
</nav>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'pdm-pagination',
|
|
5
|
+
templateUrl: './pagination.component.html',
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
7
|
+
})
|
|
8
|
+
export class PdmPaginationComponent {
|
|
9
|
+
@Input() page = 1;
|
|
10
|
+
@Input() pageCount = 1;
|
|
11
|
+
@Input() maxVisible = 5;
|
|
12
|
+
@Input() className = '';
|
|
13
|
+
|
|
14
|
+
@Output() pageChange = new EventEmitter<number>();
|
|
15
|
+
|
|
16
|
+
get visiblePages(): number[] {
|
|
17
|
+
const total = Math.max(1, this.pageCount);
|
|
18
|
+
const visible = Math.max(1, this.maxVisible);
|
|
19
|
+
const half = Math.floor(visible / 2);
|
|
20
|
+
let start = Math.max(1, this.page - half);
|
|
21
|
+
let end = Math.min(total, start + visible - 1);
|
|
22
|
+
|
|
23
|
+
if (end - start + 1 < visible) {
|
|
24
|
+
start = Math.max(1, end - visible + 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setPage(next: number): void {
|
|
31
|
+
if (next < 1 || next > this.pageCount || next === this.page) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.pageChange.emit(next);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<div class="relative inline-block" [ngClass]="className">
|
|
2
|
+
<button *ngIf="showTrigger" type="button" class="inline-flex h-8 items-center justify-center rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--background))] px-3 text-sm font-medium leading-5 text-[hsl(var(--foreground))] shadow-[0_1px_2px_rgba(0,0,0,0.1)]" [attr.aria-expanded]="open" (click)="toggle()">{{ triggerText }}</button>
|
|
3
|
+
<div *ngIf="open || !showTrigger" [ngClass]="['absolute left-0 top-full z-30 mt-2 min-w-[320px] rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--popover))] p-4 text-[hsl(var(--popover-foreground))] shadow-[0px_4px_6px_0px_rgba(0,0,0,0.09)]', panelClassName]">
|
|
4
|
+
<ng-content></ng-content>
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Input, Output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'pdm-popover',
|
|
5
|
+
templateUrl: './popover.component.html',
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
7
|
+
})
|
|
8
|
+
export class PdmPopoverComponent {
|
|
9
|
+
@Input() open = false;
|
|
10
|
+
@Input() triggerText = 'Open';
|
|
11
|
+
@Input() className = '';
|
|
12
|
+
@Input() panelClassName = '';
|
|
13
|
+
@Input() showTrigger = true;
|
|
14
|
+
@Output() openChange = new EventEmitter<boolean>();
|
|
15
|
+
|
|
16
|
+
constructor(private readonly elementRef: ElementRef<HTMLElement>) {}
|
|
17
|
+
|
|
18
|
+
toggle(): void {
|
|
19
|
+
this.open = !this.open;
|
|
20
|
+
this.openChange.emit(this.open);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@HostListener('document:keydown.escape')
|
|
24
|
+
onEsc(): void {
|
|
25
|
+
if (this.open) {
|
|
26
|
+
this.open = false;
|
|
27
|
+
this.openChange.emit(false);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@HostListener('document:click', ['$event'])
|
|
32
|
+
onDocumentClick(event: MouseEvent): void {
|
|
33
|
+
if (!this.open) return;
|
|
34
|
+
const target = event.target as Node | null;
|
|
35
|
+
if (target && !this.elementRef.nativeElement.contains(target)) {
|
|
36
|
+
this.open = false;
|
|
37
|
+
this.openChange.emit(false);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div
|
|
2
|
+
role="progressbar"
|
|
3
|
+
[attr.aria-valuemin]="0"
|
|
4
|
+
[attr.aria-valuemax]="max"
|
|
5
|
+
[attr.aria-valuenow]="indeterminate ? null : value"
|
|
6
|
+
[ngClass]="['relative h-1.5 w-full overflow-hidden rounded-full bg-[hsl(var(--muted))]', className]"
|
|
7
|
+
>
|
|
8
|
+
<div [ngClass]="['h-full bg-[hsl(var(--foreground))] transition-all', indeterminate ? 'animate-pulse' : '']" [style.width]="width"></div>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'pdm-progress',
|
|
5
|
+
templateUrl: './progress.component.html',
|
|
6
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
7
|
+
})
|
|
8
|
+
export class PdmProgressComponent {
|
|
9
|
+
@Input() value = 0;
|
|
10
|
+
@Input() max = 100;
|
|
11
|
+
@Input() indeterminate = false;
|
|
12
|
+
@Input() className = '';
|
|
13
|
+
|
|
14
|
+
get width(): string {
|
|
15
|
+
if (this.indeterminate) return '100%';
|
|
16
|
+
const safeMax = this.max > 0 ? this.max : 100;
|
|
17
|
+
const pct = Math.min(100, Math.max(0, (this.value / safeMax) * 100));
|
|
18
|
+
return pct + '%';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<div
|
|
2
|
+
role="radiogroup"
|
|
3
|
+
[ngClass]="[
|
|
4
|
+
'gap-2',
|
|
5
|
+
direction === 'horizontal' ? 'inline-flex items-center' : 'grid',
|
|
6
|
+
className
|
|
7
|
+
]"
|
|
8
|
+
>
|
|
9
|
+
<label *ngFor="let option of options" [attr.for]="optionId(option)" class="inline-flex cursor-pointer items-center gap-2">
|
|
10
|
+
<input
|
|
11
|
+
[id]="optionId(option)"
|
|
12
|
+
type="radio"
|
|
13
|
+
[name]="name"
|
|
14
|
+
[value]="option.value"
|
|
15
|
+
[checked]="value === option.value"
|
|
16
|
+
[disabled]="option.disabled"
|
|
17
|
+
class="peer sr-only"
|
|
18
|
+
(change)="onChange($event)"
|
|
19
|
+
/>
|
|
20
|
+
<span class="relative block h-4 w-4 rounded-full border border-[hsl(var(--input))] bg-[hsl(var(--background))] peer-checked:border-[hsl(var(--foreground))] peer-focus-visible:ring-1 peer-focus-visible:ring-[hsl(var(--foreground))]">
|
|
21
|
+
<span class="absolute left-1/2 top-1/2 hidden h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[hsl(var(--foreground))] peer-checked:block"></span>
|
|
22
|
+
</span>
|
|
23
|
+
<span class="text-sm leading-5 text-[hsl(var(--foreground))]">{{ option.label }}</span>
|
|
24
|
+
</label>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export interface PdmRadioOption {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
selector: 'pdm-radio-group',
|
|
11
|
+
templateUrl: './radio-group.component.html',
|
|
12
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
13
|
+
})
|
|
14
|
+
export class PdmRadioGroupComponent {
|
|
15
|
+
@Input() name = 'pdm-radio-group';
|
|
16
|
+
@Input() value = '';
|
|
17
|
+
@Input() options: PdmRadioOption[] = [];
|
|
18
|
+
@Input() direction: 'vertical' | 'horizontal' = 'vertical';
|
|
19
|
+
@Input() className = '';
|
|
20
|
+
|
|
21
|
+
@Output() valueChange = new EventEmitter<string>();
|
|
22
|
+
|
|
23
|
+
optionId(option: PdmRadioOption): string {
|
|
24
|
+
return `${this.name}-${option.value}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onChange(event: Event): void {
|
|
28
|
+
this.valueChange.emit((event.target as HTMLInputElement).value);
|
|
29
|
+
}
|
|
30
|
+
}
|