@devstroupe/devkit-cli 1.1.8 → 1.1.9

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.
@@ -80,16 +80,25 @@ function nestMigrationTemplate(entity, entities, timestamp, properties = entity.
80
80
  const relationColumns = relationships
81
81
  .filter((relationship) => (0, relationships_1.isSingularRelationship)(relationship.kind))
82
82
  .map((relationship) => ` { name: '${relationship.foreignKey}', type: 'int', isNullable: ${!relationship.property.required}, isUnique: ${relationship.kind === 'one-to-one'} }`);
83
- const foreignKeys = relationships
83
+ // Gera SQL idempotente para cada FK: DROP IF EXISTS + ADD CONSTRAINT
84
+ const foreignKeysSql = relationships
84
85
  .filter((relationship) => (0, relationships_1.isSingularRelationship)(relationship.kind))
85
- .map((relationship) => ` new TableForeignKey({
86
- name: 'FK_${tableName}_${relationship.foreignKey}',
87
- columnNames: ['${relationship.foreignKey}'],
88
- referencedTableName: '${targetTable(relationship.target, entities)}',
89
- referencedColumnNames: ['${relationship.valueField}'],
90
- onDelete: '${relationship.onDelete}'
91
- })`)
92
- .join(',\n');
86
+ .map((relationship) => {
87
+ const fkName = `FK_${tableName}_${relationship.foreignKey}`;
88
+ const refTable = targetTable(relationship.target, entities);
89
+ return [
90
+ ` await queryRunner.query(\`ALTER TABLE \\\`${tableName}\\\` DROP FOREIGN KEY IF EXISTS \\\`${fkName}\\\`\`);`,
91
+ ` await queryRunner.query(\`ALTER TABLE \\\`${tableName}\\\` ADD CONSTRAINT \\\`${fkName}\\\` FOREIGN KEY (\\\`${relationship.foreignKey}\\\`) REFERENCES \\\`${refTable}\\\`(\\\`${relationship.valueField}\\\`) ON DELETE ${relationship.onDelete}\`);`,
92
+ ].join('\n');
93
+ })
94
+ .join('\n');
95
+ const dropForeignKeysSql = relationships
96
+ .filter((relationship) => (0, relationships_1.isSingularRelationship)(relationship.kind))
97
+ .map((relationship) => {
98
+ const fkName = `FK_${tableName}_${relationship.foreignKey}`;
99
+ return ` await queryRunner.query(\`ALTER TABLE \\\`${tableName}\\\` DROP FOREIGN KEY IF EXISTS \\\`${fkName}\\\`\`);`;
100
+ })
101
+ .join('\n');
93
102
  const junctions = relationships
94
103
  .filter((relationship) => relationship.kind === 'many-to-many' && relationship.owner)
95
104
  .map((relationship) => {
@@ -154,7 +163,7 @@ function nestMigrationTemplate(entity, entities, timestamp, properties = entity.
154
163
  return ` await queryRunner.query('ALTER TABLE \`${tableName}\` DROP CONSTRAINT \`${checkName}\`');`;
155
164
  }).join('\n')
156
165
  : '';
157
- return `import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm';
166
+ return `import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
158
167
 
159
168
  export class ${className} implements MigrationInterface {
160
169
  name = '${className}';
@@ -166,10 +175,7 @@ export class ${className} implements MigrationInterface {
166
175
  { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment' }${scalarColumns.length + relationColumns.length > 0 ? ',\n' : ''}${[...scalarColumns, ...relationColumns].join(',\n')}
167
176
  ]
168
177
  }), true);
169
- ${foreignKeys ? `
170
- await queryRunner.createForeignKeys('${tableName}', [
171
- ${foreignKeys}
172
- ]);` : ''}
178
+ ${foreignKeysSql ? `\n${foreignKeysSql}` : ''}
173
179
  ${tableIndexes ? `\n${tableIndexes}` : ''}
174
180
  ${checkConstraints ? `\n${checkConstraints}` : ''}
175
181
  ${junctions.map((junction) => `
@@ -177,7 +183,7 @@ ${junction.up}`).join('\n')}
177
183
  }
