create-ng-tailwind 1.0.0 → 2.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/CHANGELOG.md +229 -8
- package/CLAUDE.md +178 -0
- package/LICENSE +1 -1
- package/README.md +151 -246
- package/bin/create-ng-tailwind.js +5 -407
- package/lib/cli/index.js +222 -0
- package/lib/cli/interactive.js +26 -0
- package/lib/cli/validators.js +23 -0
- package/lib/config/defaults.js +7 -0
- package/lib/managers/ProjectManager.js +97 -0
- package/lib/managers/TailwindManager.js +100 -0
- package/lib/managers/TemplateManager.js +39 -0
- package/lib/templates/index.js +7 -0
- package/lib/templates/minimal/index.js +24 -0
- package/lib/templates/starter/advanced-features.js +528 -0
- package/lib/templates/starter/features.js +700 -0
- package/lib/templates/starter/index.js +4117 -0
- package/lib/templates/starter/ui-features.js +437 -0
- package/lib/utils/ai-config.js +606 -0
- package/lib/utils/constants.js +14 -0
- package/lib/utils/helpers.js +60 -0
- package/lib/utils/logger.js +109 -0
- package/package.json +6 -15
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
// ==================== MODAL SERVICE & COMPONENT ====================
|
|
5
|
+
|
|
6
|
+
async function createModalService(config) {
|
|
7
|
+
const modalService = `import { Injectable, signal } from '@angular/core';
|
|
8
|
+
|
|
9
|
+
export interface ModalData {
|
|
10
|
+
message?: string;
|
|
11
|
+
type?: 'confirm' | 'alert' | 'custom';
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ModalConfig {
|
|
16
|
+
title?: string;
|
|
17
|
+
data?: ModalData;
|
|
18
|
+
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
19
|
+
closeOnBackdrop?: boolean;
|
|
20
|
+
closeOnEscape?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Injectable({
|
|
24
|
+
providedIn: 'root'
|
|
25
|
+
})
|
|
26
|
+
export class ModalService {
|
|
27
|
+
// Reactive signals for modal state
|
|
28
|
+
isOpen = signal(false);
|
|
29
|
+
modalConfig = signal<ModalConfig | null>(null);
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
private resolve: ((value: any) => void) | null = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Open a modal with configuration
|
|
35
|
+
*/
|
|
36
|
+
open<T = boolean>(config: ModalConfig): Promise<T> {
|
|
37
|
+
this.modalConfig.set(config);
|
|
38
|
+
this.isOpen.set(true);
|
|
39
|
+
|
|
40
|
+
return new Promise<T>((resolve) => {
|
|
41
|
+
this.resolve = resolve as (value: unknown) => void;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Close the modal with optional result
|
|
47
|
+
*/
|
|
48
|
+
close<T = boolean>(result?: T): void {
|
|
49
|
+
this.isOpen.set(false);
|
|
50
|
+
this.modalConfig.set(null);
|
|
51
|
+
|
|
52
|
+
if (this.resolve) {
|
|
53
|
+
this.resolve(result);
|
|
54
|
+
this.resolve = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Show a confirmation dialog
|
|
60
|
+
*/
|
|
61
|
+
async confirm(message: string, title = 'Confirm'): Promise<boolean> {
|
|
62
|
+
const result = await this.open({
|
|
63
|
+
title,
|
|
64
|
+
data: { message, type: 'confirm' },
|
|
65
|
+
size: 'sm',
|
|
66
|
+
closeOnBackdrop: false
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return result === true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Show an alert dialog
|
|
74
|
+
*/
|
|
75
|
+
async alert(message: string, title = 'Alert'): Promise<void> {
|
|
76
|
+
await this.open({
|
|
77
|
+
title,
|
|
78
|
+
data: { message, type: 'alert' },
|
|
79
|
+
size: 'sm',
|
|
80
|
+
closeOnBackdrop: true
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}`;
|
|
84
|
+
|
|
85
|
+
await fs.writeFile(
|
|
86
|
+
path.join(config.fullPath, "src/app/core/services/modal.service.ts"),
|
|
87
|
+
modalService,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function createModalComponent(config) {
|
|
92
|
+
const modalComponent = `import { Component, inject, HostListener } from '@angular/core';
|
|
93
|
+
import { CommonModule } from '@angular/common';
|
|
94
|
+
import { ModalService } from '@core/services/modal.service';
|
|
95
|
+
import { ButtonComponent } from '@shared/components/button/button.component';
|
|
96
|
+
|
|
97
|
+
@Component({
|
|
98
|
+
selector: 'app-modal',
|
|
99
|
+
imports: [CommonModule, ButtonComponent],
|
|
100
|
+
template: \`
|
|
101
|
+
@if (modalService.isOpen()) {
|
|
102
|
+
<!-- Backdrop -->
|
|
103
|
+
<div
|
|
104
|
+
class="fixed inset-0 [backdrop-filter:blur(2px)] bg-[#4a494b4d] z-40 transition-opacity"
|
|
105
|
+
(click)="onBackdropClick()"
|
|
106
|
+
role="presentation"
|
|
107
|
+
tabindex="-1"
|
|
108
|
+
(keydown)="onBackdropKeydown($event)">
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<!-- Modal Container -->
|
|
112
|
+
<div class="fixed inset-0 z-50 overflow-y-auto">
|
|
113
|
+
<div class="flex min-h-full items-center justify-center p-4">
|
|
114
|
+
<!-- Modal Content -->
|
|
115
|
+
<div
|
|
116
|
+
[class]="getModalClasses()"
|
|
117
|
+
class="relative bg-white rounded-lg shadow-xl transform transition-all animate-fade-in">
|
|
118
|
+
|
|
119
|
+
<!-- Header -->
|
|
120
|
+
@if (modalService.modalConfig()?.title) {
|
|
121
|
+
<div class="border-b border-gray-200 px-6 py-4">
|
|
122
|
+
<div class="flex items-center justify-between">
|
|
123
|
+
<h3 class="text-xl font-semibold text-gray-900">
|
|
124
|
+
{{ modalService.modalConfig()?.title }}
|
|
125
|
+
</h3>
|
|
126
|
+
<button
|
|
127
|
+
(click)="modalService.close()"
|
|
128
|
+
class="text-gray-400 hover:text-gray-600 transition-colors">
|
|
129
|
+
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
130
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
131
|
+
</svg>
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
<!-- Body -->
|
|
138
|
+
<div class="px-6 py-4">
|
|
139
|
+
@if (getModalType() === 'confirm' || getModalType() === 'alert') {
|
|
140
|
+
<p class="text-gray-700">{{ getModalMessage() }}</p>
|
|
141
|
+
} @else {
|
|
142
|
+
<!-- Custom component content would go here -->
|
|
143
|
+
<div>
|
|
144
|
+
<p class="text-gray-600">Modal content goes here</p>
|
|
145
|
+
</div>
|
|
146
|
+
}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- Footer -->
|
|
150
|
+
@if (getModalType() === 'confirm') {
|
|
151
|
+
<div class="border-t border-gray-200 px-6 py-4 flex justify-end space-x-3">
|
|
152
|
+
<app-button
|
|
153
|
+
variant="secondary"
|
|
154
|
+
(click)="modalService.close(false)">
|
|
155
|
+
Cancel
|
|
156
|
+
</app-button>
|
|
157
|
+
<app-button
|
|
158
|
+
(click)="modalService.close(true)">
|
|
159
|
+
Confirm
|
|
160
|
+
</app-button>
|
|
161
|
+
</div>
|
|
162
|
+
} @else if (getModalType() === 'alert') {
|
|
163
|
+
<div class="border-t border-gray-200 px-6 py-4 flex justify-end">
|
|
164
|
+
<app-button
|
|
165
|
+
(click)="modalService.close()">
|
|
166
|
+
OK
|
|
167
|
+
</app-button>
|
|
168
|
+
</div>
|
|
169
|
+
}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
}
|
|
174
|
+
\`,
|
|
175
|
+
styles: [\`
|
|
176
|
+
@keyframes fade-in {
|
|
177
|
+
from {
|
|
178
|
+
opacity: 0;
|
|
179
|
+
transform: scale(0.95);
|
|
180
|
+
}
|
|
181
|
+
to {
|
|
182
|
+
opacity: 1;
|
|
183
|
+
transform: scale(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.animate-fade-in {
|
|
188
|
+
animation: fade-in 0.2s ease-out;
|
|
189
|
+
}
|
|
190
|
+
\`]
|
|
191
|
+
})
|
|
192
|
+
export class ModalComponent {
|
|
193
|
+
modalService = inject(ModalService);
|
|
194
|
+
|
|
195
|
+
@HostListener('document:keydown.escape')
|
|
196
|
+
onEscapeKey(): void {
|
|
197
|
+
const config = this.modalService.modalConfig();
|
|
198
|
+
if (config?.closeOnEscape !== false) {
|
|
199
|
+
this.modalService.close();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
onBackdropClick(): void {
|
|
204
|
+
const config = this.modalService.modalConfig();
|
|
205
|
+
if (config?.closeOnBackdrop !== false) {
|
|
206
|
+
this.modalService.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
onBackdropKeydown(event: KeyboardEvent): void {
|
|
211
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
this.onBackdropClick();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getModalClasses(): string {
|
|
218
|
+
const size = this.modalService.modalConfig()?.size || 'md';
|
|
219
|
+
const sizeClasses = {
|
|
220
|
+
sm: 'max-w-sm',
|
|
221
|
+
md: 'max-w-md',
|
|
222
|
+
lg: 'max-w-lg',
|
|
223
|
+
xl: 'max-w-xl',
|
|
224
|
+
full: 'max-w-full mx-4'
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return \`w-full \${sizeClasses[size]}\`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
getModalType(): string | undefined {
|
|
231
|
+
return this.modalService.modalConfig()?.data?.type;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getModalMessage(): string {
|
|
235
|
+
return this.modalService.modalConfig()?.data?.message || '';
|
|
236
|
+
}
|
|
237
|
+
}`;
|
|
238
|
+
|
|
239
|
+
await fs.writeFile(
|
|
240
|
+
path.join(
|
|
241
|
+
config.fullPath,
|
|
242
|
+
"src/app/shared/components/modal/modal.component.ts",
|
|
243
|
+
),
|
|
244
|
+
modalComponent,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ==================== ADDITIONAL PIPES ====================
|
|
249
|
+
|
|
250
|
+
async function createTruncatePipe(config) {
|
|
251
|
+
const truncatePipe = `import { Pipe, PipeTransform } from '@angular/core';
|
|
252
|
+
|
|
253
|
+
@Pipe({
|
|
254
|
+
name: 'truncate',
|
|
255
|
+
pure: true
|
|
256
|
+
})
|
|
257
|
+
export class TruncatePipe implements PipeTransform {
|
|
258
|
+
transform(value: string, limit = 50, completeWords = false, ellipsis = '...'): string {
|
|
259
|
+
if (!value) return '';
|
|
260
|
+
|
|
261
|
+
if (value.length <= limit) {
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (completeWords) {
|
|
266
|
+
limit = value.substring(0, limit).lastIndexOf(' ');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return value.substring(0, limit) + ellipsis;
|
|
270
|
+
}
|
|
271
|
+
}`;
|
|
272
|
+
|
|
273
|
+
await fs.writeFile(
|
|
274
|
+
path.join(config.fullPath, "src/app/shared/pipes/truncate.pipe.ts"),
|
|
275
|
+
truncatePipe,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ==================== ADDITIONAL DIRECTIVES ====================
|
|
280
|
+
|
|
281
|
+
async function createClickOutsideDirective(config) {
|
|
282
|
+
const clickOutsideDirective = `import { Directive, ElementRef, EventEmitter, HostListener, Output, inject } from '@angular/core';
|
|
283
|
+
|
|
284
|
+
@Directive({
|
|
285
|
+
selector: '[appClickOutside]',
|
|
286
|
+
standalone: true
|
|
287
|
+
})
|
|
288
|
+
export class ClickOutsideDirective {
|
|
289
|
+
@Output() appClickOutside = new EventEmitter<void>();
|
|
290
|
+
|
|
291
|
+
private elementRef = inject(ElementRef);
|
|
292
|
+
|
|
293
|
+
@HostListener('document:click', ['$event'])
|
|
294
|
+
onClick(event: MouseEvent): void {
|
|
295
|
+
const target = event.target as HTMLElement;
|
|
296
|
+
|
|
297
|
+
if (!target) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const clickedInside = this.elementRef.nativeElement.contains(target);
|
|
302
|
+
|
|
303
|
+
if (!clickedInside) {
|
|
304
|
+
this.appClickOutside.emit();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}`;
|
|
308
|
+
|
|
309
|
+
await fs.writeFile(
|
|
310
|
+
path.join(
|
|
311
|
+
config.fullPath,
|
|
312
|
+
"src/app/shared/directives/click-outside.directive.ts",
|
|
313
|
+
),
|
|
314
|
+
clickOutsideDirective,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function createTooltipDirective(config) {
|
|
319
|
+
const tooltipDirective = `import { Directive, ElementRef, HostListener, Input, Renderer2, inject, OnDestroy } from '@angular/core';
|
|
320
|
+
|
|
321
|
+
@Directive({
|
|
322
|
+
selector: '[appTooltip]',
|
|
323
|
+
standalone: true
|
|
324
|
+
})
|
|
325
|
+
export class TooltipDirective implements OnDestroy {
|
|
326
|
+
@Input() appTooltip = '';
|
|
327
|
+
@Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
|
|
328
|
+
|
|
329
|
+
private elementRef = inject(ElementRef);
|
|
330
|
+
private renderer = inject(Renderer2);
|
|
331
|
+
private tooltipElement: HTMLElement | null = null;
|
|
332
|
+
|
|
333
|
+
@HostListener('mouseenter')
|
|
334
|
+
onMouseEnter(): void {
|
|
335
|
+
if (!this.appTooltip) return;
|
|
336
|
+
|
|
337
|
+
this.createTooltip();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@HostListener('mouseleave')
|
|
341
|
+
onMouseLeave(): void {
|
|
342
|
+
this.removeTooltip();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
ngOnDestroy(): void {
|
|
346
|
+
this.removeTooltip();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private createTooltip(): void {
|
|
350
|
+
// Create tooltip element
|
|
351
|
+
this.tooltipElement = this.renderer.createElement('div');
|
|
352
|
+
this.renderer.addClass(this.tooltipElement, 'tooltip');
|
|
353
|
+
this.renderer.addClass(this.tooltipElement, 'absolute');
|
|
354
|
+
this.renderer.addClass(this.tooltipElement, 'z-50');
|
|
355
|
+
this.renderer.addClass(this.tooltipElement, 'px-3');
|
|
356
|
+
this.renderer.addClass(this.tooltipElement, 'py-2');
|
|
357
|
+
this.renderer.addClass(this.tooltipElement, 'text-sm');
|
|
358
|
+
this.renderer.addClass(this.tooltipElement, 'text-white');
|
|
359
|
+
this.renderer.addClass(this.tooltipElement, 'bg-gray-900');
|
|
360
|
+
this.renderer.addClass(this.tooltipElement, 'rounded-lg');
|
|
361
|
+
this.renderer.addClass(this.tooltipElement, 'shadow-lg');
|
|
362
|
+
this.renderer.addClass(this.tooltipElement, 'whitespace-nowrap');
|
|
363
|
+
this.renderer.addClass(this.tooltipElement, 'pointer-events-none');
|
|
364
|
+
|
|
365
|
+
// Set tooltip text
|
|
366
|
+
const text = this.renderer.createText(this.appTooltip);
|
|
367
|
+
this.renderer.appendChild(this.tooltipElement, text);
|
|
368
|
+
|
|
369
|
+
// Append to body
|
|
370
|
+
this.renderer.appendChild(document.body, this.tooltipElement);
|
|
371
|
+
|
|
372
|
+
// Position tooltip
|
|
373
|
+
this.positionTooltip();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private positionTooltip(): void {
|
|
377
|
+
if (!this.tooltipElement) return;
|
|
378
|
+
|
|
379
|
+
const hostPos = this.elementRef.nativeElement.getBoundingClientRect();
|
|
380
|
+
const tooltipPos = this.tooltipElement.getBoundingClientRect();
|
|
381
|
+
|
|
382
|
+
let top = 0;
|
|
383
|
+
let left = 0;
|
|
384
|
+
|
|
385
|
+
switch (this.tooltipPosition) {
|
|
386
|
+
case 'top':
|
|
387
|
+
top = hostPos.top - tooltipPos.height - 8;
|
|
388
|
+
left = hostPos.left + (hostPos.width - tooltipPos.width) / 2;
|
|
389
|
+
break;
|
|
390
|
+
case 'bottom':
|
|
391
|
+
top = hostPos.bottom + 8;
|
|
392
|
+
left = hostPos.left + (hostPos.width - tooltipPos.width) / 2;
|
|
393
|
+
break;
|
|
394
|
+
case 'left':
|
|
395
|
+
top = hostPos.top + (hostPos.height - tooltipPos.height) / 2;
|
|
396
|
+
left = hostPos.left - tooltipPos.width - 8;
|
|
397
|
+
break;
|
|
398
|
+
case 'right':
|
|
399
|
+
top = hostPos.top + (hostPos.height - tooltipPos.height) / 2;
|
|
400
|
+
left = hostPos.right + 8;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.renderer.setStyle(this.tooltipElement, 'top', \`\${top}px\`);
|
|
405
|
+
this.renderer.setStyle(this.tooltipElement, 'left', \`\${left}px\`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private removeTooltip(): void {
|
|
409
|
+
if (this.tooltipElement) {
|
|
410
|
+
this.renderer.removeChild(document.body, this.tooltipElement);
|
|
411
|
+
this.tooltipElement = null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}`;
|
|
415
|
+
|
|
416
|
+
await fs.writeFile(
|
|
417
|
+
path.join(
|
|
418
|
+
config.fullPath,
|
|
419
|
+
"src/app/shared/directives/tooltip.directive.ts",
|
|
420
|
+
),
|
|
421
|
+
tooltipDirective,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Export all functions
|
|
426
|
+
module.exports = {
|
|
427
|
+
// Modal System
|
|
428
|
+
createModalService,
|
|
429
|
+
createModalComponent,
|
|
430
|
+
|
|
431
|
+
// Pipes
|
|
432
|
+
createTruncatePipe,
|
|
433
|
+
|
|
434
|
+
// Directives
|
|
435
|
+
createClickOutsideDirective,
|
|
436
|
+
createTooltipDirective,
|
|
437
|
+
};
|