angular-dev-utils 1.0.1 → 1.0.2
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/esm2022/angular-dev-utils.mjs +5 -0
- package/esm2022/lib/components/button/button.component.mjs +68 -0
- package/esm2022/lib/components/card/card.component.mjs +78 -0
- package/esm2022/lib/components/input/input.component.mjs +141 -0
- package/esm2022/lib/components/modal/modal.component.mjs +102 -0
- package/esm2022/lib/components/spinner/spinner.component.mjs +44 -0
- package/esm2022/lib/components/table/table.component.mjs +240 -0
- package/esm2022/lib/models/types.mjs +2 -0
- package/esm2022/lib/services/modal.service.mjs +102 -0
- package/esm2022/public-api.mjs +15 -0
- package/fesm2022/angular-dev-utils.mjs +765 -0
- package/fesm2022/angular-dev-utils.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/components/button/button.component.d.ts +16 -0
- package/lib/components/card/card.component.d.ts +12 -0
- package/lib/components/input/input.component.d.ts +27 -0
- package/lib/components/modal/modal.component.d.ts +17 -0
- package/lib/components/spinner/spinner.component.d.ts +11 -0
- package/lib/components/table/table.component.d.ts +29 -0
- package/{src/lib/models/types.ts → lib/models/types.d.ts} +15 -20
- package/lib/services/modal.service.d.ts +21 -0
- package/package.json +15 -26
- package/{src/public-api.ts → public-api.d.ts} +0 -9
- package/.github/workflows/ci.yml +0 -39
- package/.github/workflows/publish.yml +0 -53
- package/angular.json +0 -43
- package/ng-package.json +0 -8
- package/src/lib/components/button/button.component.ts +0 -100
- package/src/lib/components/card/card.component.ts +0 -101
- package/src/lib/components/input/input.component.ts +0 -141
- package/src/lib/components/modal/modal.component.ts +0 -139
- package/src/lib/components/spinner/spinner.component.ts +0 -64
- package/src/lib/components/table/table.component.ts +0 -240
- package/src/lib/services/modal.service.ts +0 -120
- package/src/lib/styles.scss +0 -8
- package/tailwind.config.js +0 -25
- package/tsconfig.json +0 -32
- package/tsconfig.lib.json +0 -13
- package/tsconfig.lib.prod.json +0 -9
- package/tsconfig.spec.json +0 -13
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { Component, Input } from '@angular/core';
|
|
2
|
-
import { CommonModule } from '@angular/common';
|
|
3
|
-
|
|
4
|
-
@Component({
|
|
5
|
-
selector: 'adu-card',
|
|
6
|
-
standalone: true,
|
|
7
|
-
imports: [CommonModule],
|
|
8
|
-
template: `
|
|
9
|
-
<div [class]="cardClasses">
|
|
10
|
-
<div *ngIf="hasHeader" class="adu-card-header">
|
|
11
|
-
<ng-content select="[card-header]"></ng-content>
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
<div class="adu-card-body">
|
|
15
|
-
<ng-content></ng-content>
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<div *ngIf="hasFooter" class="adu-card-footer">
|
|
19
|
-
<ng-content select="[card-footer]"></ng-content>
|
|
20
|
-
</div>
|
|
21
|
-
</div>
|
|
22
|
-
`,
|
|
23
|
-
styles: [`
|
|
24
|
-
.adu-card {
|
|
25
|
-
@apply rounded-lg border border-zinc-200 bg-white text-zinc-950 shadow-sm;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.adu-card-hoverable:hover {
|
|
29
|
-
@apply shadow-md;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.adu-card-shadow-sm {
|
|
33
|
-
@apply shadow-sm;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.adu-card-shadow-md {
|
|
37
|
-
@apply shadow-md;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.adu-card-shadow-lg {
|
|
41
|
-
@apply shadow-lg;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.adu-card-shadow-xl {
|
|
45
|
-
@apply shadow-xl;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.adu-card-header {
|
|
49
|
-
@apply flex flex-col space-y-1.5 p-6;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.adu-card-body {
|
|
53
|
-
@apply p-6 pt-0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.adu-card-body-compact {
|
|
57
|
-
@apply p-4 pt-0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.adu-card-body-spacious {
|
|
61
|
-
@apply p-8 pt-0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.adu-card-footer {
|
|
65
|
-
@apply flex items-center p-6 pt-0;
|
|
66
|
-
}
|
|
67
|
-
`]
|
|
68
|
-
})
|
|
69
|
-
export class CardComponent {
|
|
70
|
-
@Input() shadow: 'none' | 'sm' | 'md' | 'lg' | 'xl' = 'sm';
|
|
71
|
-
@Input() hoverable = false;
|
|
72
|
-
@Input() padding: 'compact' | 'normal' | 'spacious' = 'normal';
|
|
73
|
-
@Input() hasHeader = false;
|
|
74
|
-
@Input() hasFooter = false;
|
|
75
|
-
|
|
76
|
-
get cardClasses(): string {
|
|
77
|
-
const classes = ['adu-card'];
|
|
78
|
-
|
|
79
|
-
if (this.shadow !== 'none') {
|
|
80
|
-
classes.push(`adu-card-shadow-${this.shadow}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (this.hoverable) {
|
|
84
|
-
classes.push('adu-card-hoverable');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return classes.join(' ');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
get bodyClasses(): string {
|
|
91
|
-
const classes = ['adu-card-body'];
|
|
92
|
-
|
|
93
|
-
if (this.padding === 'compact') {
|
|
94
|
-
classes.push('adu-card-body-compact');
|
|
95
|
-
} else if (this.padding === 'spacious') {
|
|
96
|
-
classes.push('adu-card-body-spacious');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return classes.join(' ');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { Component, Input, forwardRef } from '@angular/core';
|
|
2
|
-
import { CommonModule } from '@angular/common';
|
|
3
|
-
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
4
|
-
import { InputType, InputSize } from '../../models/types';
|
|
5
|
-
|
|
6
|
-
@Component({
|
|
7
|
-
selector: 'adu-input',
|
|
8
|
-
standalone: true,
|
|
9
|
-
imports: [CommonModule],
|
|
10
|
-
providers: [
|
|
11
|
-
{
|
|
12
|
-
provide: NG_VALUE_ACCESSOR,
|
|
13
|
-
useExisting: forwardRef(() => InputComponent),
|
|
14
|
-
multi: true
|
|
15
|
-
}
|
|
16
|
-
],
|
|
17
|
-
template: `
|
|
18
|
-
<div class="adu-input-wrapper">
|
|
19
|
-
<label *ngIf="label" [for]="id" class="adu-input-label">
|
|
20
|
-
{{ label }}
|
|
21
|
-
<span *ngIf="required" class="text-red-500">*</span>
|
|
22
|
-
</label>
|
|
23
|
-
|
|
24
|
-
<div class="relative">
|
|
25
|
-
<input
|
|
26
|
-
[id]="id"
|
|
27
|
-
[type]="type"
|
|
28
|
-
[placeholder]="placeholder"
|
|
29
|
-
[disabled]="disabled"
|
|
30
|
-
[readonly]="readonly"
|
|
31
|
-
[value]="value"
|
|
32
|
-
[class]="inputClasses"
|
|
33
|
-
(input)="onInputChange($event)"
|
|
34
|
-
(blur)="onTouched()"
|
|
35
|
-
/>
|
|
36
|
-
<span *ngIf="icon" class="adu-input-icon">{{ icon }}</span>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<p *ngIf="error" class="adu-input-error">{{ error }}</p>
|
|
40
|
-
<p *ngIf="hint && !error" class="adu-input-hint">{{ hint }}</p>
|
|
41
|
-
</div>
|
|
42
|
-
`,
|
|
43
|
-
styles: [`
|
|
44
|
-
.adu-input-wrapper {
|
|
45
|
-
@apply w-full;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.adu-input-label {
|
|
49
|
-
@apply block text-sm font-medium text-zinc-900 mb-1.5;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.adu-input {
|
|
53
|
-
@apply flex w-full rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm;
|
|
54
|
-
@apply ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium;
|
|
55
|
-
@apply placeholder:text-zinc-500;
|
|
56
|
-
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2;
|
|
57
|
-
@apply disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-zinc-50;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.adu-input-sm {
|
|
61
|
-
@apply h-9 px-3 text-sm;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.adu-input-md {
|
|
65
|
-
@apply h-10 px-3 text-sm;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.adu-input-lg {
|
|
69
|
-
@apply h-11 px-4 text-base;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.adu-input-error-state {
|
|
73
|
-
@apply border-red-500 focus-visible:ring-red-500;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.adu-input-icon {
|
|
77
|
-
@apply absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.adu-input-error {
|
|
81
|
-
@apply mt-1.5 text-sm text-red-600 font-medium;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.adu-input-hint {
|
|
85
|
-
@apply mt-1.5 text-sm text-zinc-500;
|
|
86
|
-
}
|
|
87
|
-
`]
|
|
88
|
-
})
|
|
89
|
-
export class InputComponent implements ControlValueAccessor {
|
|
90
|
-
@Input() id = `adu-input-${Math.random().toString(36).substr(2, 9)}`;
|
|
91
|
-
@Input() label = '';
|
|
92
|
-
@Input() type: InputType = 'text';
|
|
93
|
-
@Input() placeholder = '';
|
|
94
|
-
@Input() size: InputSize = 'md';
|
|
95
|
-
@Input() disabled = false;
|
|
96
|
-
@Input() readonly = false;
|
|
97
|
-
@Input() required = false;
|
|
98
|
-
@Input() error = '';
|
|
99
|
-
@Input() hint = '';
|
|
100
|
-
@Input() icon = '';
|
|
101
|
-
|
|
102
|
-
value = '';
|
|
103
|
-
onChange: (value: string) => void = () => { };
|
|
104
|
-
onTouched: () => void = () => { };
|
|
105
|
-
|
|
106
|
-
get inputClasses(): string {
|
|
107
|
-
const classes = ['adu-input', `adu-input-${this.size}`];
|
|
108
|
-
|
|
109
|
-
if (this.error) {
|
|
110
|
-
classes.push('adu-input-error-state');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (this.icon) {
|
|
114
|
-
classes.push('pr-10');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return classes.join(' ');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
onInputChange(event: Event): void {
|
|
121
|
-
const input = event.target as HTMLInputElement;
|
|
122
|
-
this.value = input.value;
|
|
123
|
-
this.onChange(this.value);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
writeValue(value: string): void {
|
|
127
|
-
this.value = value || '';
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
registerOnChange(fn: (value: string) => void): void {
|
|
131
|
-
this.onChange = fn;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
registerOnTouched(fn: () => void): void {
|
|
135
|
-
this.onTouched = fn;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
setDisabledState(isDisabled: boolean): void {
|
|
139
|
-
this.disabled = isDisabled;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
2
|
-
import { CommonModule } from '@angular/common';
|
|
3
|
-
|
|
4
|
-
@Component({
|
|
5
|
-
selector: 'adu-modal',
|
|
6
|
-
standalone: true,
|
|
7
|
-
imports: [CommonModule],
|
|
8
|
-
template: `
|
|
9
|
-
<div class="adu-modal-wrapper" *ngIf="isOpen" (click)="onBackdropClick()">
|
|
10
|
-
<div [class]="modalClasses" (click)="$event.stopPropagation()">
|
|
11
|
-
<button
|
|
12
|
-
*ngIf="showCloseButton"
|
|
13
|
-
class="adu-modal-close"
|
|
14
|
-
(click)="close()"
|
|
15
|
-
aria-label="Close modal"
|
|
16
|
-
>
|
|
17
|
-
×
|
|
18
|
-
</button>
|
|
19
|
-
|
|
20
|
-
<div *ngIf="title" class="adu-modal-title">
|
|
21
|
-
{{ title }}
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
<div class="adu-modal-body">
|
|
25
|
-
<ng-content></ng-content>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div *ngIf="hasFooter" class="adu-modal-footer">
|
|
29
|
-
<ng-content select="[modal-footer]"></ng-content>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
`,
|
|
34
|
-
styles: [`
|
|
35
|
-
.adu-modal-wrapper {
|
|
36
|
-
@apply fixed inset-0 z-50 flex items-center justify-center p-4;
|
|
37
|
-
background: rgba(0, 0, 0, 0.5);
|
|
38
|
-
backdrop-filter: blur(4px);
|
|
39
|
-
animation: fadeIn 0.15s ease-out;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.adu-modal-content {
|
|
43
|
-
@apply relative bg-white rounded-lg shadow-lg max-h-[90vh] overflow-hidden;
|
|
44
|
-
animation: slideUp 0.2s ease-out;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.adu-modal-sm {
|
|
48
|
-
@apply w-full max-w-sm;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.adu-modal-md {
|
|
52
|
-
@apply w-full max-w-lg;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.adu-modal-lg {
|
|
56
|
-
@apply w-full max-w-2xl;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.adu-modal-xl {
|
|
60
|
-
@apply w-full max-w-4xl;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.adu-modal-full {
|
|
64
|
-
@apply w-full h-full max-w-none rounded-none;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.adu-modal-close {
|
|
68
|
-
@apply absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white;
|
|
69
|
-
@apply transition-opacity hover:opacity-100;
|
|
70
|
-
@apply focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2;
|
|
71
|
-
@apply disabled:pointer-events-none;
|
|
72
|
-
@apply text-zinc-500 hover:text-zinc-900 text-2xl font-light w-6 h-6 flex items-center justify-center;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.adu-modal-title {
|
|
76
|
-
@apply flex flex-col space-y-1.5 text-center sm:text-left p-6 pb-4;
|
|
77
|
-
@apply text-lg font-semibold leading-none tracking-tight text-zinc-950;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.adu-modal-body {
|
|
81
|
-
@apply p-6 pt-0 overflow-y-auto;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.adu-modal-footer {
|
|
85
|
-
@apply flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 p-6 pt-4;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
@keyframes fadeIn {
|
|
89
|
-
from { opacity: 0; }
|
|
90
|
-
to { opacity: 1; }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
@keyframes slideUp {
|
|
94
|
-
from {
|
|
95
|
-
opacity: 0;
|
|
96
|
-
transform: translateY(4px) scale(0.98);
|
|
97
|
-
}
|
|
98
|
-
to {
|
|
99
|
-
opacity: 1;
|
|
100
|
-
transform: translateY(0) scale(1);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.animate-fadeIn {
|
|
105
|
-
animation: fadeIn 0.15s ease-out;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.animate-slideUp {
|
|
109
|
-
animation: slideUp 0.3s ease-out;
|
|
110
|
-
}
|
|
111
|
-
`]
|
|
112
|
-
})
|
|
113
|
-
export class ModalComponent {
|
|
114
|
-
@Input() isOpen = false;
|
|
115
|
-
@Input() title = '';
|
|
116
|
-
@Input() size: 'sm' | 'md' | 'lg' | 'xl' | 'full' = 'md';
|
|
117
|
-
@Input() closeOnBackdrop = true;
|
|
118
|
-
@Input() showCloseButton = true;
|
|
119
|
-
@Input() hasFooter = false;
|
|
120
|
-
|
|
121
|
-
@Output() isOpenChange = new EventEmitter<boolean>();
|
|
122
|
-
@Output() closed = new EventEmitter<void>();
|
|
123
|
-
|
|
124
|
-
get modalClasses(): string {
|
|
125
|
-
return ['adu-modal-content', `adu-modal-${this.size}`].join(' ');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
close(): void {
|
|
129
|
-
this.isOpen = false;
|
|
130
|
-
this.isOpenChange.emit(false);
|
|
131
|
-
this.closed.emit();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
onBackdropClick(): void {
|
|
135
|
-
if (this.closeOnBackdrop) {
|
|
136
|
-
this.close();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Component, Input } from '@angular/core';
|
|
2
|
-
import { CommonModule } from '@angular/common';
|
|
3
|
-
import { SpinnerSize, SpinnerColor } from '../../models/types';
|
|
4
|
-
|
|
5
|
-
@Component({
|
|
6
|
-
selector: 'adu-spinner',
|
|
7
|
-
standalone: true,
|
|
8
|
-
imports: [CommonModule],
|
|
9
|
-
template: `
|
|
10
|
-
<div *ngIf="overlay" class="adu-spinner-overlay">
|
|
11
|
-
<div [class]="spinnerClasses"></div>
|
|
12
|
-
<p *ngIf="message" class="adu-spinner-message">{{ message }}</p>
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
<div *ngIf="!overlay" [class]="spinnerClasses"></div>
|
|
16
|
-
`,
|
|
17
|
-
styles: [`
|
|
18
|
-
.adu-spinner {
|
|
19
|
-
@apply inline-block border-2 border-zinc-200 border-t-zinc-900 rounded-full animate-spin;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.adu-spinner-sm {
|
|
23
|
-
@apply w-4 h-4 border-2;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.adu-spinner-md {
|
|
27
|
-
@apply w-6 h-6 border-2;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
.adu-spinner-lg {
|
|
31
|
-
@apply w-10 h-10 border-[3px];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.adu-spinner-primary {
|
|
35
|
-
@apply border-zinc-200 border-t-zinc-900;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.adu-spinner-secondary {
|
|
39
|
-
@apply border-zinc-300 border-t-zinc-600;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
.adu-spinner-white {
|
|
43
|
-
@apply border-zinc-100 border-t-white;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.adu-spinner-overlay {
|
|
47
|
-
@apply fixed inset-0 bg-black/50 backdrop-blur-sm flex flex-col items-center justify-center z-50;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.adu-spinner-message {
|
|
51
|
-
@apply mt-4 text-white text-sm font-medium;
|
|
52
|
-
}
|
|
53
|
-
`]
|
|
54
|
-
})
|
|
55
|
-
export class SpinnerComponent {
|
|
56
|
-
@Input() size: SpinnerSize = 'md';
|
|
57
|
-
@Input() color: SpinnerColor = 'primary';
|
|
58
|
-
@Input() overlay = false;
|
|
59
|
-
@Input() message = '';
|
|
60
|
-
|
|
61
|
-
get spinnerClasses(): string {
|
|
62
|
-
return ['adu-spinner', `adu-spinner-${this.size}`, `adu-spinner-${this.color}`].join(' ');
|
|
63
|
-
}
|
|
64
|
-
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter, ContentChild, TemplateRef } from '@angular/core';
|
|
2
|
-
import { CommonModule } from '@angular/common';
|
|
3
|
-
import { TableColumn, TableConfig } from '../../models/types';
|
|
4
|
-
|
|
5
|
-
@Component({
|
|
6
|
-
selector: 'adu-table',
|
|
7
|
-
standalone: true,
|
|
8
|
-
imports: [CommonModule],
|
|
9
|
-
template: `
|
|
10
|
-
<div class="adu-table-container">
|
|
11
|
-
<table class="adu-table" [class.adu-table-striped]="config.striped" [class.adu-table-hoverable]="config.hoverable">
|
|
12
|
-
<thead class="adu-table-header">
|
|
13
|
-
<tr>
|
|
14
|
-
<th
|
|
15
|
-
*ngFor="let column of columns"
|
|
16
|
-
[style.width]="column.width"
|
|
17
|
-
[class.adu-table-sortable]="column.sortable && config.sortable"
|
|
18
|
-
(click)="onSort(column)"
|
|
19
|
-
class="adu-table-th"
|
|
20
|
-
>
|
|
21
|
-
<div class="flex items-center justify-between">
|
|
22
|
-
<span>{{ column.label }}</span>
|
|
23
|
-
<span *ngIf="column.sortable && config.sortable" class="adu-table-sort-icon">
|
|
24
|
-
<span *ngIf="sortColumn === column.key">
|
|
25
|
-
{{ sortDirection === 'asc' ? '↑' : '↓' }}
|
|
26
|
-
</span>
|
|
27
|
-
<span *ngIf="sortColumn !== column.key" class="text-gray-300">↕</span>
|
|
28
|
-
</span>
|
|
29
|
-
</div>
|
|
30
|
-
</th>
|
|
31
|
-
</tr>
|
|
32
|
-
</thead>
|
|
33
|
-
<tbody class="adu-table-body">
|
|
34
|
-
<tr *ngFor="let row of paginatedData; let i = index" class="adu-table-row">
|
|
35
|
-
<td *ngFor="let column of columns" class="adu-table-td">
|
|
36
|
-
<ng-container *ngIf="cellTemplate; else defaultCell">
|
|
37
|
-
<ng-container *ngTemplateOutlet="cellTemplate; context: { $implicit: row, column: column }"></ng-container>
|
|
38
|
-
</ng-container>
|
|
39
|
-
<ng-template #defaultCell>
|
|
40
|
-
{{ getCellValue(row, column) }}
|
|
41
|
-
</ng-template>
|
|
42
|
-
</td>
|
|
43
|
-
</tr>
|
|
44
|
-
<tr *ngIf="paginatedData.length === 0" class="adu-table-empty">
|
|
45
|
-
<td [attr.colspan]="columns.length" class="text-center py-8 text-gray-500">
|
|
46
|
-
<ng-content select="[empty-state]"></ng-content>
|
|
47
|
-
<span *ngIf="!hasEmptyState">No data available</span>
|
|
48
|
-
</td>
|
|
49
|
-
</tr>
|
|
50
|
-
</tbody>
|
|
51
|
-
</table>
|
|
52
|
-
|
|
53
|
-
<!-- Pagination -->
|
|
54
|
-
<div *ngIf="config.pageable && totalPages > 1" class="adu-table-pagination">
|
|
55
|
-
<button
|
|
56
|
-
class="adu-pagination-btn"
|
|
57
|
-
[disabled]="currentPage === 1"
|
|
58
|
-
(click)="goToPage(currentPage - 1)"
|
|
59
|
-
>
|
|
60
|
-
Previous
|
|
61
|
-
</button>
|
|
62
|
-
|
|
63
|
-
<div class="adu-pagination-info">
|
|
64
|
-
Page {{ currentPage }} of {{ totalPages }}
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<button
|
|
68
|
-
class="adu-pagination-btn"
|
|
69
|
-
[disabled]="currentPage === totalPages"
|
|
70
|
-
(click)="goToPage(currentPage + 1)"
|
|
71
|
-
>
|
|
72
|
-
Next
|
|
73
|
-
</button>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
`,
|
|
77
|
-
styles: [`
|
|
78
|
-
.adu-table-container {
|
|
79
|
-
@apply w-full overflow-x-auto rounded-md border border-zinc-200;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.adu-table {
|
|
83
|
-
@apply w-full caption-bottom text-sm;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.adu-table-header {
|
|
87
|
-
@apply border-b border-zinc-200;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.adu-table-th {
|
|
91
|
-
@apply h-12 px-4 text-left align-middle font-medium text-zinc-500;
|
|
92
|
-
@apply [&:has([role=checkbox])]:pr-0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.adu-table-sortable {
|
|
96
|
-
@apply cursor-pointer hover:text-zinc-900 transition-colors;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.adu-table-sort-icon {
|
|
100
|
-
@apply ml-2 inline-block text-zinc-400;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.adu-table-body {
|
|
104
|
-
@apply [&_tr:last-child]:border-0;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.adu-table-row {
|
|
108
|
-
@apply border-b border-zinc-200 transition-colors;
|
|
109
|
-
@apply hover:bg-zinc-50/50;
|
|
110
|
-
@apply data-[state=selected]:bg-zinc-100;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.adu-table-striped .adu-table-row:nth-child(even) {
|
|
114
|
-
@apply bg-zinc-50/30;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.adu-table-hoverable .adu-table-row:hover {
|
|
118
|
-
@apply bg-zinc-50;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.adu-table-td {
|
|
122
|
-
@apply p-4 align-middle text-zinc-900;
|
|
123
|
-
@apply [&:has([role=checkbox])]:pr-0;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.adu-table-empty {
|
|
127
|
-
@apply text-center py-10 text-zinc-500;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.adu-table-pagination {
|
|
131
|
-
@apply flex items-center justify-between px-4 py-3 border-t border-zinc-200 bg-zinc-50/50;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.adu-pagination-btn {
|
|
135
|
-
@apply inline-flex items-center justify-center rounded-md text-sm font-medium;
|
|
136
|
-
@apply ring-offset-white transition-colors;
|
|
137
|
-
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2;
|
|
138
|
-
@apply disabled:pointer-events-none disabled:opacity-50;
|
|
139
|
-
@apply border border-zinc-200 bg-white hover:bg-zinc-100 hover:text-zinc-900;
|
|
140
|
-
@apply h-9 px-4 py-2;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.adu-pagination-info {
|
|
144
|
-
@apply text-sm text-zinc-600 font-medium;
|
|
145
|
-
}
|
|
146
|
-
`]
|
|
147
|
-
})
|
|
148
|
-
export class TableComponent<T = any> {
|
|
149
|
-
@Input() data: T[] = [];
|
|
150
|
-
@Input() columns: TableColumn<T>[] = [];
|
|
151
|
-
@Input() config: TableConfig = {
|
|
152
|
-
sortable: true,
|
|
153
|
-
pageable: true,
|
|
154
|
-
pageSize: 10,
|
|
155
|
-
striped: true,
|
|
156
|
-
hoverable: true
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
@ContentChild('cellTemplate') cellTemplate?: TemplateRef<any>;
|
|
160
|
-
@Output() rowClicked = new EventEmitter<T>();
|
|
161
|
-
@Output() sortChanged = new EventEmitter<{ column: string; direction: 'asc' | 'desc' }>();
|
|
162
|
-
|
|
163
|
-
sortColumn = '';
|
|
164
|
-
sortDirection: 'asc' | 'desc' = 'asc';
|
|
165
|
-
currentPage = 1;
|
|
166
|
-
hasEmptyState = false;
|
|
167
|
-
|
|
168
|
-
ngOnInit() {
|
|
169
|
-
this.config = { ...this.getDefaultConfig(), ...this.config };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
get sortedData(): T[] {
|
|
173
|
-
if (!this.config.sortable || !this.sortColumn) {
|
|
174
|
-
return this.data;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return [...this.data].sort((a, b) => {
|
|
178
|
-
const aVal = this.getNestedValue(a, this.sortColumn);
|
|
179
|
-
const bVal = this.getNestedValue(b, this.sortColumn);
|
|
180
|
-
|
|
181
|
-
if (aVal < bVal) return this.sortDirection === 'asc' ? -1 : 1;
|
|
182
|
-
if (aVal > bVal) return this.sortDirection === 'asc' ? 1 : -1;
|
|
183
|
-
return 0;
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
get paginatedData(): T[] {
|
|
188
|
-
if (!this.config.pageable) {
|
|
189
|
-
return this.sortedData;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const start = (this.currentPage - 1) * (this.config.pageSize || 10);
|
|
193
|
-
const end = start + (this.config.pageSize || 10);
|
|
194
|
-
return this.sortedData.slice(start, end);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
get totalPages(): number {
|
|
198
|
-
return Math.ceil(this.data.length / (this.config.pageSize || 10));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
onSort(column: TableColumn<T>): void {
|
|
202
|
-
if (!column.sortable || !this.config.sortable) return;
|
|
203
|
-
|
|
204
|
-
if (this.sortColumn === column.key) {
|
|
205
|
-
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
206
|
-
} else {
|
|
207
|
-
this.sortColumn = column.key;
|
|
208
|
-
this.sortDirection = 'asc';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
this.sortChanged.emit({ column: column.key, direction: this.sortDirection });
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
goToPage(page: number): void {
|
|
215
|
-
if (page >= 1 && page <= this.totalPages) {
|
|
216
|
-
this.currentPage = page;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
getCellValue(row: T, column: TableColumn<T>): any {
|
|
221
|
-
if (column.cellTemplate) {
|
|
222
|
-
return column.cellTemplate(row);
|
|
223
|
-
}
|
|
224
|
-
return this.getNestedValue(row, column.key);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private getNestedValue(obj: any, path: string): any {
|
|
228
|
-
return path.split('.').reduce((current, prop) => current?.[prop], obj);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
private getDefaultConfig(): TableConfig {
|
|
232
|
-
return {
|
|
233
|
-
sortable: true,
|
|
234
|
-
pageable: true,
|
|
235
|
-
pageSize: 10,
|
|
236
|
-
striped: true,
|
|
237
|
-
hoverable: true
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|