178
184
 
179
185
  async down(queryRunner: QueryRunner): Promise<void> {
180
- ${dropChecks ? `${dropChecks}\n` : ''}${dropIndexes ? `${dropIndexes}\n` : ''}${junctions.slice().reverse().map((junction) => ` await queryRunner.dropTable('${junction.tableName}', true);`).join('\n')}${junctions.length > 0 ? '\n' : ''} await queryRunner.dropTable('${tableName}', true);
186
+ ${dropChecks ? `${dropChecks}\n` : ''}${dropIndexes ? `${dropIndexes}\n` : ''}${dropForeignKeysSql ? `${dropForeignKeysSql}\n` : ''}${junctions.slice().reverse().map((junction) => ` await queryRunner.dropTable('${junction.tableName}', true);`).join('\n')}${junctions.length > 0 ? '\n' : ''} await queryRunner.dropTable('${tableName}', true);
181
187
  }
182
188
  }
183
189
  `;
@@ -149,8 +149,9 @@ function assertValidTypeScript(source) {
149
149
  strict_1.default.match(companyMigration, /name: 'companies'/);
150
150
  strict_1.default.doesNotMatch(companyMigration, /FK_companies_cabins/);
151
151
  strict_1.default.match(cabinMigration, /name: 'company_id', type: 'int'/);
152
- strict_1.default.match(cabinMigration, /referencedTableName: 'companies'/);
153
- strict_1.default.match(cabinMigration, /onDelete: 'CASCADE'/);
152
+ strict_1.default.match(cabinMigration, /ADD CONSTRAINT/);
153
+ strict_1.default.match(cabinMigration, /FK_cabins_company_id/);
154
+ strict_1.default.match(cabinMigration, /ON DELETE CASCADE/);
154
155
  strict_1.default.match(dataSource, /migrations: \[path\.join/);
155
156
  strict_1.default.match(dataSource, /synchronize: false/);
156
157
  assertValidTypeScript(companyMigration);
@@ -8,11 +8,12 @@ import { CommonModule } from '@angular/common';
8
8
  import { HlmButton } from '@spartan-ng/helm/button';
9
9
  import { HlmInput } from '@spartan-ng/helm/input';
10
10
  import { NgIconComponent } from '@ng-icons/core';
11
+ import type { ThemeMode } from '../../../core/services/theme.service';
11
12
 
12
13
  export interface IUserProfile {
13
14
  name: string;
14
15
  avatar?: string;
15
- role?: string;
16
+ role?: any; // pode ser string ou { name: string, permissions: string[] }
16
17
  }
17
18
 
18
19
  @Component({
@@ -28,16 +29,35 @@ export class DevkitHeaderComponent {
28
29
  @Input() user: IUserProfile | null = null;
29
30
  @Input() isSidebarCollapsed = false;
30
31
  @Input() isDarkMode = false;
32
+ @Input() themeMode: ThemeMode = 'auto';
31
33
 
32
34
  @Output() toggleSidebar = new EventEmitter<void>();
33
35
  @Output() logout = new EventEmitter<void>();
34
36
  @Output() toggleTheme = new EventEmitter<void>();
35
37
  @Output() openProfile = new EventEmitter<void>();
38
+ @Output() setThemeMode = new EventEmitter<ThemeMode>();
36
39
 
37
40
  isProfileDropdownOpen = false;
41
+ isThemeDropdownOpen = false;
42
+
43
+ get roleName(): string {
44
+ if (!this.user?.role) return '';
45
+ return typeof this.user.role === 'object' ? (this.user.role.name ?? '') : this.user.role;
46
+ }
38
47
 
39
48
  toggleProfileDropdown(): void {
40
49
  this.isProfileDropdownOpen = !this.isProfileDropdownOpen;
50
+ this.isThemeDropdownOpen = false;
51
+ }
52
+
53
+ toggleThemeDropdown(): void {
54
+ this.isThemeDropdownOpen = !this.isThemeDropdownOpen;
55
+ this.isProfileDropdownOpen = false;
56
+ }
57
+
58
+ onSelectTheme(mode: ThemeMode): void {
59
+ this.isThemeDropdownOpen = false;
60
+ this.setThemeMode.emit(mode);
41
61
  }
42
62
 
43
63
  onLogoutClick(): void {
@@ -53,6 +73,18 @@ export class DevkitHeaderComponent {
53
73
  onToggleTheme(): void {
54
74
  this.toggleTheme.emit();
55
75
  }
76
+
77
+ get themeIcon(): string {
78
+ if (this.themeMode === 'light') return 'lucideSun';
79
+ if (this.themeMode === 'dark') return 'lucideMoon';
80
+ return 'lucideMonitor';
81
+ }
82
+
83
+ get themeLabel(): string {
84
+ if (this.themeMode === 'light') return 'Light';
85
+ if (this.themeMode === 'dark') return 'Dark';
86
+ return 'Auto';
87
+ }
56
88
  }
57
89
  `,
