angular-dev-utils 1.0.1
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/.github/workflows/ci.yml +39 -0
- package/.github/workflows/publish.yml +53 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/angular.json +43 -0
- package/ng-package.json +8 -0
- package/package.json +60 -0
- package/src/lib/components/button/button.component.ts +100 -0
- package/src/lib/components/card/card.component.ts +101 -0
- package/src/lib/components/input/input.component.ts +141 -0
- package/src/lib/components/modal/modal.component.ts +139 -0
- package/src/lib/components/spinner/spinner.component.ts +64 -0
- package/src/lib/components/table/table.component.ts +240 -0
- package/src/lib/models/types.ts +32 -0
- package/src/lib/services/modal.service.ts +120 -0
- package/src/lib/styles.scss +8 -0
- package/src/public-api.ts +17 -0
- package/tailwind.config.js +25 -0
- package/tsconfig.json +32 -0
- package/tsconfig.lib.json +13 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +13 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, develop]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node-version: [18.x, 20.x]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: ${{ matrix.node-version }}
|
|
25
|
+
cache: "npm"
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: npm ci
|
|
29
|
+
|
|
30
|
+
- name: Build library
|
|
31
|
+
run: npm run build
|
|
32
|
+
|
|
33
|
+
- name: Archive build artifacts
|
|
34
|
+
if: matrix.node-version == '20.x'
|
|
35
|
+
uses: actions/upload-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
retention-days: 7
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
version:
|
|
9
|
+
description: "Version to publish (e.g., 1.0.0)"
|
|
10
|
+
required: false
|
|
11
|
+
type: string
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
packages: write
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- name: Checkout repository
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Setup Node.js
|
|
25
|
+
uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: "20.x"
|
|
28
|
+
registry-url: "https://registry.npmjs.org"
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: npm ci
|
|
32
|
+
|
|
33
|
+
- name: Build library
|
|
34
|
+
run: npm run build
|
|
35
|
+
|
|
36
|
+
- name: Copy package files
|
|
37
|
+
run: |
|
|
38
|
+
cp package.json dist/angular-dev-utils/
|
|
39
|
+
cp README.md dist/angular-dev-utils/
|
|
40
|
+
cp LICENSE dist/angular-dev-utils/
|
|
41
|
+
|
|
42
|
+
- name: Publish to npm Registry
|
|
43
|
+
run: |
|
|
44
|
+
cd dist/angular-dev-utils
|
|
45
|
+
npm publish
|
|
46
|
+
env:
|
|
47
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
48
|
+
|
|
49
|
+
- name: Create Git Tag
|
|
50
|
+
if: github.event.inputs.version != ''
|
|
51
|
+
run: |
|
|
52
|
+
git tag v${{ github.event.inputs.version }}
|
|
53
|
+
git push origin v${{ github.event.inputs.version }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hopesf
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Angular Dev Utils
|
|
2
|
+
|
|
3
|
+
Modern Angular component library with standalone components and Tailwind CSS.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/hopesf/Dev-Utils/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/hopesf-angular-dev-utils)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install hopesf-angular-dev-utils
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
No authentication or tokens required!
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
Import components directly in your standalone components:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Component } from "@angular/core";
|
|
23
|
+
import { ButtonComponent, CardComponent, InputComponent } from "hopesf-angular-dev-utils";
|
|
24
|
+
|
|
25
|
+
@Component({
|
|
26
|
+
selector: "app-example",
|
|
27
|
+
standalone: true,
|
|
28
|
+
imports: [ButtonComponent, CardComponent, InputComponent],
|
|
29
|
+
template: `
|
|
30
|
+
<adu-card>
|
|
31
|
+
<adu-input label="Name" [(ngModel)]="name"></adu-input>
|
|
32
|
+
<adu-button variant="primary" (clicked)="submit()">Submit</adu-button>
|
|
33
|
+
</adu-card>
|
|
34
|
+
`,
|
|
35
|
+
})
|
|
36
|
+
export class ExampleComponent {
|
|
37
|
+
name = "";
|
|
38
|
+
|
|
39
|
+
submit() {
|
|
40
|
+
console.log("Name:", this.name);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Documentation
|
|
46
|
+
|
|
47
|
+
For full documentation, visit the [main README](../../README.md).
|
|
48
|
+
|
|
49
|
+
## Components
|
|
50
|
+
|
|
51
|
+
- **Button** - Customizable button with variants and loading states
|
|
52
|
+
- **Input** - Form-ready input with validation
|
|
53
|
+
- **Card** - Container with header/footer sections
|
|
54
|
+
- **Modal** - Dialog with service and component modes
|
|
55
|
+
- **Table** - Data table with sorting and pagination
|
|
56
|
+
- **Spinner** - Loading indicator with overlay option
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/angular.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"newProjectRoot": "projects",
|
|
5
|
+
"projects": {
|
|
6
|
+
"angular-dev-utils": {
|
|
7
|
+
"projectType": "library",
|
|
8
|
+
"root": "",
|
|
9
|
+
"sourceRoot": "src",
|
|
10
|
+
"prefix": "adu",
|
|
11
|
+
"architect": {
|
|
12
|
+
"build": {
|
|
13
|
+
"builder": "@angular-devkit/build-angular:ng-packagr",
|
|
14
|
+
"options": {
|
|
15
|
+
"project": "ng-package.json"
|
|
16
|
+
},
|
|
17
|
+
"configurations": {
|
|
18
|
+
"production": {
|
|
19
|
+
"tsConfig": "tsconfig.lib.prod.json"
|
|
20
|
+
},
|
|
21
|
+
"development": {
|
|
22
|
+
"tsConfig": "tsconfig.lib.json"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"defaultConfiguration": "production"
|
|
26
|
+
},
|
|
27
|
+
"test": {
|
|
28
|
+
"builder": "@angular-devkit/build-angular:karma",
|
|
29
|
+
"options": {
|
|
30
|
+
"tsConfig": "tsconfig.spec.json",
|
|
31
|
+
"polyfills": [
|
|
32
|
+
"zone.js",
|
|
33
|
+
"zone.js/testing"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"cli": {
|
|
41
|
+
"analytics": false
|
|
42
|
+
}
|
|
43
|
+
}
|
package/ng-package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "angular-dev-utils",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Modern Angular UI component library with Tailwind CSS styling",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"angular",
|
|
7
|
+
"components",
|
|
8
|
+
"ui",
|
|
9
|
+
"tailwind",
|
|
10
|
+
"library",
|
|
11
|
+
"standalone"
|
|
12
|
+
],
|
|
13
|
+
"author": "hopesf",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/hopesf/Dev-Utils.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/hopesf/Dev-Utils#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/hopesf/Dev-Utils/issues"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"ng": "ng",
|
|
28
|
+
"build": "ng build angular-dev-utils",
|
|
29
|
+
"watch": "ng build angular-dev-utils --watch",
|
|
30
|
+
"test": "ng test angular-dev-utils"
|
|
31
|
+
},
|
|
32
|
+
"private": false,
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
|
35
|
+
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
|
36
|
+
"@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"tslib": "^2.3.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@angular/animations": "^17.3.0",
|
|
43
|
+
"@angular/common": "^17.3.0",
|
|
44
|
+
"@angular/compiler": "^17.3.0",
|
|
45
|
+
"@angular/core": "^17.3.0",
|
|
46
|
+
"@angular/forms": "^17.3.12",
|
|
47
|
+
"@angular/platform-browser": "^17.3.0",
|
|
48
|
+
"@angular/platform-browser-dynamic": "^17.3.0",
|
|
49
|
+
"rxjs": "~7.8.0",
|
|
50
|
+
"zone.js": "~0.14.3",
|
|
51
|
+
"@angular-devkit/build-angular": "^17.3.0",
|
|
52
|
+
"@angular/cli": "^17.3.0",
|
|
53
|
+
"@angular/compiler-cli": "^17.3.0",
|
|
54
|
+
"autoprefixer": "^10.4.17",
|
|
55
|
+
"ng-packagr": "^17.3.0",
|
|
56
|
+
"postcss": "^8.4.35",
|
|
57
|
+
"tailwindcss": "^3.4.1",
|
|
58
|
+
"typescript": "~5.4.2"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { ButtonVariant, ButtonSize } from '../../models/types';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'adu-button',
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [CommonModule],
|
|
9
|
+
template: `
|
|
10
|
+
<button
|
|
11
|
+
[type]="type"
|
|
12
|
+
[disabled]="disabled || loading"
|
|
13
|
+
[class]="buttonClasses"
|
|
14
|
+
(click)="handleClick($event)"
|
|
15
|
+
>
|
|
16
|
+
<span *ngIf="loading" class="adu-button-spinner"></span>
|
|
17
|
+
<ng-content></ng-content>
|
|
18
|
+
</button>
|
|
19
|
+
`,
|
|
20
|
+
styles: [`
|
|
21
|
+
.adu-button {
|
|
22
|
+
@apply inline-flex items-center justify-center gap-2 font-medium rounded-md transition-colors duration-200;
|
|
23
|
+
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2;
|
|
24
|
+
@apply disabled:pointer-events-none disabled:opacity-50;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Variants */
|
|
28
|
+
.adu-button-primary {
|
|
29
|
+
@apply bg-zinc-900 text-zinc-50 hover:bg-zinc-800 shadow-sm;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.adu-button-secondary {
|
|
33
|
+
@apply bg-zinc-100 text-zinc-900 hover:bg-zinc-200 shadow-sm;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.adu-button-success {
|
|
37
|
+
@apply bg-emerald-600 text-white hover:bg-emerald-700 shadow-sm;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.adu-button-danger {
|
|
41
|
+
@apply bg-red-600 text-zinc-50 hover:bg-red-700 shadow-sm;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.adu-button-warning {
|
|
45
|
+
@apply bg-amber-500 text-white hover:bg-amber-600 shadow-sm;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.adu-button-ghost {
|
|
49
|
+
@apply bg-transparent hover:bg-zinc-100 hover:text-zinc-900 text-zinc-700;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.adu-button-outline {
|
|
53
|
+
@apply border border-zinc-200 bg-white hover:bg-zinc-100 hover:text-zinc-900 text-zinc-900;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Sizes */
|
|
57
|
+
.adu-button-sm {
|
|
58
|
+
@apply h-9 px-3 text-sm rounded-md;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.adu-button-md {
|
|
62
|
+
@apply h-10 px-4 text-sm;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.adu-button-lg {
|
|
66
|
+
@apply h-11 px-8 text-base;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Loading Spinner */
|
|
70
|
+
.adu-button-spinner {
|
|
71
|
+
@apply inline-block w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin;
|
|
72
|
+
}
|
|
73
|
+
`]
|
|
74
|
+
})
|
|
75
|
+
export class ButtonComponent {
|
|
76
|
+
@Input() variant: ButtonVariant = 'primary';
|
|
77
|
+
@Input() size: ButtonSize = 'md';
|
|
78
|
+
@Input() type: 'button' | 'submit' | 'reset' = 'button';
|
|
79
|
+
@Input() disabled = false;
|
|
80
|
+
@Input() loading = false;
|
|
81
|
+
@Input() fullWidth = false;
|
|
82
|
+
|
|
83
|
+
@Output() clicked = new EventEmitter<MouseEvent>();
|
|
84
|
+
|
|
85
|
+
get buttonClasses(): string {
|
|
86
|
+
const classes = ['adu-button', `adu-button-${this.variant}`, `adu-button-${this.size}`];
|
|
87
|
+
|
|
88
|
+
if (this.fullWidth) {
|
|
89
|
+
classes.push('w-full');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return classes.join(' ');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
handleClick(event: MouseEvent): void {
|
|
96
|
+
if (!this.disabled && !this.loading) {
|
|
97
|
+
this.clicked.emit(event);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'ghost' | 'outline';
|
|
2
|
+
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
|
|
4
|
+
export type InputType = 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
5
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
6
|
+
|
|
7
|
+
export interface TableColumn<T = any> {
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
sortable?: boolean;
|
|
11
|
+
width?: string;
|
|
12
|
+
cellTemplate?: (row: T) => string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TableConfig {
|
|
16
|
+
sortable?: boolean;
|
|
17
|
+
pageable?: boolean;
|
|
18
|
+
pageSize?: number;
|
|
19
|
+
striped?: boolean;
|
|
20
|
+
hoverable?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModalConfig {
|
|
24
|
+
title?: string;
|
|
25
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
26
|
+
closeOnBackdrop?: boolean;
|
|
27
|
+
closeOnEscape?: boolean;
|
|
28
|
+
showCloseButton?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SpinnerSize = 'sm' | 'md' | 'lg';
|
|
32
|
+
export type SpinnerColor = 'primary' | 'secondary' | 'white';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Injectable, ComponentRef, ApplicationRef, createComponent, EnvironmentInjector, Type } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
import { ModalConfig } from '../models/types';
|
|
4
|
+
|
|
5
|
+
export interface ModalRef<T = any> {
|
|
6
|
+
close: (result?: T) => void;
|
|
7
|
+
afterClosed: () => Subject<T | undefined>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@Injectable({
|
|
11
|
+
providedIn: 'root'
|
|
12
|
+
})
|
|
13
|
+
export class ModalService {
|
|
14
|
+
private modalComponentRef: ComponentRef<any> | null = null;
|
|
15
|
+
private closeSubject = new Subject<any>();
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private appRef: ApplicationRef,
|
|
19
|
+
private injector: EnvironmentInjector
|
|
20
|
+
) { }
|
|
21
|
+
|
|
22
|
+
open<T, R = any>(component: Type<T>, config: ModalConfig = {}): ModalRef<R> {
|
|
23
|
+
// Close existing modal if any
|
|
24
|
+
if (this.modalComponentRef) {
|
|
25
|
+
this.close();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Create modal wrapper
|
|
29
|
+
const modalWrapper = document.createElement('div');
|
|
30
|
+
modalWrapper.className = this.getModalWrapperClasses(config);
|
|
31
|
+
|
|
32
|
+
// Create backdrop
|
|
33
|
+
const backdrop = document.createElement('div');
|
|
34
|
+
backdrop.className = 'adu-modal-backdrop';
|
|
35
|
+
if (config.closeOnBackdrop !== false) {
|
|
36
|
+
backdrop.onclick = () => this.close();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create modal content
|
|
40
|
+
const modalContent = document.createElement('div');
|
|
41
|
+
modalContent.className = this.getModalContentClasses(config);
|
|
42
|
+
modalContent.onclick = (e) => e.stopPropagation();
|
|
43
|
+
|
|
44
|
+
// Create close button if needed
|
|
45
|
+
if (config.showCloseButton !== false) {
|
|
46
|
+
const closeBtn = document.createElement('button');
|
|
47
|
+
closeBtn.className = 'adu-modal-close';
|
|
48
|
+
closeBtn.innerHTML = '×';
|
|
49
|
+
closeBtn.onclick = () => this.close();
|
|
50
|
+
modalContent.appendChild(closeBtn);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Add title if provided
|
|
54
|
+
if (config.title) {
|
|
55
|
+
const titleEl = document.createElement('div');
|
|
56
|
+
titleEl.className = 'adu-modal-title';
|
|
57
|
+
titleEl.textContent = config.title;
|
|
58
|
+
modalContent.appendChild(titleEl);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create component container
|
|
62
|
+
const componentContainer = document.createElement('div');
|
|
63
|
+
componentContainer.className = 'adu-modal-body';
|
|
64
|
+
modalContent.appendChild(componentContainer);
|
|
65
|
+
|
|
66
|
+
// Assemble modal
|
|
67
|
+
modalWrapper.appendChild(backdrop);
|
|
68
|
+
modalWrapper.appendChild(modalContent);
|
|
69
|
+
document.body.appendChild(modalWrapper);
|
|
70
|
+
|
|
71
|
+
// Create and attach component
|
|
72
|
+
this.modalComponentRef = createComponent(component, {
|
|
73
|
+
environmentInjector: this.injector,
|
|
74
|
+
hostElement: componentContainer
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.appRef.attachView(this.modalComponentRef.hostView);
|
|
78
|
+
|
|
79
|
+
// Handle ESC key
|
|
80
|
+
if (config.closeOnEscape !== false) {
|
|
81
|
+
const escHandler = (e: KeyboardEvent) => {
|
|
82
|
+
if (e.key === 'Escape') {
|
|
83
|
+
this.close();
|
|
84
|
+
document.removeEventListener('keydown', escHandler);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
document.addEventListener('keydown', escHandler);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
close: (result?: R) => this.close(result),
|
|
92
|
+
afterClosed: () => this.closeSubject as Subject<R | undefined>
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
close(result?: any): void {
|
|
97
|
+
if (this.modalComponentRef) {
|
|
98
|
+
this.appRef.detachView(this.modalComponentRef.hostView);
|
|
99
|
+
this.modalComponentRef.destroy();
|
|
100
|
+
this.modalComponentRef = null;
|
|
101
|
+
|
|
102
|
+
// Remove modal from DOM
|
|
103
|
+
const modals = document.querySelectorAll('.adu-modal-wrapper');
|
|
104
|
+
modals.forEach(modal => modal.remove());
|
|
105
|
+
|
|
106
|
+
this.closeSubject.next(result);
|
|
107
|
+
this.closeSubject.complete();
|
|
108
|
+
this.closeSubject = new Subject<any>();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getModalWrapperClasses(config: ModalConfig): string {
|
|
113
|
+
return 'adu-modal-wrapper';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private getModalContentClasses(config: ModalConfig): string {
|
|
117
|
+
const size = config.size || 'md';
|
|
118
|
+
return `adu-modal-content adu-modal-${size}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public API Surface of angular-dev-utils
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Components
|
|
6
|
+
export * from './lib/components/button/button.component';
|
|
7
|
+
export * from './lib/components/input/input.component';
|
|
8
|
+
export * from './lib/components/card/card.component';
|
|
9
|
+
export * from './lib/components/modal/modal.component';
|
|
10
|
+
export * from './lib/components/table/table.component';
|
|
11
|
+
export * from './lib/components/spinner/spinner.component';
|
|
12
|
+
|
|
13
|
+
// Services
|
|
14
|
+
export * from './lib/services/modal.service';
|
|
15
|
+
|
|
16
|
+
// Models & Types
|
|
17
|
+
export * from './lib/models/types';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
content: [
|
|
4
|
+
"./projects/angular-dev-utils/src/**/*.{html,ts}",
|
|
5
|
+
],
|
|
6
|
+
theme: {
|
|
7
|
+
extend: {
|
|
8
|
+
colors: {
|
|
9
|
+
primary: {
|
|
10
|
+
50: '#eff6ff',
|
|
11
|
+
100: '#dbeafe',
|
|
12
|
+
200: '#bfdbfe',
|
|
13
|
+
300: '#93c5fd',
|
|
14
|
+
400: '#60a5fa',
|
|
15
|
+
500: '#3b82f6',
|
|
16
|
+
600: '#2563eb',
|
|
17
|
+
700: '#1d4ed8',
|
|
18
|
+
800: '#1e40af',
|
|
19
|
+
900: '#1e3a8a',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
plugins: [],
|
|
25
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compileOnSave": false,
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist/out-tsc",
|
|
5
|
+
"forceConsistentCasingInFileNames": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noImplicitOverride": true,
|
|
8
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
9
|
+
"noImplicitReturns": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"declaration": false,
|
|
15
|
+
"experimentalDecorators": true,
|
|
16
|
+
"moduleResolution": "node",
|
|
17
|
+
"importHelpers": true,
|
|
18
|
+
"target": "ES2022",
|
|
19
|
+
"module": "ES2022",
|
|
20
|
+
"useDefineForClassFields": false,
|
|
21
|
+
"lib": [
|
|
22
|
+
"ES2022",
|
|
23
|
+
"dom"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"angularCompilerOptions": {
|
|
27
|
+
"enableI18nLegacyMessageIdFormat": false,
|
|
28
|
+
"strictInjectionParameters": true,
|
|
29
|
+
"strictInputAccessModifiers": true,
|
|
30
|
+
"strictTemplates": true
|
|
31
|
+
}
|
|
32
|
+
}
|