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.
@@ -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
+ [![CI](https://github.com/hopesf/Dev-Utils/actions/workflows/ci.yml/badge.svg)](https://github.com/hopesf/Dev-Utils/actions/workflows/ci.yml)
6
+ [![npm version](https://badge.fury.io/js/hopesf-angular-dev-utils.svg)](https://www.npmjs.com/package/hopesf-angular-dev-utils)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "./dist/angular-dev-utils",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts",
6
+ "cssUrl": "inline"
7
+ }
8
+ }
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,8 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Angular Dev Utils - Base Styles */
6
+ .adu-component {
7
+ @apply transition-all duration-200 ease-in-out;
8
+ }
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/lib",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "inlineSources": true,
8
+ "types": []
9
+ },
10
+ "exclude": [
11
+ "**/*.spec.ts"
12
+ ]
13
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.lib.json",
3
+ "compilerOptions": {
4
+ "declarationMap": false
5
+ },
6
+ "angularCompilerOptions": {
7
+ "compilationMode": "partial"
8
+ }
9
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/spec",
5
+ "types": [
6
+ "jasmine"
7
+ ]
8
+ },
9
+ "include": [
10
+ "**/*.spec.ts",
11
+ "**/*.d.ts"
12
+ ]
13
+ }