58
90
  html: `<header class="dt-header">
@@ -70,18 +102,35 @@ export class DevkitHeaderComponent {
70
102
 
71
103
  <div class="flex items-center space-x-3">
72
104
 
73
- <!-- Toggle Dark / Light Mode -->
74
- <button
75
- hlmBtn
76
- variant="ghost"
77
- size="icon"
78
- class="dt-header-action"
79
- [title]="isDarkMode ? 'Mudar para modo claro' : 'Mudar para modo escuro'"
80
- (click)="onToggleTheme()"
81
- >
82
- <ng-icon *ngIf="isDarkMode" name="lucideSun" class="size-5"></ng-icon>
83
- <ng-icon *ngIf="!isDarkMode" name="lucideMoon" class="size-5"></ng-icon>
84
- </button>
105
+ <!-- Seletor de Tema (Light / Dark / Auto) -->
106
+ <div class="relative">
107
+ <button
108
+ hlmBtn
109
+ variant="ghost"
110
+ size="icon"
111
+ class="dt-header-action"
112
+ [title]="'Tema: ' + themeLabel"
113
+ (click)="toggleThemeDropdown()"
114
+ >
115
+ <ng-icon [name]="themeIcon" class="size-5"></ng-icon>
116
+ </button>
117
+
118
+ <!-- Dropdown de Tema -->
119
+ <div *ngIf="isThemeDropdownOpen" class="dt-theme-dropdown">
120
+ <button class="dt-theme-option" [class.dt-theme-option--active]="themeMode === 'light'" (click)="onSelectTheme('light')">
121
+ <ng-icon name="lucideSun" class="size-4 mr-2"></ng-icon>
122
+ Light
123
+ </button>
124
+ <button class="dt-theme-option" [class.dt-theme-option--active]="themeMode === 'dark'" (click)="onSelectTheme('dark')">
125
+ <ng-icon name="lucideMoon" class="size-4 mr-2"></ng-icon>
126
+ Dark
127
+ </button>
128
+ <button class="dt-theme-option" [class.dt-theme-option--active]="themeMode === 'auto'" (click)="onSelectTheme('auto')">
129
+ <ng-icon name="lucideMonitor" class="size-4 mr-2"></ng-icon>
130
+ Auto
131
+ </button>
132
+ </div>
133
+ </div>
85
134
 
86
135
  <!-- Notificações -->
87
136
  <button *ngIf="showNotifications" hlmBtn variant="ghost" size="icon" class="dt-header-action relative" title="Notificações">
@@ -103,7 +152,7 @@ export class DevkitHeaderComponent {
103
152
  </ng-template>
104
153
  <div class="hidden md:flex flex-col text-left">
105
154
  <span class="text-sm font-semibold leading-none text-foreground">{{ user.name }}</span>
106
- <span *ngIf="user.role" class="text-[10px] font-medium leading-none mt-1 text-muted-foreground">{{ user.role }}</span>
155
+ <span *ngIf="roleName" class="text-[10px] font-medium leading-none mt-1 text-muted-foreground">{{ roleName }}</span>
107
156
  </div>
108
157
  </button>
109
158
 
@@ -111,7 +160,7 @@ export class DevkitHeaderComponent {
111
160
  <div *ngIf="isProfileDropdownOpen" class="dt-header-dropdown">
112
161
  <div class="px-4 py-3 border-b border-border">
113
162
  <p class="text-sm font-bold text-foreground">{{ user.name }}</p>
114
- <p *ngIf="user.role" class="text-xs text-muted-foreground mt-0.5">{{ user.role }}</p>
163
+ <p *ngIf="roleName" class="text-xs text-muted-foreground mt-0.5">{{ roleName }}</p>
115
164
  </div>
116
165
  <button hlmBtn variant="ghost" (click)="onOpenProfile()" class="dt-header-dropdown-item">
117
166
  <ng-icon name="lucideUser" class="size-4 mr-2"></ng-icon>
@@ -180,6 +229,46 @@ export class DevkitHeaderComponent {
180
229
  width: 280px;
181
230
  }
182
231
 
232
+ .dt-theme-dropdown {
233
+ position: absolute;
234
+ right: 0;
235
+ top: calc(100% + 8px);
236
+ width: 140px;
237
+ background-color: var(--popover);
238
+ color: var(--popover-foreground);
239
+ border: 1px solid var(--border);
240
+ border-radius: var(--radius);
241
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
242
+ z-index: 100;
243
+ display: flex;
244
+ flex-direction: column;
245
+ padding: 4px 0;
246
+ font-family: var(--font-family);
247
+ }
248
+
249
+ .dt-theme-option {
250
+ display: flex;
251
+ align-items: center;
252
+ width: 100%;
253
+ padding: 9px 14px;
254
+ background: transparent;
255
+ border: none;
256
+ font-size: 13px;
257
+ color: var(--popover-foreground);
258
+ cursor: pointer;
259
+ font-family: var(--font-family);
260
+ transition: background-color 0.15s ease-in-out;
261
+ }
262
+
263
+ .dt-theme-option:hover {
264
+ background-color: var(--muted);
265
+ }
266
+
267
+ .dt-theme-option--active {
268
+ color: var(--primary);
269
+ font-weight: 600;
270
+ }
271
+
183
272
  .dt-header-dropdown {
184
273
  position: absolute;
185
274
  right: 0;
@@ -32,6 +32,7 @@ export class MainLayoutComponent implements OnInit {
32
32
  menuItems: INavigationItem[] = APP_NAVIGATION;
33
33
  currentUser$ = this.authService.currentUser$;
34
34
  isDark$ = this.themeService.isDark$;
35
+ themeMode$ = this.themeService.themeMode$;
35
36
 
36
37
  brandLogo = '${brandLogo}';
37
38
  brandLogoCollapsed = '${brandLogoCollapsed}';
@@ -53,6 +54,10 @@ export class MainLayoutComponent implements OnInit {
53
54
  this.themeService.toggle();
54
55
  }
55
56
 
57
+ onSetThemeMode(mode: 'light' | 'dark' | 'auto'): void {
58
+ this.themeService.setMode(mode);
59
+ }
60
+
56
61
  onOpenProfile(): void {
57
62
  this.router.navigate(['/profile']);
58
63
  }
@@ -87,9 +92,11 @@ export class MainLayoutComponent implements OnInit {
87
92
  [user]="currentUser$ | async"
88
93
  [isSidebarCollapsed]="isCollapsed"
89
94
  [isDarkMode]="(isDark$ | async) ?? false"
95
+ [themeMode]="(themeMode$ | async) ?? 'auto'"
90
96
  (toggleSidebar)="isCollapsed = !isCollapsed"
91
97
  (logout)="onLogout()"
92
98
  (toggleTheme)="onToggleTheme()"
99
+ (setThemeMode)="onSetThemeMode($event)"
93
100
  (openProfile)="onOpenProfile()"
94
101
  ></devkit-header>
95
102
 
@@ -5,42 +5,57 @@ function devkitThemeServiceTemplate() {
5
5
  return `import { Injectable } from '@angular/core';
