create-ng-tailwind 3.1.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of create-ng-tailwind might be problematic. Click here for more details.

Files changed (48) hide show
  1. package/CHANGELOG.md +96 -341
  2. package/README.md +111 -157
  3. package/lib/cli/index.js +74 -3
  4. package/lib/cli/interactive.js +26 -1
  5. package/lib/managers/ProjectManager.js +2 -5
  6. package/lib/templates/base/components.js +243 -0
  7. package/lib/templates/base/index.js +207 -0
  8. package/lib/templates/base/infrastructure.js +314 -0
  9. package/lib/templates/base/linting.js +359 -0
  10. package/lib/templates/base/pwa.js +103 -0
  11. package/lib/templates/base/services.js +362 -0
  12. package/lib/templates/blog/app.js +250 -0
  13. package/lib/templates/blog/components.js +360 -0
  14. package/lib/templates/blog/i18n.js +77 -0
  15. package/lib/templates/blog/index.js +126 -0
  16. package/lib/templates/blog/pages.js +554 -0
  17. package/lib/templates/blog/services.js +390 -0
  18. package/lib/templates/dashboard/app.js +320 -0
  19. package/lib/templates/dashboard/charts.js +305 -0
  20. package/lib/templates/dashboard/components.js +410 -0
  21. package/lib/templates/dashboard/i18n.js +340 -0
  22. package/lib/templates/dashboard/index.js +141 -0
  23. package/lib/templates/dashboard/layout.js +310 -0
  24. package/lib/templates/dashboard/pages.js +681 -0
  25. package/lib/templates/ecommerce/app.js +315 -0
  26. package/lib/templates/ecommerce/components.js +496 -0
  27. package/lib/templates/ecommerce/i18n.js +389 -0
  28. package/lib/templates/ecommerce/index.js +152 -0
  29. package/lib/templates/ecommerce/layout.js +270 -0
  30. package/lib/templates/ecommerce/pages.js +969 -0
  31. package/lib/templates/ecommerce/services.js +300 -0
  32. package/lib/templates/index.js +12 -0
  33. package/lib/templates/landing/index.js +1117 -0
  34. package/lib/templates/portfolio/index.js +1160 -0
  35. package/lib/templates/saas/index.js +1371 -0
  36. package/lib/templates/starter/app.js +364 -0
  37. package/lib/templates/starter/i18n.js +856 -0
  38. package/lib/templates/starter/index.js +52 -4060
  39. package/lib/templates/starter/layout.js +852 -0
  40. package/lib/templates/starter/pages.js +1241 -0
  41. package/lib/utils/nodeCompat.js +85 -0
  42. package/package.json +1 -1
  43. package/lib/templates/starter/features.js +0 -867
  44. package/lib/utils/ai-config.js +0 -641
  45. /package/lib/templates/{starter → base}/advanced-features.js +0 -0
  46. /package/lib/templates/{starter → base}/seo-assets.js +0 -0
  47. /package/lib/templates/{starter → base}/seo-features.js +0 -0
  48. /package/lib/templates/{starter → base}/ui-features.js +0 -0
