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.
Files changed (122) hide show
  1. package/FIGMA_COMPONENT_AUDIT.md +154 -0
  2. package/README.md +72 -0
  3. package/ng-package.json +7 -0
  4. package/package.json +29 -0
  5. package/src/lib/components/accordion/accordion.component.html +34 -0
  6. package/src/lib/components/accordion/accordion.component.ts +38 -0
  7. package/src/lib/components/alert/alert.component.html +52 -0
  8. package/src/lib/components/alert/alert.component.ts +25 -0
  9. package/src/lib/components/alert-dialog/alert-dialog.component.html +41 -0
  10. package/src/lib/components/alert-dialog/alert-dialog.component.ts +45 -0
  11. package/src/lib/components/aspect-ratio/aspect-ratio.component.html +11 -0
  12. package/src/lib/components/aspect-ratio/aspect-ratio.component.ts +18 -0
  13. package/src/lib/components/avatar/avatar.component.html +21 -0
  14. package/src/lib/components/avatar/avatar.component.ts +32 -0
  15. package/src/lib/components/badge/badge.component.html +28 -0
  16. package/src/lib/components/badge/badge.component.ts +23 -0
  17. package/src/lib/components/breadcrumb/breadcrumb.component.html +39 -0
  18. package/src/lib/components/breadcrumb/breadcrumb.component.ts +26 -0
  19. package/src/lib/components/button/button.component.html +15 -0
  20. package/src/lib/components/button/button.component.ts +84 -0
  21. package/src/lib/components/button-group/button-group.component.html +39 -0
  22. package/src/lib/components/button-group/button-group.component.ts +15 -0
  23. package/src/lib/components/calendar/calendar.component.html +73 -0
  24. package/src/lib/components/calendar/calendar.component.ts +78 -0
  25. package/src/lib/components/card/card.component.html +77 -0
  26. package/src/lib/components/card/card.component.ts +39 -0
  27. package/src/lib/components/carousel/carousel.component.html +86 -0
  28. package/src/lib/components/carousel/carousel.component.ts +100 -0
  29. package/src/lib/components/chart/chart.component.html +143 -0
  30. package/src/lib/components/chart/chart.component.ts +147 -0
  31. package/src/lib/components/checkbox/checkbox.component.html +38 -0
  32. package/src/lib/components/checkbox/checkbox.component.ts +32 -0
  33. package/src/lib/components/collapsible/collapsible.component.html +26 -0
  34. package/src/lib/components/collapsible/collapsible.component.ts +29 -0
  35. package/src/lib/components/combobox/combobox.component.html +42 -0
  36. package/src/lib/components/combobox/combobox.component.ts +32 -0
  37. package/src/lib/components/command/command.component.html +55 -0
  38. package/src/lib/components/command/command.component.ts +67 -0
  39. package/src/lib/components/context-menu/context-menu.component.html +47 -0
  40. package/src/lib/components/context-menu/context-menu.component.ts +67 -0
  41. package/src/lib/components/data-table/data-table.component.html +63 -0
  42. package/src/lib/components/data-table/data-table.component.ts +78 -0
  43. package/src/lib/components/date-picker/date-picker.component.html +38 -0
  44. package/src/lib/components/date-picker/date-picker.component.ts +34 -0
  45. package/src/lib/components/dialog/dialog.component.html +78 -0
  46. package/src/lib/components/dialog/dialog.component.ts +55 -0
  47. package/src/lib/components/drawer/drawer.component.html +56 -0
  48. package/src/lib/components/drawer/drawer.component.ts +43 -0
  49. package/src/lib/components/dropdown-menu/dropdown-menu.component.html +56 -0
  50. package/src/lib/components/dropdown-menu/dropdown-menu.component.ts +126 -0
  51. package/src/lib/components/empty/empty.component.html +29 -0
  52. package/src/lib/components/empty/empty.component.ts +35 -0
  53. package/src/lib/components/field/field.component.html +22 -0
  54. package/src/lib/components/field/field.component.ts +28 -0
  55. package/src/lib/components/hover-card/hover-card.component.html +24 -0
  56. package/src/lib/components/hover-card/hover-card.component.ts +36 -0
  57. package/src/lib/components/icon/icon.component.html +286 -0
  58. package/src/lib/components/icon/icon.component.ts +133 -0
  59. package/src/lib/components/input/input.component.html +22 -0
  60. package/src/lib/components/input/input.component.ts +33 -0
  61. package/src/lib/components/input-group/input-group.component.html +31 -0
  62. package/src/lib/components/input-group/input-group.component.ts +26 -0
  63. package/src/lib/components/input-otp/input-otp.component.html +25 -0
  64. package/src/lib/components/input-otp/input-otp.component.ts +146 -0
  65. package/src/lib/components/input-password/input-password.component.html +64 -0
  66. package/src/lib/components/input-password/input-password.component.ts +46 -0
  67. package/src/lib/components/item/item.component.html +10 -0
  68. package/src/lib/components/item/item.component.ts +12 -0
  69. package/src/lib/components/kbd/kbd.component.html +3 -0
  70. package/src/lib/components/kbd/kbd.component.ts +10 -0
  71. package/src/lib/components/label/label.component.html +7 -0
  72. package/src/lib/components/label/label.component.ts +12 -0
  73. package/src/lib/components/menubar/menubar.component.html +16 -0
  74. package/src/lib/components/menubar/menubar.component.ts +29 -0
  75. package/src/lib/components/native-select/native-select.component.html +17 -0
  76. package/src/lib/components/native-select/native-select.component.ts +28 -0
  77. package/src/lib/components/navigation-menu/navigation-menu.component.html +15 -0
  78. package/src/lib/components/navigation-menu/navigation-menu.component.ts +17 -0
  79. package/src/lib/components/pagination/pagination.component.html +30 -0
  80. package/src/lib/components/pagination/pagination.component.ts +37 -0
  81. package/src/lib/components/popover/popover.component.html +6 -0
  82. package/src/lib/components/popover/popover.component.ts +40 -0
  83. package/src/lib/components/progress/progress.component.html +9 -0
  84. package/src/lib/components/progress/progress.component.ts +20 -0
  85. package/src/lib/components/radio-group/radio-group.component.html +25 -0
  86. package/src/lib/components/radio-group/radio-group.component.ts +30 -0
  87. package/src/lib/components/scroll-area/scroll-area.component.html +5 -0
  88. package/src/lib/components/scroll-area/scroll-area.component.ts +11 -0
  89. package/src/lib/components/select/select.component.html +14 -0
  90. package/src/lib/components/select/select.component.ts +27 -0
  91. package/src/lib/components/separator/separator.component.html +5 -0
  92. package/src/lib/components/separator/separator.component.ts +16 -0
  93. package/src/lib/components/sheet/sheet.component.html +10 -0
  94. package/src/lib/components/sheet/sheet.component.ts +28 -0
  95. package/src/lib/components/sidebar/sidebar.component.html +3 -0
  96. package/src/lib/components/sidebar/sidebar.component.ts +11 -0
  97. package/src/lib/components/skeleton/skeleton.component.html +1 -0
  98. package/src/lib/components/skeleton/skeleton.component.ts +10 -0
  99. package/src/lib/components/slider/slider.component.html +15 -0
  100. package/src/lib/components/slider/slider.component.ts +31 -0
  101. package/src/lib/components/sonner/sonner.component.html +10 -0
  102. package/src/lib/components/sonner/sonner.component.ts +25 -0
  103. package/src/lib/components/spinner/spinner.component.html +6 -0
  104. package/src/lib/components/spinner/spinner.component.ts +11 -0
  105. package/src/lib/components/switch/switch.component.html +14 -0
  106. package/src/lib/components/switch/switch.component.ts +20 -0
  107. package/src/lib/components/table/table.component.html +5 -0
  108. package/src/lib/components/table/table.component.ts +10 -0
  109. package/src/lib/components/tabs/tabs.component.html +21 -0
  110. package/src/lib/components/tabs/tabs.component.ts +26 -0
  111. package/src/lib/components/textarea/textarea.component.html +21 -0
  112. package/src/lib/components/textarea/textarea.component.ts +28 -0
  113. package/src/lib/components/toggle/toggle.component.html +16 -0
  114. package/src/lib/components/toggle/toggle.component.ts +29 -0
  115. package/src/lib/components/toggle-group/toggle-group.component.html +17 -0
  116. package/src/lib/components/toggle-group/toggle-group.component.ts +26 -0
  117. package/src/lib/components/tooltip/tooltip.component.html +6 -0
  118. package/src/lib/components/tooltip/tooltip.component.ts +20 -0
  119. package/src/lib/pdm-ui-kit.module.ts +126 -0
  120. package/src/public-api.ts +58 -0
  121. package/tsconfig.lib.json +17 -0
  122. 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,3 @@
1
+ <kbd [ngClass]="['inline-flex h-6 min-w-[24px] items-center justify-center rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--muted))] px-1.5 font-mono text-xs font-medium text-[hsl(var(--muted-foreground))]', className]">
2
+ <ng-content></ng-content>
3
+ </kbd>
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ <div [style.maxHeight]="maxHeight" [ngClass]="['relative overflow-auto rounded-[var(--radius)] border border-[hsl(var(--border))]', className]">
2
+ <div class="p-3">
3
+ <ng-content></ng-content>
4
+ </div>
5
+ </div>