6
6
  import { BehaviorSubject } from 'rxjs';
7
7
 
8
+ export type ThemeMode = 'light' | 'dark' | 'auto';
9
+
8
10
  const THEME_KEY = 'devkit-theme';
9
11
 
10
12
  @Injectable({
11
13
  providedIn: 'root'
12
14
  })
13
15
  export class ThemeService {
16
+ private modeSubject = new BehaviorSubject<ThemeMode>('auto');
14
17
  private isDarkSubject = new BehaviorSubject<boolean>(false);
18
+
19
+ themeMode$ = this.modeSubject.asObservable();
15
20
  isDark$ = this.isDarkSubject.asObservable();
16
21
 
17
- get isDark(): boolean {
18
- return this.isDarkSubject.getValue();
19
- }
22
+ get isDark(): boolean { return this.isDarkSubject.getValue(); }
23
+ get mode(): ThemeMode { return this.modeSubject.getValue(); }
20
24
 
21
25
  constructor() {
22
26
  this.loadTheme();
27
+ window.matchMedia?.('(prefers-color-scheme: dark)')
28
+ .addEventListener('change', () => {
29
+ if (this.mode === 'auto') this.applyMode('auto');
30
+ });
23
31
  }
24
32
 
33
+ /** Cicla entre light → dark → auto */
25
34
  toggle(): void {
26
- this.setDark(!this.isDark);
35
+ const next: ThemeMode = this.mode === 'light' ? 'dark' : this.mode === 'dark' ? 'auto' : 'light';
36
+ this.setMode(next);
27
37
  }
28
38
 
29
- setDark(dark: boolean): void {
39
+ setMode(mode: ThemeMode): void {
40
+ this.modeSubject.next(mode);
41
+ localStorage.setItem(THEME_KEY, mode);
42
+ this.applyMode(mode);
43
+ }
44
+
45
+ /** @deprecated use setMode */
46
+ setDark(dark: boolean): void { this.setMode(dark ? 'dark' : 'light'); }
47
+
48
+ private applyMode(mode: ThemeMode): void {
49
+ const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
50
+ const dark = mode === 'dark' || (mode === 'auto' && prefersDark);
30
51
  this.isDarkSubject.next(dark);
31
- localStorage.setItem(THEME_KEY, dark ? 'dark' : 'light');
32
- if (dark) {
33
- document.documentElement.classList.add('dark');
34
- } else {
35
- document.documentElement.classList.remove('dark');
36
- }
52
+ document.documentElement.classList.toggle('dark', dark);
37
53
  }
38
54
 
39
55
  private loadTheme(): void {
40
- const saved = localStorage.getItem(THEME_KEY);
41
- const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
42
- const dark = saved ? saved === 'dark' : prefersDark;
43
- this.setDark(dark);
56
+ const saved = (localStorage.getItem(THEME_KEY) ?? 'auto') as ThemeMode;
57
+ this.modeSubject.next(saved);
58
+ this.applyMode(saved);
44
59
  }
45
60
  }
46
61
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devstroupe/devkit-cli",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "DevsTroupe Development Kit CLI — scaffold NestJS+Angular projects, inject Spartan UI, generate CRUDs and audit architectural governance",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "commander": "^12.0.0",
38
38
  "fs-extra": "^11.2.0",
39
- "@devstroupe/devkit-core": "1.1.8"
39
+ "@devstroupe/devkit-core": "1.1.9"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/fs-extra": "^11.0.4",