@@ -0,0 +1,410 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Create dashboard shared components (StatsCard, Breadcrumb, DataTable)
6
+ */
7
+ async function createComponents(config) {
8
+ // Stats Card Component
9
+ const statsCardComponent = `import { Component, Input } from '@angular/core';
10
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
11
+ import {
12
+ heroArrowTrendingUp,
13
+ heroArrowTrendingDown,
14
+ } from '@ng-icons/heroicons/outline';
15
+
16
+ @Component({
17
+ selector: 'app-stats-card',
18
+ standalone: true,
19
+ imports: [NgIconComponent],
20
+ viewProviders: [provideIcons({ heroArrowTrendingUp, heroArrowTrendingDown })],
21
+ template: \`
22
+ <div class="group relative overflow-hidden rounded-2xl border border-gray-100 bg-white p-6 shadow-sm transition-all duration-300 hover:shadow-xl hover:shadow-gray-200/50 hover:-translate-y-1">
23
+ <div class="absolute inset-0 bg-linear-to-br opacity-0 transition-opacity group-hover:opacity-100" [class]="gradientClass"></div>
24
+ <div class="relative">
25
+ <div class="flex items-center justify-between">
26
+ <div>
27
+ <p class="text-sm font-medium text-gray-500">{{ label }}</p>
28
+ <p class="mt-2 text-3xl font-bold text-gray-900">{{ value }}</p>
29
+ </div>
30
+ @if (icon) {
31
+ <div
32
+ class="flex h-14 w-14 items-center justify-center rounded-2xl shadow-lg transition-transform group-hover:scale-110"
33
+ [class]="iconBgClass">
34
+ <ng-icon [name]="icon" size="28" [style.color]="iconColor"></ng-icon>
35
+ </div>
36
+ }
37
+ </div>
38
+ @if (trend !== undefined) {
39
+ <div class="mt-4 flex items-center">
40
+ <div class="flex items-center rounded-full px-2 py-1" [class]="trend >= 0 ? 'bg-emerald-50' : 'bg-red-50'">
41
+ <ng-icon
42
+ [name]="trend >= 0 ? 'heroArrowTrendingUp' : 'heroArrowTrendingDown'"
43
+ size="14"
44
+ [class]="trend >= 0 ? 'text-emerald-500' : 'text-red-500'">
45
+ </ng-icon>
46
+ <span
47
+ class="ms-1 text-xs font-semibold"
48
+ [class]="trend >= 0 ? 'text-emerald-600' : 'text-red-600'">
49
+ {{ trend >= 0 ? '+' : '' }}{{ trend }}%
50
+ </span>
51
+ </div>
52
+ <span class="ms-2 text-xs text-gray-500">vs last month</span>
53
+ </div>
54
+ }
55
+ </div>
56
+ </div>
57
+ \`,
58
+ })
59
+ export class StatsCardComponent {
60
+ @Input() label = '';
61
+ @Input() value: string | number = '';
62
+ @Input() icon?: string;
63
+ @Input() iconBgClass = 'bg-linear-to-br from-indigo-500 to-purple-600';
64
+ @Input() iconColor = 'white';
65
+ @Input() trend?: number;
66
+ @Input() gradientClass = 'from-indigo-50/50 to-purple-50/50';
67
+ }`;
68
+
69
+ await fs.writeFile(
70
+ path.join(config.fullPath, 'src/app/shared/components/stats-card/stats-card.component.ts'),
71
+ statsCardComponent
72
+ );
73
+
74
+ // Breadcrumb Component
75
+ const breadcrumbComponent = `import { Component, Input } from '@angular/core';
76
+ import { RouterModule } from '@angular/router';
77
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
78
+ import { heroChevronRight, heroHome } from '@ng-icons/heroicons/outline';
79
+
80
+ export interface BreadcrumbItem {
81
+ label: string;
82
+ route?: string;
83
+ }
84
+
85
+ @Component({
86
+ selector: 'app-breadcrumb',
87
+ standalone: true,
88
+ imports: [RouterModule, NgIconComponent],
89
+ viewProviders: [provideIcons({ heroChevronRight, heroHome })],
90
+ template: \`
91
+ <nav class="flex items-center gap-2 text-sm">
92
+ <a routerLink="/dashboard" class="text-gray-500 hover:text-primary-600 transition-colors">
93
+ <ng-icon name="heroHome" size="16"></ng-icon>
94
+ </a>
95
+ @for (item of items; track item.label; let last = $last) {
96
+ <ng-icon name="heroChevronRight" size="14" class="text-gray-400"></ng-icon>
97
+ @if (item.route && !last) {
98
+ <a [routerLink]="item.route" class="text-gray-500 hover:text-primary-600 transition-colors">
99
+ {{ item.label }}
100
+ </a>
101
+ } @else {
102
+ <span class="font-medium text-gray-900">{{ item.label }}</span>
103
+ }
104
+ }
105
+ </nav>
106
+ \`,
107
+ })
108
+ export class BreadcrumbComponent {
109
+ @Input() items: BreadcrumbItem[] = [];
110
+ }`;
111
+
112
+ await fs.writeFile(
113
+ path.join(config.fullPath, 'src/app/shared/components/breadcrumb/breadcrumb.component.ts'),
114
+ breadcrumbComponent
115
+ );
116
+
117
+ // Data Table Component
118
+ const dataTableComponent = `import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
119
+ import { DecimalPipe, DatePipe } from '@angular/common';
120
+ import { NgIconComponent, provideIcons } from '@ng-icons/core';
121
+ import {
122
+ heroChevronUp,
123
+ heroChevronDown,
124
+ heroChevronLeft,
125
+ heroChevronRight,
126
+ heroMagnifyingGlass,
127
+ } from '@ng-icons/heroicons/outline';
128
+ import { FormsModule } from '@angular/forms';
129
+
130
+ export interface TableColumn {
131
+ key: string;
132
+ label: string;
133
+ sortable?: boolean;
134
+ type?: 'text' | 'number' | 'date' | 'status' | 'actions';
135
+ width?: string;
136
+ }
137
+
138
+ export interface TableAction {
139
+ label: string;
140
+ icon?: string;
141
+ handler: (row: Record<string, unknown>) => void;
142
+ }
143
+
144
+ @Component({
145
+ selector: 'app-data-table',
146
+ standalone: true,
147
+ imports: [NgIconComponent, FormsModule, DecimalPipe, DatePipe],
148
+ viewProviders: [
149
+ provideIcons({
150
+ heroChevronUp,
151
+ heroChevronDown,
152
+ heroChevronLeft,
153
+ heroChevronRight,
154
+ heroMagnifyingGlass,
155
+ }),
156
+ ],
157
+ template: \`
158
+ <div class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
159
+ <!-- Search and filters -->
160
+ @if (searchable) {
161
+ <div class="border-b border-gray-200 px-6 py-4">
162
+ <div class="relative max-w-md">
163
+ <ng-icon
164
+ name="heroMagnifyingGlass"
165
+ size="18"
166
+ class="absolute top-1/2 -translate-y-1/2 text-gray-400"
167
+ style="inset-inline-start: 12px;">
168
+ </ng-icon>
169
+ <input
170
+ type="text"
171
+ [(ngModel)]="searchTerm"
172
+ (ngModelChange)="onSearch()"
173
+ placeholder="Search..."
174
+ class="w-full rounded-lg border border-gray-300 py-2 ps-10 pe-4 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
175
+ />
176
+ </div>
177
+ </div>
178
+ }
179
+
180
+ <!-- Table -->
181
+ <div class="overflow-x-auto">
182
+ <table class="w-full">
183
+ <thead>
184
+ <tr class="border-b border-gray-200 bg-gray-50">
185
+ @for (column of columns; track column.key) {
186
+ <th
187
+ class="px-6 py-3 text-start text-xs font-semibold uppercase tracking-wider text-gray-500"
188
+ [style.width]="column.width">
189
+ @if (column.sortable) {
190
+ <button
191
+ (click)="sort(column.key)"
192
+ class="flex items-center gap-1 hover:text-gray-900">
193
+ {{ column.label }}
194
+ @if (sortKey === column.key) {
195
+ <ng-icon
196
+ [name]="sortDirection === 'asc' ? 'heroChevronUp' : 'heroChevronDown'"
197
+ size="14">
198
+ </ng-icon>
199
+ }
200
+ </button>
201
+ } @else {
202
+ {{ column.label }}
203
+ }
204
+ </th>
205
+ }
206
+ </tr>
207
+ </thead>
208
+ <tbody class="divide-y divide-gray-200">
209
+ @for (row of paginatedData; track row['id'] || $index) {
210
+ <tr class="transition-colors hover:bg-gray-50">
211
+ @for (column of columns; track column.key) {
212
+ <td class="whitespace-nowrap px-6 py-4 text-sm text-gray-900">
213
+ @switch (column.type) {
214
+ @case ('status') {
215
+ <span
216
+ class="inline-flex rounded-full px-2 py-1 text-xs font-medium"
217
+ [class]="getStatusClass(row[column.key])">
218
+ {{ row[column.key] }}
219
+ </span>
220
+ }
221
+ @case ('date') {
222
+ {{ $any(row[column.key]) | date:'mediumDate' }}
223
+ }
224
+ @case ('number') {
225
+ {{ $any(row[column.key]) | number }}
226
+ }
227
+ @default {
228
+ {{ row[column.key] }}
229
+ }
230
+ }
231
+ </td>
232
+ }
233
+ </tr>
234
+ } @empty {
235
+ <tr>
236
+ <td [attr.colspan]="columns.length" class="px-6 py-12 text-center text-gray-500">
237
+ No data available
238
+ </td>
239
+ </tr>
240
+ }
241
+ </tbody>
242
+ </table>
243
+ </div>
244
+
245
+ <!-- Pagination -->
246
+ @if (paginate && filteredData.length > pageSize) {
247
+ <div class="flex items-center justify-between border-t border-gray-200 px-6 py-4">
248
+ <span class="text-sm text-gray-500">
249
+ Showing {{ startIndex + 1 }} to {{ endIndex }} of {{ filteredData.length }} results
250
+ </span>
251
+ <div class="flex items-center gap-2">
252
+ <button
253
+ (click)="prevPage()"
254
+ [disabled]="currentPage === 1"
255
+ class="rounded-lg border border-gray-300 p-2 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50">
256
+ <ng-icon name="heroChevronLeft" size="16"></ng-icon>
257
+ </button>
258
+ @for (page of visiblePages; track page) {
259
+ <button
260
+ (click)="goToPage(page)"
261
+ class="rounded-lg border px-3 py-1 text-sm transition-colors"
262
+ [class]="currentPage === page ? 'border-primary-500 bg-primary-50 text-primary-600' : 'border-gray-300 hover:bg-gray-50'">
263
+ {{ page }}
264
+ </button>
265
+ }
266
+ <button
267
+ (click)="nextPage()"
268
+ [disabled]="currentPage === totalPages"
269
+ class="rounded-lg border border-gray-300 p-2 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50">
270
+ <ng-icon name="heroChevronRight" size="16"></ng-icon>
271
+ </button>
272
+ </div>
273
+ </div>
274
+ }
275
+ </div>
276
+ \`,
277
+ })
278
+ export class DataTableComponent implements OnChanges {
279
+ @Input() data: Record<string, unknown>[] = [];
280
+ @Input() columns: TableColumn[] = [];
281
+ @Input() searchable = true;
282
+ @Input() paginate = true;
283
+ @Input() pageSize = 10;
284
+ @Output() rowClick = new EventEmitter<Record<string, unknown>>();
285
+
286
+ searchTerm = '';
287
+ sortKey = '';
288
+ sortDirection: 'asc' | 'desc' = 'asc';
289
+ currentPage = 1;
290
+
291
+ filteredData: Record<string, unknown>[] = [];
292
+ paginatedData: Record<string, unknown>[] = [];
293
+
294
+ ngOnChanges(changes: SimpleChanges): void {
295
+ if (changes['data']) {
296
+ this.applyFilters();
297
+ }
298
+ }
299
+
300
+ onSearch(): void {
301
+ this.currentPage = 1;
302
+ this.applyFilters();
303
+ }
304
+
305
+ sort(key: string): void {
306
+ if (this.sortKey === key) {
307
+ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
308
+ } else {
309
+ this.sortKey = key;
310
+ this.sortDirection = 'asc';
311
+ }
312
+ this.applyFilters();
313
+ }
314
+
315
+ private applyFilters(): void {
316
+ let result = [...this.data];
317
+
318
+ // Search
319
+ if (this.searchTerm) {
320
+ const term = this.searchTerm.toLowerCase();
321
+ result = result.filter((row) =>
322
+ Object.values(row).some((value) =>
323
+ String(value).toLowerCase().includes(term)
324
+ )
325
+ );
326
+ }
327
+
328
+ // Sort
329
+ if (this.sortKey) {
330
+ result.sort((a, b) => {
331
+ const aVal = a[this.sortKey];
332
+ const bVal = b[this.sortKey];
333
+ const comparison = String(aVal).localeCompare(String(bVal));
334
+ return this.sortDirection === 'asc' ? comparison : -comparison;
335
+ });
336
+ }
337
+
338
+ this.filteredData = result;
339
+ this.updatePagination();
340
+ }
341
+
342
+ private updatePagination(): void {
343
+ const start = (this.currentPage - 1) * this.pageSize;
344
+ const end = start + this.pageSize;
345
+ this.paginatedData = this.filteredData.slice(start, end);
346
+ }
347
+
348
+ get startIndex(): number {
349
+ return (this.currentPage - 1) * this.pageSize;
350
+ }
351
+
352
+ get endIndex(): number {
353
+ return Math.min(this.currentPage * this.pageSize, this.filteredData.length);
354
+ }
355
+
356
+ get totalPages(): number {
357
+ return Math.ceil(this.filteredData.length / this.pageSize);
358
+ }
359
+
360
+ get visiblePages(): number[] {
361
+ const pages: number[] = [];
362
+ const start = Math.max(1, this.currentPage - 2);
363
+ const end = Math.min(this.totalPages, start + 4);
364
+ for (let i = start; i <= end; i++) {
365
+ pages.push(i);
366
+ }
367
+ return pages;
368
+ }
369
+
370
+ goToPage(page: number): void {
371
+ this.currentPage = page;
372
+ this.updatePagination();
373
+ }
374
+
375
+ prevPage(): void {
376
+ if (this.currentPage > 1) {
377
+ this.currentPage--;
378
+ this.updatePagination();
379
+ }
380
+ }
381
+
382
+ nextPage(): void {
383
+ if (this.currentPage < this.totalPages) {
384
+ this.currentPage++;
385
+ this.updatePagination();
386
+ }
387
+ }
388
+
389
+ getStatusClass(status: unknown): string {
390
+ const statusStr = String(status).toLowerCase();
391
+ const classes: Record<string, string> = {
392
+ active: 'bg-success-100 text-success-700',
393
+ completed: 'bg-success-100 text-success-700',
394
+ pending: 'bg-warning-100 text-warning-700',
395
+ processing: 'bg-info-100 text-info-700',
396
+ inactive: 'bg-gray-100 text-gray-700',
397
+ cancelled: 'bg-danger-100 text-danger-700',
398
+ failed: 'bg-danger-100 text-danger-700',
399
+ };
400
+ return classes[statusStr] || 'bg-gray-100 text-gray-700';
401
+ }
402
+ }`;
403
+
404
+ await fs.writeFile(
405
+ path.join(config.fullPath, 'src/app/shared/components/data-table/data-table.component.ts'),
406
+ dataTableComponent
407
+ );
408
+ }
409
+
410
+ module.exports = { createComponents };