generate-ui-cli 2.2.0 → 2.3.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.
@@ -4,16 +4,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.generateFeature = generateFeature;
7
+ exports.generateAdminFeature = generateAdminFeature;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
9
- function generateFeature(schema, root, schemasRoot) {
10
+ function generateFeature(schema, featuresRoot, generatedRoot, schemasRoot) {
10
11
  const rawName = schema.api.operationId;
11
12
  const name = toPascalCase(rawName);
12
13
  const folder = toFolderName(name);
13
14
  const fileBase = toFileBase(name);
14
- const featureDir = path_1.default.join(root, folder);
15
+ const featureDir = path_1.default.join(generatedRoot, folder);
15
16
  fs_1.default.mkdirSync(featureDir, { recursive: true });
16
- const appRoot = path_1.default.resolve(root, '..');
17
+ const appRoot = path_1.default.resolve(featuresRoot, '..');
17
18
  ensureUiComponents(appRoot, schemasRoot);
18
19
  const method = String(schema.api.method || '').toLowerCase();
19
20
  const endpoint = String(schema.api.endpoint || '');
@@ -35,6 +36,7 @@ function generateFeature(schema, root, schemasRoot) {
35
36
  }));
36
37
  const includeBody = ['post', 'put', 'patch'].includes(method);
37
38
  const includeParams = pathParams.length > 0 || queryParams.length > 0;
39
+ const shouldAutoRefresh = method === 'get' && !includeParams && !includeBody;
38
40
  const formFields = [
39
41
  ...(includeParams
40
42
  ? [...paramFields, ...normalizedQueryFields]
@@ -47,24 +49,32 @@ function generateFeature(schema, root, schemasRoot) {
47
49
  ? String(schema.entity).trim()
48
50
  : rawName;
49
51
  const subtitle = `${method.toUpperCase()} ${endpoint}`;
52
+ const responseFormat = resolveResponseFormat(schema);
50
53
  const schemaImportPath = buildSchemaImportPath(featureDir, schemasRoot, rawName);
51
54
  /**
52
55
  * 1️⃣ Component (sempre sobrescreve)
53
56
  */
54
57
  const componentPath = path_1.default.join(featureDir, `${fileBase}.component.ts`);
55
- fs_1.default.writeFileSync(componentPath, `
56
- import { Component } from '@angular/core'
58
+ const uiCardImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-card', 'ui-card.component'));
59
+ const uiButtonImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-button', 'ui-button.component'));
60
+ const uiSelectImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-select', 'ui-select.component'));
61
+ const uiCheckboxImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-checkbox', 'ui-checkbox.component'));
62
+ const uiInputImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-input', 'ui-input.component'));
63
+ const uiTextareaImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-textarea', 'ui-textarea.component'));
64
+ const generatedComponentSource = `
65
+ import { Component, OnDestroy, OnInit, AfterViewInit } from '@angular/core'
57
66
  import { CommonModule } from '@angular/common'
58
67
  import { FormBuilder, ReactiveFormsModule } from '@angular/forms'
59
- import { UiCardComponent } from '../../ui/ui-card/ui-card.component'
60
- import { UiButtonComponent } from '../../ui/ui-button/ui-button.component'
61
- import { UiSelectComponent } from '../../ui/ui-select/ui-select.component'
62
- import { UiCheckboxComponent } from '../../ui/ui-checkbox/ui-checkbox.component'
63
- import { UiInputComponent } from '../../ui/ui-input/ui-input.component'
64
- import { UiTextareaComponent } from '../../ui/ui-textarea/ui-textarea.component'
68
+ import { UiCardComponent } from '${uiCardImport}'
69
+ import { UiButtonComponent } from '${uiButtonImport}'
70
+ import { UiSelectComponent } from '${uiSelectImport}'
71
+ import { UiCheckboxComponent } from '${uiCheckboxImport}'
72
+ import { UiInputComponent } from '${uiInputImport}'
73
+ import { UiTextareaComponent } from '${uiTextareaImport}'
65
74
  import { ${name}Service } from './${fileBase}.service.gen'
66
75
  import { ${name}Gen } from './${fileBase}.gen'
67
76
  import screenSchema from '${schemaImportPath}'
77
+ import { BehaviorSubject } from 'rxjs'
68
78
 
69
79
  @Component({
70
80
  selector: 'app-${toKebab(name)}',
@@ -82,13 +92,77 @@ import screenSchema from '${schemaImportPath}'
82
92
  templateUrl: './${fileBase}.component.html',
83
93
  styleUrls: ['./${fileBase}.component.scss']
84
94
  })
85
- export class ${name}Component extends ${name}Gen {
95
+ export class ${name}Component extends ${name}Gen implements OnInit, AfterViewInit, OnDestroy {
96
+ private readonly autoRefresh = ${shouldAutoRefresh}
97
+ private readonly allowDeepArraySearch = ${method === 'get' && includeParams && !includeBody ? 'false' : 'true'}
98
+ private readonly onFocus = () => {
99
+ if (this.autoRefresh && !this.loading) {
100
+ this.submit()
101
+ }
102
+ }
103
+ private readonly onVisibility = () => {
104
+ if (
105
+ this.autoRefresh &&
106
+ typeof document !== 'undefined' &&
107
+ document.visibilityState === 'visible'
108
+ ) {
109
+ this.submit()
110
+ }
111
+ }
112
+
86
113
  constructor(
87
114
  protected override fb: FormBuilder,
88
115
  protected override service: ${name}Service
89
116
  ) {
90
117
  super(fb, service)
91
118
  this.setSchema(screenSchema as any)
119
+ this.applyPrefill()
120
+ }
121
+
122
+ ngOnInit() {
123
+ this.ensureInitialLoad()
124
+ this.setupAutoRefreshListeners()
125
+ }
126
+
127
+ ngAfterViewInit() {
128
+ this.ensureInitialLoad()
129
+ }
130
+
131
+ ngOnDestroy() {
132
+ if (!this.autoRefresh) return
133
+ if (typeof window !== 'undefined') {
134
+ window.removeEventListener('focus', this.onFocus)
135
+ }
136
+ if (typeof document !== 'undefined') {
137
+ document.removeEventListener('visibilitychange', this.onVisibility)
138
+ }
139
+ }
140
+
141
+ private setupAutoRefreshListeners() {
142
+ if (!this.autoRefresh) return
143
+ if (typeof window !== 'undefined') {
144
+ window.addEventListener('focus', this.onFocus)
145
+ }
146
+ if (typeof document !== 'undefined') {
147
+ document.addEventListener('visibilitychange', this.onVisibility)
148
+ }
149
+ }
150
+
151
+ private ensureInitialLoad() {
152
+ if (!this.autoRefresh) return
153
+ if (this.loading || this.result$.value !== null) return
154
+ this.submit()
155
+ }
156
+
157
+ private applyPrefill() {
158
+ const state = (history as any)?.state
159
+ const prefill = state?.prefill
160
+ if (prefill) {
161
+ this.form.patchValue(prefill)
162
+ if (state?.autoSubmit) {
163
+ this.submit()
164
+ }
165
+ }
92
166
  }
93
167
 
94
168
  submit() {
@@ -98,32 +172,32 @@ export class ${name}Component extends ${name}Gen {
98
172
  const body = this.pick(value, this.bodyFieldNames)
99
173
 
100
174
  this.loading = true
101
- this.error = null
175
+ this.error$.next(null)
102
176
 
103
177
  this.service
104
178
  .execute(pathParams, queryParams, body)
105
179
  .subscribe({
106
- next: result => {
180
+ next: (result: any) => {
107
181
  const normalized =
108
182
  result && typeof result === 'object' && 'body' in result
109
183
  ? (result as any).body
110
184
  : result
111
- this.result = normalized
185
+ this.result$.next(normalized)
112
186
  this.loading = false
113
187
  },
114
- error: error => {
115
- this.error = error
188
+ error: (error: any) => {
189
+ this.error$.next(error)
116
190
  this.loading = false
117
191
  }
118
192
  })
119
193
  }
120
194
 
121
- isArrayResult() {
122
- return this.getRows().length > 0
195
+ isArrayResult(raw?: any) {
196
+ return this.getRows(raw).length > 0
123
197
  }
124
198
 
125
- getRows() {
126
- const value = this.unwrapResult(this.result)
199
+ getRows(raw?: any) {
200
+ const value = this.unwrapResult(raw ?? this.result$.value)
127
201
  if (Array.isArray(value)) return value
128
202
  if (!value || typeof value !== 'object') return []
129
203
 
@@ -132,20 +206,57 @@ export class ${name}Component extends ${name}Gen {
132
206
  if (Array.isArray(value[key])) return value[key]
133
207
  }
134
208
 
209
+ if (!this.allowDeepArraySearch) return []
135
210
  const found = this.findFirstArray(value, 0, 5)
136
211
  return found ?? []
137
212
  }
138
213
 
139
- getColumns() {
140
- const raw = this.form.get('fields')?.value
141
- if (typeof raw === 'string' && raw.trim().length > 0) {
142
- return raw
214
+ private getConfiguredColumns() {
215
+ const columns = this.schema?.data?.table?.columns
216
+ if (!Array.isArray(columns)) return []
217
+ return columns
218
+ .map((entry: any) => {
219
+ if (typeof entry === 'string') {
220
+ return { key: entry, label: '', visible: true }
221
+ }
222
+ if (entry && typeof entry === 'object') {
223
+ const key = String(
224
+ entry.key ?? entry.id ?? entry.column ?? ''
225
+ ).trim()
226
+ if (!key) return null
227
+ const label =
228
+ typeof entry.label === 'string' ? entry.label : ''
229
+ const visible = entry.visible !== false
230
+ return { key, label, visible }
231
+ }
232
+ return null
233
+ })
234
+ .filter(Boolean) as Array<{ key: string; label: string; visible: boolean }>
235
+ }
236
+
237
+ private getColumnLabel(value: string) {
238
+ const configured = this.getConfiguredColumns()
239
+ const match = configured.find(column => column.key === value)
240
+ return match?.label ?? ''
241
+ }
242
+
243
+ getColumns(value?: any) {
244
+ const configured = this.getConfiguredColumns()
245
+ if (configured.length) {
246
+ return configured
247
+ .filter(column => column.visible)
248
+ .map(column => column.key)
249
+ }
250
+
251
+ const fieldsRaw = this.form.get('fields')?.value
252
+ if (typeof fieldsRaw === 'string' && fieldsRaw.trim().length > 0) {
253
+ return fieldsRaw
143
254
  .split(',')
144
255
  .map((value: string) => value.trim())
145
256
  .filter(Boolean)
146
257
  }
147
258
 
148
- const rows = this.getRows()
259
+ const rows = this.getRows(value)
149
260
  if (rows.length > 0 && rows[0] && typeof rows[0] === 'object') {
150
261
  return Object.keys(rows[0])
151
262
  }
@@ -154,6 +265,8 @@ export class ${name}Component extends ${name}Gen {
154
265
  }
155
266
 
156
267
  formatHeader(value: string) {
268
+ const configured = this.getColumnLabel(value)
269
+ if (configured) return configured
157
270
  return value
158
271
  .replace(/[_-]/g, ' ')
159
272
  .replace(/([a-z])([A-Z])/g, '$1 $2')
@@ -205,20 +318,64 @@ export class ${name}Component extends ${name}Gen {
205
318
  return String(value)
206
319
  }
207
320
 
208
- getObjectRows() {
209
- const value = this.unwrapResult(this.result)
321
+ getCardImage(row: any) {
322
+ if (!row || typeof row !== 'object') return ''
323
+ const directKeys = ['thumbnail', 'image', 'avatar', 'photo', 'picture']
324
+ for (const key of directKeys) {
325
+ if (typeof row[key] === 'string') return row[key]
326
+ }
327
+ if (Array.isArray(row.images) && row.images.length) {
328
+ return row.images[0]
329
+ }
330
+ return ''
331
+ }
332
+
333
+ getCardTitle(row: any) {
334
+ if (!row || typeof row !== 'object') return 'Item'
335
+ return (
336
+ row.title ??
337
+ row.name ??
338
+ row.label ??
339
+ row.id ??
340
+ 'Item'
341
+ )
342
+ }
343
+
344
+ getCardSubtitle(row: any) {
345
+ if (!row || typeof row !== 'object') return ''
346
+ return (
347
+ row.description ??
348
+ row.category ??
349
+ row.brand ??
350
+ ''
351
+ )
352
+ }
353
+
354
+ formatError(error: any) {
355
+ if (!error) return ''
356
+ if (typeof error === 'string') return error
357
+ if (typeof error?.message === 'string') return error.message
358
+ try {
359
+ return JSON.stringify(error)
360
+ } catch {
361
+ return String(error)
362
+ }
363
+ }
364
+
365
+ getObjectRows(raw?: any) {
366
+ const value = this.unwrapResult(raw ?? this.result$.value)
210
367
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
211
368
  return []
212
369
  }
213
370
  return this.flattenObject(value)
214
371
  }
215
372
 
216
- hasObjectRows() {
217
- return this.getObjectRows().length > 0
373
+ hasObjectRows(raw?: any) {
374
+ return this.getObjectRows(raw).length > 0
218
375
  }
219
376
 
220
- isSingleValue() {
221
- const value = this.unwrapResult(this.result)
377
+ isSingleValue(raw?: any) {
378
+ const value = this.unwrapResult(raw ?? this.result$.value)
222
379
  return (
223
380
  value !== null &&
224
381
  value !== undefined &&
@@ -275,7 +432,8 @@ export class ${name}Component extends ${name}Gen {
275
432
  }
276
433
 
277
434
  }
278
- `);
435
+ `;
436
+ fs_1.default.writeFileSync(componentPath, generatedComponentSource);
279
437
  /**
280
438
  * 2️⃣ Arquivo gerado (sempre sobrescreve)
281
439
  */
@@ -283,6 +441,7 @@ export class ${name}Component extends ${name}Gen {
283
441
  fs_1.default.writeFileSync(genTsPath, `
284
442
  import { FormBuilder, FormGroup, Validators } from '@angular/forms'
285
443
  import { Injectable } from '@angular/core'
444
+ import { BehaviorSubject } from 'rxjs'
286
445
  import { ${name}Service } from './${fileBase}.service.gen'
287
446
 
288
447
  @Injectable()
@@ -295,8 +454,8 @@ export class ${name}Gen {
295
454
  schema: any
296
455
 
297
456
  loading = false
298
- result: any = null
299
- error: any = null
457
+ readonly result$ = new BehaviorSubject<any>(null)
458
+ readonly error$ = new BehaviorSubject<any>(null)
300
459
 
301
460
  constructor(
302
461
  protected fb: FormBuilder,
@@ -506,163 +665,1695 @@ export class ${name}Service {
506
665
  formFields,
507
666
  actionLabel,
508
667
  method,
509
- hasForm: formFields.length > 0
668
+ hasForm: formFields.length > 0,
669
+ responseFormat
510
670
  }));
511
671
  /**
512
672
  * 5️⃣ SCSS base
513
673
  */
514
674
  const scssPath = path_1.default.join(featureDir, `${fileBase}.component.scss`);
515
- fs_1.default.writeFileSync(scssPath, `
516
- :host {
517
- display: block;
518
- padding: 24px;
519
- min-height: 100vh;
675
+ fs_1.default.writeFileSync(scssPath, buildBaseScss());
676
+ return {
677
+ path: toRouteSegment(name),
678
+ component: `${name}Component`,
679
+ folder,
680
+ fileBase
681
+ };
520
682
  }
683
+ function generateAdminFeature(schema, schemaByOpId, featuresRoot, generatedRoot, schemasRoot) {
684
+ const smart = schema?.meta?.intelligent ?? {};
685
+ const adminOpId = String(schema?.api?.operationId || '');
686
+ const name = toPascalCase(adminOpId);
687
+ const folder = toFolderName(name);
688
+ const fileBase = toFileBase(name);
689
+ const featureDir = path_1.default.join(generatedRoot, folder);
690
+ fs_1.default.mkdirSync(featureDir, { recursive: true });
691
+ const appRoot = path_1.default.resolve(featuresRoot, '..');
692
+ ensureUiComponents(appRoot, schemasRoot);
693
+ const listOpId = smart.listOperationId;
694
+ const detailOpId = smart.detailOperationId;
695
+ const updateOpId = smart.updateOperationId;
696
+ const deleteOpId = smart.deleteOperationId;
697
+ const listSchema = listOpId ? schemaByOpId.get(listOpId) : null;
698
+ const detailSchema = detailOpId ? schemaByOpId.get(detailOpId) : null;
699
+ const updateSchema = updateOpId ? schemaByOpId.get(updateOpId) : null;
700
+ const deleteSchema = deleteOpId ? schemaByOpId.get(deleteOpId) : null;
701
+ const listName = listOpId ? toPascalCase(listOpId) : '';
702
+ const detailName = detailOpId ? toPascalCase(detailOpId) : '';
703
+ const updateName = updateOpId ? toPascalCase(updateOpId) : '';
704
+ const deleteName = deleteOpId ? toPascalCase(deleteOpId) : '';
705
+ const listFileBase = listOpId ? toFileBase(listName) : '';
706
+ const detailFileBase = detailOpId ? toFileBase(detailName) : '';
707
+ const updateFileBase = updateOpId ? toFileBase(updateName) : '';
708
+ const deleteFileBase = deleteOpId ? toFileBase(deleteName) : '';
709
+ const listFolder = listOpId ? toFolderName(listName) : '';
710
+ const detailFolder = detailOpId ? toFolderName(detailName) : '';
711
+ const updateFolder = updateOpId ? toFolderName(updateName) : '';
712
+ const deleteFolder = deleteOpId ? toFolderName(deleteName) : '';
713
+ const listServicePath = listOpId
714
+ ? buildRelativeImportPath(featureDir, path_1.default.join(generatedRoot, listFolder, `${listFileBase}.service.gen`))
715
+ : '';
716
+ const deleteServicePath = deleteOpId
717
+ ? buildRelativeImportPath(featureDir, path_1.default.join(generatedRoot, deleteFolder, `${deleteFileBase}.service.gen`))
718
+ : '';
719
+ const title = smart.label || `${schema.entity || 'Admin'}`;
720
+ const subtitle = listSchema?.api?.endpoint
721
+ ? `GET ${listSchema.api.endpoint}`
722
+ : 'Admin list';
723
+ const idParam = detailSchema?.api?.pathParams?.[0]?.name ??
724
+ smart.idParam ??
725
+ 'id';
726
+ const detailRoute = detailOpId
727
+ ? normalizeRoutePath(detailOpId)
728
+ : '';
729
+ const updateRoute = updateOpId
730
+ ? normalizeRoutePath(updateOpId)
731
+ : '';
732
+ const componentPath = path_1.default.join(featureDir, `${fileBase}.component.ts`);
733
+ const hasDelete = Boolean(deleteOpId);
734
+ const hasEdit = Boolean(updateOpId);
735
+ const hasDetail = Boolean(detailOpId);
736
+ const uiCardImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-card', 'ui-card.component'));
737
+ const uiButtonImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-button', 'ui-button.component'));
738
+ const uiCheckboxImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-checkbox', 'ui-checkbox.component'));
739
+ const uiInputImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-input', 'ui-input.component'));
740
+ const uiTextareaImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-textarea', 'ui-textarea.component'));
741
+ const uiSelectImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-select', 'ui-select.component'));
742
+ const uiSearchImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-search', 'ui-search.component'));
743
+ const uiBadgeImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-badge', 'ui-badge.component'));
744
+ const uiStatCardImport = buildRelativeImportPath(featureDir, path_1.default.join(appRoot, 'ui', 'ui-stat-card', 'ui-stat-card.component'));
745
+ const schemaImportPath = buildSchemaImportPath(featureDir, schemasRoot, toSafeFileName(adminOpId));
746
+ const generatedAdminComponentSource = `
747
+ import { Component, OnDestroy, OnInit, AfterViewInit } from '@angular/core'
748
+ import { CommonModule } from '@angular/common'
749
+ import { Router } from '@angular/router'
750
+ import { FormBuilder, ReactiveFormsModule } from '@angular/forms'
751
+ import { UiCardComponent } from '${uiCardImport}'
752
+ import { UiButtonComponent } from '${uiButtonImport}'
753
+ import { UiCheckboxComponent } from '${uiCheckboxImport}'
754
+ import { UiInputComponent } from '${uiInputImport}'
755
+ import { UiTextareaComponent } from '${uiTextareaImport}'
756
+ import { UiSelectComponent } from '${uiSelectImport}'
757
+ import { UiSearchComponent } from '${uiSearchImport}'
758
+ import { UiBadgeComponent } from '${uiBadgeImport}'
759
+ import { UiStatCardComponent } from '${uiStatCardImport}'
760
+ import { ${listName}Service } from '${listServicePath}'
761
+ ${hasDelete ? `import { ${deleteName}Service } from '${deleteServicePath}'` : ''}
762
+ import { BehaviorSubject, forkJoin } from 'rxjs'
763
+ import screenSchema from '${schemaImportPath}'
521
764
 
522
- .page {
523
- display: grid;
524
- gap: 16px;
525
- min-height: calc(100vh - 48px);
526
- }
765
+ @Component({
766
+ selector: 'app-${toKebab(name)}',
767
+ standalone: true,
768
+ imports: [
769
+ CommonModule,
770
+ ReactiveFormsModule,
771
+ UiCardComponent,
772
+ UiButtonComponent,
773
+ UiSelectComponent,
774
+ UiCheckboxComponent,
775
+ UiInputComponent,
776
+ UiTextareaComponent,
777
+ UiSearchComponent,
778
+ UiBadgeComponent,
779
+ UiStatCardComponent
780
+ ],
781
+ templateUrl: './${fileBase}.component.html',
782
+ styleUrls: ['./${fileBase}.component.scss']
783
+ })
784
+ export class ${name}Component implements OnInit, AfterViewInit, OnDestroy {
785
+ private readonly onFocus = () => {
786
+ if (!this.loading) {
787
+ this.refresh()
788
+ }
789
+ }
790
+ private readonly onVisibility = () => {
791
+ if (
792
+ typeof document !== 'undefined' &&
793
+ document.visibilityState === 'visible'
794
+ ) {
795
+ this.refresh()
796
+ }
797
+ }
527
798
 
528
- .screen-description {
529
- margin: 0 0 18px;
530
- color: #6b7280;
531
- font-size: 14px;
532
- line-height: 1.5;
533
- }
799
+ loading = false
800
+ readonly result$ = new BehaviorSubject<any>(null)
801
+ readonly error$ = new BehaviorSubject<any>(null)
802
+ schema = screenSchema as any
803
+ selected = new Set<any>()
804
+ confirmIds: any[] = []
805
+ searchTerm = ''
806
+ private hasInitialLoad = false
534
807
 
535
- .form-grid {
536
- display: grid;
537
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
538
- gap: 16px;
539
- width: 100%;
540
- max-width: 960px;
541
- margin: 0 auto;
542
- }
808
+ constructor(
809
+ private router: Router,
810
+ private listService: ${listName}Service
811
+ ${hasDelete ? `, private deleteService: ${deleteName}Service` : ''}
812
+ ) {
813
+ }
543
814
 
544
- .form-field {
545
- display: grid;
546
- gap: 8px;
547
- }
815
+ ngOnInit() {
816
+ this.setupAutoRefresh()
817
+ this.ensureInitialLoad()
818
+ this.setupAutoRefreshListeners()
819
+ }
548
820
 
549
- .field-error {
550
- color: #ef4444;
551
- font-size: 12px;
552
- margin-top: -4px;
553
- }
821
+ ngAfterViewInit() {
822
+ this.ensureInitialLoad()
823
+ }
554
824
 
555
- .actions {
556
- display: flex;
557
- justify-content: flex-end;
558
- gap: 14px;
559
- margin-top: 20px;
560
- flex-wrap: wrap;
561
- }
825
+ ngOnDestroy() {
826
+ if (typeof window !== 'undefined') {
827
+ window.removeEventListener('focus', this.onFocus)
828
+ }
829
+ if (typeof document !== 'undefined') {
830
+ document.removeEventListener('visibilitychange', this.onVisibility)
831
+ }
832
+ }
562
833
 
563
- .result {
564
- margin-top: 20px;
565
- padding: 16px;
566
- border-radius: 12px;
567
- background: #0f172a;
568
- color: #e2e8f0;
569
- font-size: 12px;
570
- box-shadow: 0 12px 28px rgba(15, 23, 42, 0.25);
571
- overflow: auto;
572
- }
834
+ private setupAutoRefresh() {
835
+ this.load()
836
+ }
573
837
 
574
- .result-table {
575
- margin-top: 20px;
576
- max-width: 100%;
577
- overflow: auto;
578
- border-radius: 16px;
579
- border: 1px solid #e2e8f0;
580
- box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
581
- -webkit-overflow-scrolling: touch;
582
- }
838
+ private setupAutoRefreshListeners() {
839
+ if (typeof window !== 'undefined') {
840
+ window.addEventListener('focus', this.onFocus)
841
+ }
842
+ if (typeof document !== 'undefined') {
843
+ document.addEventListener('visibilitychange', this.onVisibility)
844
+ }
845
+ }
583
846
 
584
- .result-card {
585
- margin-top: 20px;
586
- border-radius: 16px;
587
- border: 1px solid #e2e8f0;
588
- background: #ffffff;
589
- box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
590
- padding: 18px;
591
- }
847
+ private ensureInitialLoad() {
848
+ if (this.hasInitialLoad) return
849
+ this.hasInitialLoad = true
850
+ this.load()
851
+ }
592
852
 
593
- .result-card__grid {
594
- display: grid;
595
- gap: 12px;
596
- }
853
+ load() {
854
+ if (this.loading) return
855
+ this.loading = true
856
+ this.error$.next(null)
857
+ this.listService
858
+ .execute({}, {}, {})
859
+ .subscribe({
860
+ next: (result: any) => {
861
+ const normalized =
862
+ result && typeof result === 'object' && 'body' in result
863
+ ? (result as any).body
864
+ : result
865
+ this.result$.next(normalized)
866
+ this.loading = false
867
+ },
868
+ error: (error: any) => {
869
+ this.error$.next(error)
870
+ this.loading = false
871
+ }
872
+ })
873
+ }
597
874
 
598
- .result-card__row {
599
- display: flex;
600
- justify-content: space-between;
601
- gap: 16px;
602
- border-bottom: 1px solid #e2e8f0;
603
- padding-bottom: 10px;
604
- }
875
+ refresh() {
876
+ this.load()
877
+ }
605
878
 
606
- .result-card__row:last-child {
607
- border-bottom: none;
608
- padding-bottom: 0;
609
- }
879
+ onSearch(value: string) {
880
+ this.searchTerm = value
881
+ }
610
882
 
611
- .result-card__label {
612
- font-weight: 600;
613
- color: #475569;
614
- font-size: 12px;
615
- letter-spacing: 0.08em;
616
- text-transform: uppercase;
617
- }
883
+ isArrayResult(raw?: any) {
884
+ return this.getVisibleRows(raw).length > 0
885
+ }
618
886
 
619
- .result-card__value {
620
- color: #0f172a;
621
- font-weight: 600;
622
- text-align: right;
623
- }
887
+ getRows(raw?: any) {
888
+ const value = this.unwrapResult(raw ?? this.result$.value)
889
+ if (Array.isArray(value)) return value
890
+ if (!value || typeof value !== 'object') return []
624
891
 
625
- .result-error {
626
- margin-top: 24px;
627
- padding: 16px 18px;
628
- border-radius: 16px;
629
- border: 1px solid rgba(239, 68, 68, 0.3);
630
- background: #fff1f2;
631
- color: #881337;
632
- box-shadow: 0 10px 24px rgba(239, 68, 68, 0.15);
633
- display: grid;
634
- gap: 8px;
635
- }
892
+ const commonKeys = ['data', 'items', 'results', 'list', 'records', 'products']
893
+ for (const key of commonKeys) {
894
+ if (Array.isArray(value[key])) return value[key]
895
+ }
636
896
 
637
- .result-error__body {
638
- font-size: 13px;
639
- color: #7f1d1d;
640
- word-break: break-word;
641
- }
897
+ const found = this.findFirstArray(value, 0, 5)
898
+ return found ?? []
899
+ }
642
900
 
643
- .result-raw {
644
- margin-top: 24px;
645
- padding: 16px 18px;
646
- border-radius: 16px;
647
- border: 1px dashed rgba(15, 23, 42, 0.18);
648
- background: #f8fafc;
649
- color: #0f172a;
650
- display: grid;
651
- gap: 10px;
652
- }
901
+ getVisibleRows(raw?: any) {
902
+ const rows = this.getRows(raw)
903
+ const term = this.searchTerm.trim().toLowerCase()
904
+ if (!term) return rows
905
+ return rows.filter(row => this.matchesSearch(row, term))
906
+ }
653
907
 
654
- .result-raw summary {
655
- cursor: pointer;
656
- font-weight: 600;
657
- color: #334155;
658
- }
908
+ private getConfiguredColumns() {
909
+ const columns = this.schema?.data?.table?.columns
910
+ if (!Array.isArray(columns)) return []
911
+ return columns
912
+ .map((entry: any) => {
913
+ if (typeof entry === 'string') {
914
+ return { key: entry, label: '', visible: true }
915
+ }
916
+ if (entry && typeof entry === 'object') {
917
+ const key = String(
918
+ entry.key ?? entry.id ?? entry.column ?? ''
919
+ ).trim()
920
+ if (!key) return null
921
+ const label =
922
+ typeof entry.label === 'string' ? entry.label : ''
923
+ const visible = entry.visible !== false
924
+ return { key, label, visible }
925
+ }
926
+ return null
927
+ })
928
+ .filter(Boolean) as Array<{ key: string; label: string; visible: boolean }>
929
+ }
659
930
 
660
- .result-raw pre {
661
- margin: 0;
662
- padding: 12px;
663
- border-radius: 12px;
664
- background: #ffffff;
665
- border: 1px solid rgba(15, 23, 42, 0.08);
931
+ private getColumnLabel(value: string) {
932
+ const configured = this.getConfiguredColumns()
933
+ const match = configured.find(column => column.key === value)
934
+ return match?.label ?? ''
935
+ }
936
+
937
+ getColumns(raw?: any) {
938
+ const configured = this.getConfiguredColumns()
939
+ if (configured.length) {
940
+ return configured
941
+ .filter(column => column.visible)
942
+ .map(column => column.key)
943
+ }
944
+
945
+ const rows = this.getRows(raw)
946
+ if (rows.length > 0 && rows[0] && typeof rows[0] === 'object') {
947
+ return Object.keys(rows[0])
948
+ }
949
+ return []
950
+ }
951
+
952
+ formatHeader(value: string) {
953
+ const configured = this.getColumnLabel(value)
954
+ if (configured) return configured
955
+ return value
956
+ .replace(/[_-]/g, ' ')
957
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
958
+ .replace(/\\b\\w/g, char => char.toUpperCase())
959
+ }
960
+
961
+ getCellValue(row: any, column: string) {
962
+ if (!row || !column) return ''
963
+
964
+ if (column.includes('.')) {
965
+ return column
966
+ .split('.')
967
+ .reduce((acc, key) => (acc ? acc[key] : undefined), row) ?? ''
968
+ }
969
+
970
+ const value = row[column]
971
+ return this.formatValue(value)
972
+ }
973
+
974
+ isImageCell(row: any, column: string) {
975
+ const value = this.getCellValue(row, column)
976
+ return (
977
+ typeof value === 'string' &&
978
+ /^https?:\\/\\//.test(value) &&
979
+ /(\\.png|\\.jpg|\\.jpeg|\\.svg)/i.test(value)
980
+ )
981
+ }
982
+
983
+ formatValue(value: any): string {
984
+ if (value === null || value === undefined) return ''
985
+ if (typeof value === 'string' || typeof value === 'number') {
986
+ return String(value)
987
+ }
988
+ if (typeof value === 'boolean') {
989
+ return value ? 'Yes' : 'No'
990
+ }
991
+ if (Array.isArray(value)) {
992
+ return value
993
+ .map((item: any) => this.formatValue(item))
994
+ .join(', ')
995
+ }
996
+ if (typeof value === 'object') {
997
+ if (typeof value.common === 'string') return value.common
998
+ if (typeof value.official === 'string') return value.official
999
+ if (typeof value.name === 'string') return value.name
1000
+ if (typeof value.label === 'string') return value.label
1001
+ return JSON.stringify(value)
1002
+ }
1003
+ return String(value)
1004
+ }
1005
+
1006
+ getCardImage(row: any) {
1007
+ if (!row || typeof row !== 'object') return ''
1008
+ const directKeys = ['thumbnail', 'image', 'avatar', 'photo', 'picture']
1009
+ for (const key of directKeys) {
1010
+ if (typeof row[key] === 'string') return row[key]
1011
+ }
1012
+ if (Array.isArray(row.images) && row.images.length) {
1013
+ return row.images[0]
1014
+ }
1015
+ return ''
1016
+ }
1017
+
1018
+ getCardTitle(row: any) {
1019
+ if (!row || typeof row !== 'object') return 'Item'
1020
+ return (
1021
+ row.title ??
1022
+ row.name ??
1023
+ row.label ??
1024
+ row.id ??
1025
+ 'Item'
1026
+ )
1027
+ }
1028
+
1029
+ getCardSubtitle(row: any) {
1030
+ if (!row || typeof row !== 'object') return ''
1031
+ return (
1032
+ row.description ??
1033
+ row.category ??
1034
+ row.brand ??
1035
+ ''
1036
+ )
1037
+ }
1038
+
1039
+ formatError(error: any) {
1040
+ if (!error) return ''
1041
+ if (typeof error === 'string') return error
1042
+ if (typeof error?.message === 'string') return error.message
1043
+ try {
1044
+ return JSON.stringify(error)
1045
+ } catch {
1046
+ return String(error)
1047
+ }
1048
+ }
1049
+
1050
+ toggleAll(checked: boolean) {
1051
+ this.selected.clear()
1052
+ if (!checked) return
1053
+ for (const row of this.getVisibleRows()) {
1054
+ const id = this.getRowId(row)
1055
+ if (id !== null && id !== undefined) {
1056
+ this.selected.add(id)
1057
+ }
1058
+ }
1059
+ }
1060
+
1061
+ toggleRow(row: any, checked: boolean) {
1062
+ const id = this.getRowId(row)
1063
+ if (id === null || id === undefined) return
1064
+ if (checked) {
1065
+ this.selected.add(id)
1066
+ } else {
1067
+ this.selected.delete(id)
1068
+ }
1069
+ }
1070
+
1071
+ isSelected(row: any) {
1072
+ const id = this.getRowId(row)
1073
+ if (id === null || id === undefined) return false
1074
+ return this.selected.has(id)
1075
+ }
1076
+
1077
+ openDetail(row: any) {
1078
+ ${hasDetail ? '' : 'return'}
1079
+ const id = this.getRowId(row)
1080
+ if (id === null || id === undefined) return
1081
+ this.router.navigate(['${detailRoute}'], {
1082
+ state: { prefill: { ${idParam}: id }, autoSubmit: true }
1083
+ })
1084
+ }
1085
+
1086
+ openEdit(row: any) {
1087
+ ${hasEdit ? '' : 'return'}
1088
+ this.router.navigate(['${updateRoute}'], {
1089
+ state: { prefill: row }
1090
+ })
1091
+ }
1092
+
1093
+ confirmBulkDelete() {
1094
+ this.confirmDelete([...this.selected])
1095
+ }
1096
+
1097
+ confirmDelete(ids: any[]) {
1098
+ this.confirmIds = ids.filter(
1099
+ id => id !== null && id !== undefined
1100
+ )
1101
+ }
1102
+
1103
+ cancelDelete() {
1104
+ this.confirmIds = []
1105
+ }
1106
+
1107
+ deleteConfirmed() {
1108
+ const ids = [...this.confirmIds]
1109
+ this.confirmIds = []
1110
+ if (!ids.length) return
1111
+ ${hasDelete ? `const calls = ids.map(id => this.deleteService.execute({ ${idParam}: id }, {}, {}))` : 'const calls: any[] = []'}
1112
+ if (!calls.length) return
1113
+ forkJoin(calls).subscribe({
1114
+ next: () => {
1115
+ this.removeFromResult(ids)
1116
+ ids.forEach(id => this.selected.delete(id))
1117
+ },
1118
+ error: error => {
1119
+ this.error$.next(error)
1120
+ }
1121
+ })
1122
+ }
1123
+
1124
+ getRowId(row: any) {
1125
+ if (!row || typeof row !== 'object') return null
1126
+ if (row['${idParam}'] !== undefined) return row['${idParam}']
1127
+ if (row['id'] !== undefined) return row['id']
1128
+ return null
1129
+ }
1130
+
1131
+ getStatusVariant(row: any): 'success' | 'warning' | 'danger' | 'neutral' {
1132
+ const raw = this.getStatusValue(row)
1133
+ const value = String(raw ?? '').toLowerCase()
1134
+ if (/active|ok|success|instock|available/.test(value)) return 'success'
1135
+ if (/warning|low|pending|draft/.test(value)) return 'warning'
1136
+ if (/error|inactive|disabled|out|blocked|archived/.test(value)) return 'danger'
1137
+ return 'neutral'
1138
+ }
1139
+
1140
+ getStatusLabel(row: any) {
1141
+ const raw = this.getStatusValue(row)
1142
+ if (raw === null || raw === undefined || raw === '') return 'Unknown'
1143
+ return this.formatValue(raw)
1144
+ }
1145
+
1146
+ getStatusValue(row: any, key?: string) {
1147
+ if (key && row && typeof row === 'object') {
1148
+ return row[key]
1149
+ }
1150
+ return (
1151
+ row?.status ??
1152
+ row?.paymentStatus ??
1153
+ row?.availabilityStatus ??
1154
+ row?.state ??
1155
+ row?.stockStatus ??
1156
+ row?.result
1157
+ )
1158
+ }
1159
+
1160
+ isStatusColumn(column: string) {
1161
+ return /status|state|availability/i.test(column)
1162
+ }
1163
+
1164
+ isIdColumn(column: string) {
1165
+ return /(^id$)|(_id$)|(Id$)/.test(column)
1166
+ }
1167
+
1168
+ isMethodColumn(column: string) {
1169
+ return /method|paymentMethod/i.test(column)
1170
+ }
1171
+
1172
+ isDateColumn(column: string) {
1173
+ return /date|createdAt|updatedAt|timestamp/i.test(column)
1174
+ }
1175
+
1176
+ formatMethod(value: any) {
1177
+ const raw = String(value ?? '')
1178
+ if (!raw) return ''
1179
+ const labels: Record<string, string> = {
1180
+ credit_card: 'Cartão de Crédito',
1181
+ bank_transfer: 'Transferência Bancária',
1182
+ pix: 'PIX'
1183
+ }
1184
+ return labels[raw] ?? this.formatValue(raw)
1185
+ }
1186
+
1187
+ formatDateValue(value: any) {
1188
+ if (!value) return ''
1189
+ const date = new Date(String(value))
1190
+ if (Number.isNaN(date.getTime())) return this.formatValue(value)
1191
+ return date.toLocaleDateString('pt-BR', {
1192
+ day: '2-digit',
1193
+ month: '2-digit',
1194
+ year: 'numeric',
1195
+ hour: '2-digit',
1196
+ minute: '2-digit'
1197
+ })
1198
+ }
1199
+
1200
+ formatCurrency(value: number) {
1201
+ try {
1202
+ return new Intl.NumberFormat('pt-BR', {
1203
+ style: 'currency',
1204
+ currency: 'BRL'
1205
+ }).format(value)
1206
+ } catch {
1207
+ return 'R$ ' + value.toFixed(2)
1208
+ }
1209
+ }
1210
+
1211
+ getStatusKey(raw?: any) {
1212
+ const configured = this.getConfiguredColumns().map(column => column.key)
1213
+ const candidates = configured.length
1214
+ ? configured
1215
+ : this.getColumns(raw)
1216
+ const match = candidates.find(column => this.isStatusColumn(column))
1217
+ return match ?? ''
1218
+ }
1219
+
1220
+ getStatusLabelValue(value: string) {
1221
+ const normalized = value.toLowerCase()
1222
+ const labels: Record<string, string> = {
1223
+ completed: 'Concluído',
1224
+ pending: 'Pendente',
1225
+ failed: 'Falhou',
1226
+ active: 'Ativo',
1227
+ inactive: 'Inativo'
1228
+ }
1229
+ if (labels[normalized]) return labels[normalized]
1230
+ return value
1231
+ .replace(/[_-]/g, ' ')
1232
+ .replace(/\\b\\w/g, char => char.toUpperCase())
1233
+ }
1234
+
1235
+ getStatusSummary(raw?: any) {
1236
+ const rows = this.getVisibleRows(raw)
1237
+ if (!rows.length) return []
1238
+ const key = this.getStatusKey(raw)
1239
+ if (!key) return []
1240
+
1241
+ const buckets = new Map<
1242
+ string,
1243
+ { key: string; label: string; variant: 'success' | 'warning' | 'danger' | 'neutral'; count: number; total: number }
1244
+ >()
1245
+
1246
+ for (const row of rows) {
1247
+ const rawValue = this.getStatusValue(row, key)
1248
+ if (rawValue === null || rawValue === undefined || rawValue === '') {
1249
+ continue
1250
+ }
1251
+ const value = String(rawValue)
1252
+ const normalized = value.toLowerCase()
1253
+ const current = buckets.get(normalized) ?? {
1254
+ key: normalized,
1255
+ label: this.getStatusLabelValue(value),
1256
+ variant: this.getStatusVariant({ [key]: normalized }),
1257
+ count: 0,
1258
+ total: 0
1259
+ }
1260
+ current.count += 1
1261
+ current.total += this.getAmountValue(row)
1262
+ buckets.set(normalized, current)
1263
+ }
1264
+
1265
+ return Array.from(buckets.values()).sort(
1266
+ (a, b) => b.count - a.count
1267
+ )
1268
+ }
1269
+
1270
+ getAmountValue(row: any) {
1271
+ const candidates = ['amount', 'total', 'price', 'value', 'paidAmount']
1272
+ for (const key of candidates) {
1273
+ const raw = row?.[key]
1274
+ const num = Number(raw)
1275
+ if (Number.isFinite(num)) return num
1276
+ }
1277
+ return 0
1278
+ }
1279
+
1280
+
1281
+ private unwrapResult(value: any) {
1282
+ if (!value || typeof value !== 'object') return value
1283
+ if (Object.prototype.hasOwnProperty.call(value, 'data')) {
1284
+ return value.data
1285
+ }
1286
+ if (Object.prototype.hasOwnProperty.call(value, 'result')) {
1287
+ return value.result
1288
+ }
1289
+ if (Object.prototype.hasOwnProperty.call(value, 'body')) {
1290
+ return value.body
1291
+ }
1292
+ return value
1293
+ }
1294
+
1295
+ private removeFromResult(ids: any[]) {
1296
+ const value = this.unwrapResult(this.result$.value)
1297
+ if (Array.isArray(value)) {
1298
+ this.result$.next(value.filter(item => !ids.includes(this.getRowId(item))))
1299
+ return
1300
+ }
1301
+
1302
+ if (!value || typeof value !== 'object') return
1303
+ const key = this.findArrayKey(value)
1304
+ if (!key) return
1305
+ const nextArray = (value[key] as any[]).filter(
1306
+ item => !ids.includes(this.getRowId(item))
1307
+ )
1308
+ this.result$.next({ ...value, [key]: nextArray })
1309
+ }
1310
+
1311
+ private findArrayKey(value: Record<string, any>) {
1312
+ const commonKeys = ['data', 'items', 'results', 'list', 'records', 'products']
1313
+ for (const key of commonKeys) {
1314
+ if (Array.isArray(value[key])) return key
1315
+ }
1316
+ for (const key of Object.keys(value)) {
1317
+ if (Array.isArray(value[key])) return key
1318
+ }
1319
+ return null
1320
+ }
1321
+
1322
+ private matchesSearch(row: any, term: string) {
1323
+ if (!row || typeof row !== 'object') return false
1324
+ const values = Object.values(row)
1325
+ for (const value of values) {
1326
+ const normalized = this.formatValue(value).toLowerCase()
1327
+ if (normalized.includes(term)) return true
1328
+ }
1329
+ return false
1330
+ }
1331
+
1332
+ private findFirstArray(
1333
+ value: any,
1334
+ depth: number,
1335
+ maxDepth: number
1336
+ ): any[] | null {
1337
+ if (!value || depth > maxDepth) return null
1338
+ if (Array.isArray(value)) return value
1339
+ if (typeof value !== 'object') return null
1340
+
1341
+ for (const key of Object.keys(value)) {
1342
+ const found = this.findFirstArray(value[key], depth + 1, maxDepth)
1343
+ if (found) return found
1344
+ }
1345
+ return null
1346
+ }
1347
+ }
1348
+ `;
1349
+ fs_1.default.writeFileSync(componentPath, generatedAdminComponentSource);
1350
+ const htmlPath = path_1.default.join(featureDir, `${fileBase}.component.html`);
1351
+ const responseFormat = resolveResponseFormat(schema);
1352
+ const useCards = responseFormat === 'cards';
1353
+ const useRaw = responseFormat === 'raw';
1354
+ fs_1.default.writeFileSync(htmlPath, `
1355
+ <div class="page">
1356
+ <div class="admin-header">
1357
+ <div>
1358
+ <div class="admin-title">
1359
+ <span class="admin-icon"></span>
1360
+ <span>${escapeAttr(title)}</span>
1361
+ </div>
1362
+ <div class="admin-subtitle">${escapeAttr(subtitle)}</div>
1363
+ </div>
1364
+ <div class="admin-actions">
1365
+ <ui-button variant="ghost" (click)="refresh()">Atualizar</ui-button>
1366
+ ${hasDelete ? '<ui-button variant="danger" [disabled]="!selected.size" (click)="confirmBulkDelete()">Excluir selecionados</ui-button>' : ''}
1367
+ </div>
1368
+ </div>
1369
+
1370
+ <div class="admin-summary" *ngIf="getStatusSummary(result$ | async).length">
1371
+ <ui-stat-card
1372
+ *ngFor="let item of getStatusSummary(result$ | async)"
1373
+ [title]="item.label"
1374
+ [value]="item.total > 0 ? formatCurrency(item.total) : (item.count + '')"
1375
+ [meta]="item.count + ' registros'"
1376
+ >
1377
+ <span class="stat-icon" [class.success]="item.variant === 'success'" [class.warning]="item.variant === 'warning'" [class.danger]="item.variant === 'danger'"></span>
1378
+ </ui-stat-card>
1379
+ </div>
1380
+
1381
+ <ui-card>
1382
+ <div class="admin-toolbar">
1383
+ <ui-search
1384
+ [value]="searchTerm"
1385
+ [placeholder]="'Buscar...'"
1386
+ (valueChange)="onSearch($event)"
1387
+ ></ui-search>
1388
+ <div class="admin-toolbar__meta">
1389
+ <ui-badge variant="neutral">
1390
+ {{ getVisibleRows(result$ | async).length }} registros
1391
+ </ui-badge>
1392
+ </div>
1393
+ </div>
1394
+
1395
+ <ng-container *ngIf="error$ | async as error">
1396
+ <div class="result-error" *ngIf="error">
1397
+ <strong>Request failed.</strong>
1398
+ <div class="result-error__body">
1399
+ {{ formatError(error) }}
1400
+ </div>
1401
+ </div>
1402
+ </ng-container>
1403
+
1404
+ <ng-container *ngIf="result$ | async as result">
1405
+ ${useRaw ? `
1406
+ <details class="result-raw" *ngIf="result">
1407
+ <summary>Raw response</summary>
1408
+ <pre>{{ result | json }}</pre>
1409
+ </details>
1410
+ ` : useCards ? `
1411
+ <div class="result-cards" *ngIf="isArrayResult(result)">
1412
+ <article class="card-tile" *ngFor="let row of getVisibleRows(result)">
1413
+ <div class="card-media" *ngIf="getCardImage(row)">
1414
+ <img [src]="getCardImage(row)" [alt]="getCardTitle(row)" />
1415
+ </div>
1416
+ <div class="card-body">
1417
+ <div class="card-header">
1418
+ ${hasDelete ? '<input type="checkbox" [checked]="isSelected(row)" (click)="$event.stopPropagation()" (change)="toggleRow(row, $event.target.checked)" />' : ''}
1419
+ <h3 class="card-title">{{ getCardTitle(row) }}</h3>
1420
+ <ui-badge [variant]="getStatusVariant(row)">
1421
+ {{ getStatusLabel(row) }}
1422
+ </ui-badge>
1423
+ </div>
1424
+ <p class="card-subtitle" *ngIf="getCardSubtitle(row)">
1425
+ {{ getCardSubtitle(row) }}
1426
+ </p>
1427
+ <div class="card-actions" (click)="$event.stopPropagation()">
1428
+ ${hasDetail ? '<button class="link" type="button" (click)="openDetail(row)">Details</button>' : ''}
1429
+ ${hasEdit ? '<button class="link" type="button" (click)="openEdit(row)">Edit</button>' : ''}
1430
+ ${hasDelete ? '<button class="link danger" type="button" (click)="confirmDelete([getRowId(row)])">Delete</button>' : ''}
1431
+ </div>
1432
+ </div>
1433
+ </article>
1434
+ </div>
1435
+ ` : `
1436
+ <div class="result-table" *ngIf="isArrayResult(result)">
1437
+ <table class="data-table">
1438
+ <thead>
1439
+ <tr>
1440
+ ${hasDelete ? '<th class="select-cell"><input type="checkbox" (change)="toggleAll($event.target.checked)" /></th>' : ''}
1441
+ <th *ngFor="let column of getColumns(result)">
1442
+ {{ formatHeader(column) }}
1443
+ </th>
1444
+ ${hasDetail || hasEdit || hasDelete ? '<th class="actions-cell">Actions</th>' : ''}
1445
+ </tr>
1446
+ </thead>
1447
+ <tbody>
1448
+ <tr *ngFor="let row of getVisibleRows(result)" (click)="openDetail(row)">
1449
+ ${hasDelete ? '<td class="select-cell"><input type="checkbox" [checked]="isSelected(row)" (click)="$event.stopPropagation()" (change)="toggleRow(row, $event.target.checked)" /></td>' : ''}
1450
+ <td *ngFor="let column of getColumns(result)">
1451
+ <img
1452
+ *ngIf="isImageCell(row, column)"
1453
+ [src]="getCellValue(row, column)"
1454
+ [alt]="formatHeader(column)"
1455
+ class="cell-image"
1456
+ />
1457
+ <ng-container *ngIf="!isImageCell(row, column)">
1458
+ <span *ngIf="isStatusColumn(column)" class="cell-status">
1459
+ <ui-badge [variant]="getStatusVariant(row)">
1460
+ {{ getStatusLabel(row) }}
1461
+ </ui-badge>
1462
+ </span>
1463
+ <span *ngIf="isIdColumn(column) && !isStatusColumn(column)" class="code-pill">
1464
+ {{ getCellValue(row, column) }}
1465
+ </span>
1466
+ <span *ngIf="isMethodColumn(column) && !isStatusColumn(column) && !isIdColumn(column)">
1467
+ {{ formatMethod(getCellValue(row, column)) }}
1468
+ </span>
1469
+ <span *ngIf="isDateColumn(column) && !isStatusColumn(column) && !isIdColumn(column) && !isMethodColumn(column)">
1470
+ {{ formatDateValue(getCellValue(row, column)) }}
1471
+ </span>
1472
+ <span *ngIf="!isStatusColumn(column) && !isIdColumn(column) && !isMethodColumn(column) && !isDateColumn(column)">
1473
+ {{ getCellValue(row, column) }}
1474
+ </span>
1475
+ </ng-container>
1476
+ </td>
1477
+ ${hasDetail || hasEdit || hasDelete ? `<td class="actions-cell">
1478
+ <div class="row-actions" (click)="$event.stopPropagation()">
1479
+ ${hasDetail ? '<button class="link" type="button" (click)="openDetail(row)">Details</button>' : ''}
1480
+ ${hasEdit ? '<button class="link" type="button" (click)="openEdit(row)">Edit</button>' : ''}
1481
+ ${hasDelete ? '<button class="link danger" type="button" (click)="confirmDelete([getRowId(row)])">Delete</button>' : ''}
1482
+ </div>
1483
+ </td>` : ''}
1484
+ </tr>
1485
+ </tbody>
1486
+ </table>
1487
+ </div>
1488
+ `}
1489
+
1490
+ ${!useRaw ? `
1491
+ <details class="result-raw" *ngIf="result">
1492
+ <summary>Raw response</summary>
1493
+ <pre>{{ result | json }}</pre>
1494
+ </details>
1495
+ ` : ''}
1496
+ </ng-container>
1497
+
1498
+ </ui-card>
1499
+
1500
+ <div class="confirm-backdrop" *ngIf="confirmIds.length">
1501
+ <div class="confirm-modal">
1502
+ <h3>Confirm delete</h3>
1503
+ <p>Are you sure you want to delete the selected item(s)? This action cannot be undone.</p>
1504
+ <div class="actions">
1505
+ <ui-button variant="ghost" (click)="cancelDelete()">Cancel</ui-button>
1506
+ <ui-button variant="danger" (click)="deleteConfirmed()">Delete</ui-button>
1507
+ </div>
1508
+ </div>
1509
+ </div>
1510
+ </div>
1511
+ `);
1512
+ const scssPath = path_1.default.join(featureDir, `${fileBase}.component.scss`);
1513
+ fs_1.default.writeFileSync(scssPath, `${buildBaseScss()}
1514
+
1515
+ .admin-toolbar {
1516
+ display: grid;
1517
+ gap: 12px;
1518
+ margin-bottom: 14px;
1519
+ }
1520
+
1521
+ .admin-toolbar__meta {
1522
+ display: inline-flex;
1523
+ justify-content: flex-end;
1524
+ }
1525
+
1526
+ .admin-actions {
1527
+ justify-content: flex-end;
1528
+ gap: 10px;
1529
+ margin-top: 0;
1530
+ }
1531
+
1532
+ .admin-header {
1533
+ display: flex;
1534
+ align-items: flex-start;
1535
+ justify-content: space-between;
1536
+ gap: 16px;
1537
+ }
1538
+
1539
+ .admin-title {
1540
+ display: flex;
1541
+ align-items: center;
1542
+ gap: 10px;
1543
+ font-size: 22px;
1544
+ font-weight: 700;
1545
+ color: var(--bg-ink);
1546
+ }
1547
+
1548
+ .admin-icon {
1549
+ width: 28px;
1550
+ height: 28px;
1551
+ border-radius: 8px;
1552
+ background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
1553
+ box-shadow: 0 8px 20px rgba(114, 157, 191, 0.2);
1554
+ display: inline-block;
1555
+ }
1556
+
1557
+ .admin-subtitle {
1558
+ margin-top: 6px;
1559
+ font-size: 13px;
1560
+ color: var(--color-muted);
1561
+ }
1562
+
1563
+ .admin-summary {
1564
+ display: grid;
1565
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1566
+ gap: 12px;
1567
+ }
1568
+
1569
+ .stat-icon {
1570
+ width: 14px;
1571
+ height: 14px;
1572
+ border-radius: 999px;
1573
+ display: inline-block;
1574
+ }
1575
+
1576
+ .stat-icon.success {
1577
+ background: #34d399;
1578
+ }
1579
+
1580
+ .stat-icon.warning {
1581
+ background: #fbbf24;
1582
+ }
1583
+
1584
+ .stat-icon.danger {
1585
+ background: #f87171;
1586
+ }
1587
+
1588
+ .code-pill {
1589
+ font-family: "DM Sans", "Segoe UI", sans-serif;
1590
+ font-size: 11px;
1591
+ background: #eef2f7;
1592
+ color: #334155;
1593
+ padding: 4px 8px;
1594
+ border-radius: 6px;
1595
+ display: inline-block;
1596
+ }
1597
+
1598
+ .cell-status {
1599
+ display: inline-flex;
1600
+ align-items: center;
1601
+ }
1602
+
1603
+ .row-actions {
1604
+ display: inline-flex;
1605
+ gap: 10px;
1606
+ align-items: center;
1607
+ }
1608
+
1609
+ .row-actions .link {
1610
+ background: none;
1611
+ border: none;
1612
+ color: #2563eb;
1613
+ font-weight: 600;
1614
+ cursor: pointer;
1615
+ padding: 0;
1616
+ }
1617
+
1618
+ .row-actions .link.danger {
1619
+ color: #dc2626;
1620
+ }
1621
+
1622
+ .select-cell {
1623
+ width: 48px;
1624
+ }
1625
+
1626
+ .actions-cell {
1627
+ white-space: nowrap;
1628
+ }
1629
+
1630
+ .confirm-backdrop {
1631
+ position: fixed;
1632
+ inset: 0;
1633
+ background: rgba(15, 23, 42, 0.45);
1634
+ display: grid;
1635
+ place-items: center;
1636
+ z-index: 50;
1637
+ }
1638
+
1639
+ .confirm-modal {
1640
+ background: #ffffff;
1641
+ border-radius: 18px;
1642
+ padding: 20px 22px;
1643
+ width: min(380px, 92vw);
1644
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.28);
1645
+ display: grid;
1646
+ gap: 12px;
1647
+ }
1648
+
1649
+ .confirm-modal h3 {
1650
+ margin: 0;
1651
+ font-size: 18px;
1652
+ }
1653
+
1654
+ .confirm-modal p {
1655
+ margin: 0;
1656
+ color: #475569;
1657
+ font-size: 14px;
1658
+ }
1659
+ `);
1660
+ return {
1661
+ path: toRouteSegment(adminOpId),
1662
+ component: `${name}Component`,
1663
+ folder,
1664
+ fileBase
1665
+ };
1666
+ }
1667
+ function normalizeFields(fields) {
1668
+ return fields.map(field => ({
1669
+ name: field.name,
1670
+ type: field.type || 'string',
1671
+ required: Boolean(field.required),
1672
+ label: field.label || toLabel(field.name),
1673
+ placeholder: field.placeholder || toPlaceholder(field.name),
1674
+ hint: field.hint || undefined,
1675
+ info: field.info || undefined,
1676
+ options: field.options || null,
1677
+ defaultValue: field.defaultValue ?? null,
1678
+ source: 'body'
1679
+ }));
1680
+ }
1681
+ function normalizeQueryParams(params) {
1682
+ return params.map(param => {
1683
+ const labelText = param.label || param.name;
1684
+ const hintText = param.hint ||
1685
+ (typeof labelText === 'string' && labelText.length > 60
1686
+ ? labelText
1687
+ : '');
1688
+ const help = resolveFieldHelp(hintText, labelText);
1689
+ return {
1690
+ name: param.name,
1691
+ type: param.type || 'string',
1692
+ required: Boolean(param.required),
1693
+ label: toLabel(param.name),
1694
+ placeholder: param.placeholder || toPlaceholder(param.name),
1695
+ hint: help.hint,
1696
+ info: help.info,
1697
+ options: param.options || null,
1698
+ defaultValue: param.defaultValue ?? null,
1699
+ source: 'query'
1700
+ };
1701
+ });
1702
+ }
1703
+ function extractPathParams(endpoint) {
1704
+ const params = [];
1705
+ const regex = /{([^}]+)}/g;
1706
+ let match = regex.exec(endpoint);
1707
+ while (match) {
1708
+ params.push(match[1]);
1709
+ match = regex.exec(endpoint);
1710
+ }
1711
+ return params;
1712
+ }
1713
+ function buildFormControls(fields) {
1714
+ if (fields.length === 0)
1715
+ return '';
1716
+ return fields
1717
+ .map(field => {
1718
+ const value = field.defaultValue !== null && field.defaultValue !== undefined
1719
+ ? JSON.stringify(field.defaultValue)
1720
+ : defaultValueFor(field.type);
1721
+ const validators = field.required ? ', Validators.required' : '';
1722
+ return ` ${field.name}: [${value}${validators}]`;
1723
+ })
1724
+ .join(',\n');
1725
+ }
1726
+ function defaultValueFor(type) {
1727
+ switch (type) {
1728
+ case 'array':
1729
+ return '[]';
1730
+ case 'boolean':
1731
+ return 'false';
1732
+ case 'number':
1733
+ case 'integer':
1734
+ return 'null';
1735
+ default:
1736
+ return "''";
1737
+ }
1738
+ }
1739
+ function toLabel(value) {
1740
+ return stripDiacritics(String(value))
1741
+ .replace(/[_-]/g, ' ')
1742
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
1743
+ .replace(/\b\w/g, char => char.toUpperCase());
1744
+ }
1745
+ function toPlaceholder(value) {
1746
+ return toLabel(value);
1747
+ }
1748
+ function stripDiacritics(value) {
1749
+ return value
1750
+ .normalize('NFD')
1751
+ .replace(/[\u0300-\u036f]/g, '');
1752
+ }
1753
+ function normalizeWhitespace(value) {
1754
+ return String(value).replace(/\s+/g, ' ').trim();
1755
+ }
1756
+ function resolveFieldHelp(rawHint, label) {
1757
+ const hint = normalizeWhitespace(rawHint || '');
1758
+ if (!hint)
1759
+ return { hint: undefined, info: undefined };
1760
+ if (hint.length > 120) {
1761
+ return { hint: undefined, info: hint };
1762
+ }
1763
+ return { hint, info: undefined };
1764
+ }
1765
+ function escapeAttr(value) {
1766
+ return String(value).replace(/"/g, '&quot;');
1767
+ }
1768
+ function defaultActionLabel(method, hasParams) {
1769
+ switch (method) {
1770
+ case 'get':
1771
+ return hasParams ? 'Search' : 'Load';
1772
+ case 'post':
1773
+ return 'Criar';
1774
+ case 'put':
1775
+ case 'patch':
1776
+ return 'Salvar';
1777
+ case 'delete':
1778
+ return 'Excluir';
1779
+ default:
1780
+ return 'Executar';
1781
+ }
1782
+ }
1783
+ function resolveResponseFormat(schema) {
1784
+ const candidate = String(schema?.meta?.responseFormat ??
1785
+ schema?.response?.format ??
1786
+ schema?.meta?.view ??
1787
+ 'table')
1788
+ .trim()
1789
+ .toLowerCase();
1790
+ if (candidate === 'cards')
1791
+ return 'cards';
1792
+ if (candidate === 'raw')
1793
+ return 'raw';
1794
+ return 'table';
1795
+ }
1796
+ function buildComponentHtml(options) {
1797
+ const buttonVariant = options.method === 'delete' ? 'danger' : 'primary';
1798
+ const useCards = options.responseFormat === 'cards';
1799
+ const useRaw = options.responseFormat === 'raw';
1800
+ if (!options.hasForm) {
1801
+ return `
1802
+ <div class="page">
1803
+ <ui-card title="${options.title}" subtitle="${options.subtitle}">
1804
+ <div class="actions">
1805
+ <ui-button
1806
+ variant="${buttonVariant}"
1807
+ [disabled]="form.invalid"
1808
+ (click)="submit()"
1809
+ >
1810
+ ${options.actionLabel}
1811
+ </ui-button>
1812
+ </div>
1813
+ </ui-card>
1814
+
1815
+ ${useRaw ? `
1816
+ <ng-container *ngIf="error$ | async as error">
1817
+ <div class="result-error" *ngIf="error">
1818
+ <strong>Request failed.</strong>
1819
+ <div class="result-error__body">
1820
+ {{ formatError(error) }}
1821
+ </div>
1822
+ </div>
1823
+ </ng-container>
1824
+
1825
+ <ng-container *ngIf="result$ | async as result">
1826
+ <details class="result-raw" *ngIf="result">
1827
+ <summary>Raw response</summary>
1828
+ <pre>{{ result | json }}</pre>
1829
+ </details>
1830
+ </ng-container>
1831
+ ` : useCards ? `
1832
+ <ng-container *ngIf="error$ | async as error">
1833
+ <div class="result-error" *ngIf="error">
1834
+ <strong>Request failed.</strong>
1835
+ <div class="result-error__body">
1836
+ {{ formatError(error) }}
1837
+ </div>
1838
+ </div>
1839
+ </ng-container>
1840
+
1841
+ <ng-container *ngIf="result$ | async as result">
1842
+ <div class="result-cards" *ngIf="isArrayResult(result)">
1843
+ <article class="card-tile" *ngFor="let row of getRows(result)">
1844
+ <div class="card-media" *ngIf="getCardImage(row)">
1845
+ <img [src]="getCardImage(row)" [alt]="getCardTitle(row)" />
1846
+ </div>
1847
+ <div class="card-body">
1848
+ <h3 class="card-title">{{ getCardTitle(row) }}</h3>
1849
+ <p class="card-subtitle" *ngIf="getCardSubtitle(row)">
1850
+ {{ getCardSubtitle(row) }}
1851
+ </p>
1852
+ </div>
1853
+ </article>
1854
+ </div>
1855
+
1856
+ <details class="result-raw" *ngIf="result">
1857
+ <summary>Raw response</summary>
1858
+ <pre>{{ result | json }}</pre>
1859
+ </details>
1860
+ </ng-container>
1861
+ ` : `
1862
+ <ng-container *ngIf="error$ | async as error">
1863
+ <div class="result-error" *ngIf="error">
1864
+ <strong>Request failed.</strong>
1865
+ <div class="result-error__body">
1866
+ {{ formatError(error) }}
1867
+ </div>
1868
+ </div>
1869
+ </ng-container>
1870
+
1871
+ <ng-container *ngIf="result$ | async as result">
1872
+ <div class="result-table" *ngIf="isArrayResult(result)">
1873
+ <table class="data-table">
1874
+ <thead>
1875
+ <tr>
1876
+ <th *ngFor="let column of getColumns(result)">
1877
+ {{ formatHeader(column) }}
1878
+ </th>
1879
+ </tr>
1880
+ </thead>
1881
+ <tbody>
1882
+ <tr *ngFor="let row of getRows(result)">
1883
+ <td *ngFor="let column of getColumns(result)">
1884
+ <img
1885
+ *ngIf="isImageCell(row, column)"
1886
+ [src]="getCellValue(row, column)"
1887
+ [alt]="formatHeader(column)"
1888
+ class="cell-image"
1889
+ />
1890
+ <span *ngIf="!isImageCell(row, column)">
1891
+ {{ getCellValue(row, column) }}
1892
+ </span>
1893
+ </td>
1894
+ </tr>
1895
+ </tbody>
1896
+ </table>
1897
+ </div>
1898
+
1899
+ <div class="result-card" *ngIf="!isArrayResult(result) && hasObjectRows(result)">
1900
+ <div class="result-card__grid">
1901
+ <div class="result-card__row" *ngFor="let row of getObjectRows(result)">
1902
+ <span class="result-card__label">
1903
+ {{ formatHeader(row.key) }}
1904
+ </span>
1905
+ <span class="result-card__value">
1906
+ {{ row.value }}
1907
+ </span>
1908
+ </div>
1909
+ </div>
1910
+ </div>
1911
+
1912
+ <div class="result-single" *ngIf="!isArrayResult(result) && isSingleValue(result)">
1913
+ <strong>Result</strong>
1914
+ <div class="result-single__value">
1915
+ {{ formatValue(result) }}
1916
+ </div>
1917
+ </div>
1918
+
1919
+ <details class="result-raw" *ngIf="result">
1920
+ <summary>Raw response</summary>
1921
+ <pre>{{ result | json }}</pre>
1922
+ </details>
1923
+ </ng-container>
1924
+ `}
1925
+ </div>
1926
+ `;
1927
+ }
1928
+ return `
1929
+ <div class="page">
1930
+ <ui-card [title]="schema.entity || schema.api.operationId" [subtitle]="schema.api.method.toUpperCase() + ' ' + schema.api.endpoint">
1931
+ <p class="screen-description" *ngIf="schema.description">
1932
+ {{ schema.description }}
1933
+ </p>
1934
+ <form [formGroup]="form" (ngSubmit)="submit()">
1935
+ <div class="form-grid">
1936
+ <div class="form-field" *ngFor="let field of formFields">
1937
+ <ui-select
1938
+ *ngIf="isSelect(field)"
1939
+ [label]="field.label || field.name"
1940
+ [hint]="field.hint"
1941
+ [info]="field.info"
1942
+ [controlName]="field.name"
1943
+ [options]="getSelectOptions(field)"
1944
+ [invalid]="isInvalid(field)"
1945
+ ></ui-select>
1946
+
1947
+ <ui-textarea
1948
+ *ngIf="isTextarea(field)"
1949
+ [label]="field.label || field.name"
1950
+ [hint]="field.hint"
1951
+ [info]="field.info"
1952
+ [controlName]="field.name"
1953
+ [rows]="3"
1954
+ [placeholder]="field.placeholder || field.label || field.name"
1955
+ [invalid]="isInvalid(field)"
1956
+ ></ui-textarea>
1957
+
1958
+ <ui-checkbox
1959
+ *ngIf="isCheckbox(field)"
1960
+ [label]="field.label || field.name"
1961
+ [hint]="field.hint"
1962
+ [info]="field.info"
1963
+ [controlName]="field.name"
1964
+ [invalid]="isInvalid(field)"
1965
+ ></ui-checkbox>
1966
+
1967
+ <ui-input
1968
+ *ngIf="!isSelect(field) && !isTextarea(field) && !isCheckbox(field)"
1969
+ [label]="field.label || field.name"
1970
+ [hint]="field.hint"
1971
+ [info]="field.info"
1972
+ [type]="inputType(field)"
1973
+ [controlName]="field.name"
1974
+ [placeholder]="field.placeholder || field.label || field.name"
1975
+ [invalid]="isInvalid(field)"
1976
+ ></ui-input>
1977
+
1978
+ <span class="field-error" *ngIf="isInvalid(field)">
1979
+ Required field
1980
+ </span>
1981
+ </div>
1982
+ </div>
1983
+ <div class="actions">
1984
+ <ui-button
1985
+ type="submit"
1986
+ variant="${buttonVariant}"
1987
+ [disabled]="form.invalid"
1988
+ >
1989
+ ${options.actionLabel}
1990
+ </ui-button>
1991
+ </div>
1992
+ </form>
1993
+ </ui-card>
1994
+
1995
+ ${useRaw ? `
1996
+ <ng-container *ngIf="error$ | async as error">
1997
+ <div class="result-error" *ngIf="error">
1998
+ <strong>Request failed.</strong>
1999
+ <div class="result-error__body">
2000
+ {{ formatError(error) }}
2001
+ </div>
2002
+ </div>
2003
+ </ng-container>
2004
+
2005
+ <ng-container *ngIf="result$ | async as result">
2006
+ <details class="result-raw" *ngIf="result">
2007
+ <summary>Raw response</summary>
2008
+ <pre>{{ result | json }}</pre>
2009
+ </details>
2010
+ </ng-container>
2011
+ ` : useCards ? `
2012
+ <ng-container *ngIf="error$ | async as error">
2013
+ <div class="result-error" *ngIf="error">
2014
+ <strong>Request failed.</strong>
2015
+ <div class="result-error__body">
2016
+ {{ formatError(error) }}
2017
+ </div>
2018
+ </div>
2019
+ </ng-container>
2020
+
2021
+ <ng-container *ngIf="result$ | async as result">
2022
+ <div class="result-cards" *ngIf="isArrayResult(result)">
2023
+ <article class="card-tile" *ngFor="let row of getRows(result)">
2024
+ <div class="card-media" *ngIf="getCardImage(row)">
2025
+ <img [src]="getCardImage(row)" [alt]="getCardTitle(row)" />
2026
+ </div>
2027
+ <div class="card-body">
2028
+ <h3 class="card-title">{{ getCardTitle(row) }}</h3>
2029
+ <p class="card-subtitle" *ngIf="getCardSubtitle(row)">
2030
+ {{ getCardSubtitle(row) }}
2031
+ </p>
2032
+ </div>
2033
+ </article>
2034
+ </div>
2035
+
2036
+ <details class="result-raw" *ngIf="result">
2037
+ <summary>Raw response</summary>
2038
+ <pre>{{ result | json }}</pre>
2039
+ </details>
2040
+ </ng-container>
2041
+ ` : `
2042
+ <ng-container *ngIf="error$ | async as error">
2043
+ <div class="result-error" *ngIf="error">
2044
+ <strong>Request failed.</strong>
2045
+ <div class="result-error__body">
2046
+ {{ formatError(error) }}
2047
+ </div>
2048
+ </div>
2049
+ </ng-container>
2050
+
2051
+ <ng-container *ngIf="result$ | async as result">
2052
+ <div class="result-table" *ngIf="isArrayResult(result)">
2053
+ <table class="data-table">
2054
+ <thead>
2055
+ <tr>
2056
+ <th *ngFor="let column of getColumns(result)">
2057
+ {{ formatHeader(column) }}
2058
+ </th>
2059
+ </tr>
2060
+ </thead>
2061
+ <tbody>
2062
+ <tr *ngFor="let row of getRows(result)">
2063
+ <td *ngFor="let column of getColumns(result)">
2064
+ <img
2065
+ *ngIf="isImageCell(row, column)"
2066
+ [src]="getCellValue(row, column)"
2067
+ [alt]="formatHeader(column)"
2068
+ class="cell-image"
2069
+ />
2070
+ <span *ngIf="!isImageCell(row, column)">
2071
+ {{ getCellValue(row, column) }}
2072
+ </span>
2073
+ </td>
2074
+ </tr>
2075
+ </tbody>
2076
+ </table>
2077
+ </div>
2078
+
2079
+ <div class="result-card" *ngIf="!isArrayResult(result) && hasObjectRows(result)">
2080
+ <div class="result-card__grid">
2081
+ <div class="result-card__row" *ngFor="let row of getObjectRows(result)">
2082
+ <span class="result-card__label">
2083
+ {{ formatHeader(row.key) }}
2084
+ </span>
2085
+ <span class="result-card__value">
2086
+ {{ row.value }}
2087
+ </span>
2088
+ </div>
2089
+ </div>
2090
+ </div>
2091
+
2092
+ <div class="result-single" *ngIf="!isArrayResult(result) && isSingleValue(result)">
2093
+ <strong>Result</strong>
2094
+ <div class="result-single__value">
2095
+ {{ formatValue(result) }}
2096
+ </div>
2097
+ </div>
2098
+
2099
+ <details class="result-raw" *ngIf="result">
2100
+ <summary>Raw response</summary>
2101
+ <pre>{{ result | json }}</pre>
2102
+ </details>
2103
+ </ng-container>
2104
+ `}
2105
+ </div>
2106
+ `;
2107
+ }
2108
+ function buildBaseScss() {
2109
+ return `
2110
+ :host {
2111
+ display: block;
2112
+ padding: 24px;
2113
+ min-height: 100vh;
2114
+ }
2115
+
2116
+ .page {
2117
+ display: grid;
2118
+ gap: 16px;
2119
+ min-height: calc(100vh - 48px);
2120
+ min-width: 0;
2121
+ }
2122
+
2123
+ .screen-description {
2124
+ margin: 0 0 18px;
2125
+ color: #6b7280;
2126
+ font-size: 14px;
2127
+ line-height: 1.5;
2128
+ }
2129
+
2130
+ .form-grid {
2131
+ display: grid;
2132
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
2133
+ gap: 16px;
2134
+ width: 100%;
2135
+ max-width: 960px;
2136
+ margin: 0 auto;
2137
+ min-width: 0;
2138
+ }
2139
+
2140
+ .form-field {
2141
+ display: grid;
2142
+ gap: 8px;
2143
+ min-width: 0;
2144
+ }
2145
+
2146
+ .field-error {
2147
+ color: #ef4444;
2148
+ font-size: 12px;
2149
+ margin-top: -4px;
2150
+ }
2151
+
2152
+ .actions {
2153
+ display: flex;
2154
+ justify-content: flex-end;
2155
+ gap: 14px;
2156
+ margin-top: 20px;
2157
+ flex-wrap: wrap;
2158
+ }
2159
+
2160
+ .result {
2161
+ margin-top: 20px;
2162
+ padding: 16px;
2163
+ border-radius: 12px;
2164
+ background: #0f172a;
2165
+ color: #e2e8f0;
2166
+ font-size: 12px;
2167
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.25);
2168
+ overflow: auto;
2169
+ }
2170
+
2171
+ .result-table {
2172
+ margin-top: 20px;
2173
+ max-width: 100%;
2174
+ overflow: auto;
2175
+ border-radius: 16px;
2176
+ border: 1px solid #e2e8f0;
2177
+ box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
2178
+ -webkit-overflow-scrolling: touch;
2179
+ }
2180
+
2181
+ .result-card {
2182
+ margin-top: 20px;
2183
+ border-radius: 16px;
2184
+ border: 1px solid #e2e8f0;
2185
+ background: #ffffff;
2186
+ box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
2187
+ padding: 18px;
2188
+ }
2189
+
2190
+ .result-card__grid {
2191
+ display: grid;
2192
+ gap: 12px;
2193
+ }
2194
+
2195
+ .result-card__row {
2196
+ display: flex;
2197
+ justify-content: space-between;
2198
+ gap: 16px;
2199
+ border-bottom: 1px solid #e2e8f0;
2200
+ padding-bottom: 10px;
2201
+ }
2202
+
2203
+ .result-card__row:last-child {
2204
+ border-bottom: none;
2205
+ padding-bottom: 0;
2206
+ }
2207
+
2208
+ .result-card__label {
2209
+ font-weight: 600;
2210
+ color: #475569;
2211
+ font-size: 12px;
2212
+ letter-spacing: 0.08em;
2213
+ text-transform: uppercase;
2214
+ }
2215
+
2216
+ .result-card__value {
2217
+ color: #0f172a;
2218
+ font-weight: 600;
2219
+ text-align: right;
2220
+ }
2221
+
2222
+ .result-cards {
2223
+ margin-top: 20px;
2224
+ display: grid;
2225
+ gap: 16px;
2226
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
2227
+ }
2228
+
2229
+ .card-tile {
2230
+ background: #ffffff;
2231
+ border-radius: 18px;
2232
+ border: 1px solid rgba(15, 23, 42, 0.08);
2233
+ box-shadow: 0 18px 32px rgba(15, 23, 42, 0.08);
2234
+ overflow: hidden;
2235
+ display: grid;
2236
+ gap: 12px;
2237
+ }
2238
+
2239
+ .card-media img {
2240
+ width: 100%;
2241
+ height: 160px;
2242
+ object-fit: cover;
2243
+ display: block;
2244
+ }
2245
+
2246
+ .card-body {
2247
+ padding: 14px 16px 16px;
2248
+ display: grid;
2249
+ gap: 8px;
2250
+ }
2251
+
2252
+ .card-header {
2253
+ display: flex;
2254
+ align-items: center;
2255
+ gap: 10px;
2256
+ }
2257
+
2258
+ .card-header input[type='checkbox'] {
2259
+ width: 22px;
2260
+ height: 22px;
2261
+ border-radius: 8px;
2262
+ border: 2px solid var(--color-border);
2263
+ background: #ffffff;
2264
+ box-shadow: 0 8px 18px rgba(99, 102, 241, 0.12);
2265
+ accent-color: var(--color-primary-strong);
2266
+ cursor: pointer;
2267
+ }
2268
+
2269
+ .card-title {
2270
+ margin: 0;
2271
+ font-size: 16px;
2272
+ font-weight: 700;
2273
+ color: #0f172a;
2274
+ }
2275
+
2276
+ .card-subtitle {
2277
+ margin: 0;
2278
+ font-size: 13px;
2279
+ color: #64748b;
2280
+ }
2281
+
2282
+ .card-actions {
2283
+ display: inline-flex;
2284
+ flex-wrap: wrap;
2285
+ gap: 10px;
2286
+ }
2287
+
2288
+ .card-actions .link {
2289
+ border: 1px solid rgba(15, 23, 42, 0.12);
2290
+ background: #ffffff;
2291
+ color: #0f172a;
2292
+ font-weight: 600;
2293
+ font-size: 12px;
2294
+ padding: 6px 12px;
2295
+ border-radius: 999px;
2296
+ cursor: pointer;
2297
+ transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
2298
+ }
2299
+
2300
+ .card-actions .link:hover {
2301
+ background: #f8fafc;
2302
+ box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
2303
+ transform: translateY(-1px);
2304
+ }
2305
+
2306
+ .card-actions .link.danger {
2307
+ border-color: rgba(239, 68, 68, 0.35);
2308
+ color: #b91c1c;
2309
+ background: #fff1f2;
2310
+ }
2311
+
2312
+ .card-actions .link.danger:hover {
2313
+ background: #ffe4e6;
2314
+ }
2315
+
2316
+ .result-error {
2317
+ margin-top: 24px;
2318
+ padding: 16px 18px;
2319
+ border-radius: 16px;
2320
+ border: 1px solid rgba(239, 68, 68, 0.3);
2321
+ background: #fff1f2;
2322
+ color: #881337;
2323
+ box-shadow: 0 10px 24px rgba(239, 68, 68, 0.15);
2324
+ display: grid;
2325
+ gap: 8px;
2326
+ }
2327
+
2328
+ .result-error__body {
2329
+ font-size: 13px;
2330
+ color: #7f1d1d;
2331
+ word-break: break-word;
2332
+ }
2333
+
2334
+ .result-raw {
2335
+ margin-top: 24px;
2336
+ padding: 16px 18px;
2337
+ border-radius: 16px;
2338
+ border: 1px dashed rgba(15, 23, 42, 0.18);
2339
+ background: #f8fafc;
2340
+ color: #0f172a;
2341
+ display: grid;
2342
+ gap: 10px;
2343
+ }
2344
+
2345
+ .result-raw summary {
2346
+ cursor: pointer;
2347
+ font-weight: 600;
2348
+ color: #334155;
2349
+ }
2350
+
2351
+ .result-raw pre {
2352
+ margin: 0;
2353
+ padding: 12px;
2354
+ border-radius: 12px;
2355
+ background: #ffffff;
2356
+ border: 1px solid rgba(15, 23, 42, 0.08);
666
2357
  font-size: 12px;
667
2358
  line-height: 1.4;
668
2359
  white-space: pre-wrap;
@@ -739,343 +2430,27 @@ export class ${name}Service {
739
2430
  min-height: 0;
740
2431
  }
741
2432
 
742
- @media (max-width: 720px) {
743
- :host {
744
- padding: 18px;
745
- }
746
-
2433
+ @media (max-width: 1024px) {
747
2434
  .form-grid {
748
- grid-template-columns: 1fr;
2435
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
749
2436
  max-width: 100%;
750
2437
  }
751
-
752
- .actions {
753
- justify-content: stretch;
754
- }
755
- }
756
- `);
757
- return {
758
- path: toRouteSegment(name),
759
- component: `${name}Component`,
760
- folder,
761
- fileBase
762
- };
763
- }
764
- function normalizeFields(fields) {
765
- return fields.map(field => ({
766
- name: field.name,
767
- type: field.type || 'string',
768
- required: Boolean(field.required),
769
- label: field.label || toLabel(field.name),
770
- placeholder: field.placeholder || toPlaceholder(field.name),
771
- hint: field.hint || undefined,
772
- info: field.info || undefined,
773
- options: field.options || null,
774
- defaultValue: field.defaultValue ?? null,
775
- source: 'body'
776
- }));
777
- }
778
- function normalizeQueryParams(params) {
779
- return params.map(param => {
780
- const labelText = param.label || param.name;
781
- const hintText = param.hint ||
782
- (typeof labelText === 'string' && labelText.length > 60
783
- ? labelText
784
- : '');
785
- const help = resolveFieldHelp(hintText, labelText);
786
- return {
787
- name: param.name,
788
- type: param.type || 'string',
789
- required: Boolean(param.required),
790
- label: toLabel(param.name),
791
- placeholder: param.placeholder || toPlaceholder(param.name),
792
- hint: help.hint,
793
- info: help.info,
794
- options: param.options || null,
795
- defaultValue: param.defaultValue ?? null,
796
- source: 'query'
797
- };
798
- });
799
- }
800
- function extractPathParams(endpoint) {
801
- const params = [];
802
- const regex = /{([^}]+)}/g;
803
- let match = regex.exec(endpoint);
804
- while (match) {
805
- params.push(match[1]);
806
- match = regex.exec(endpoint);
807
- }
808
- return params;
809
- }
810
- function buildFormControls(fields) {
811
- if (fields.length === 0)
812
- return '';
813
- return fields
814
- .map(field => {
815
- const value = field.defaultValue !== null && field.defaultValue !== undefined
816
- ? JSON.stringify(field.defaultValue)
817
- : defaultValueFor(field.type);
818
- const validators = field.required ? ', Validators.required' : '';
819
- return ` ${field.name}: [${value}${validators}]`;
820
- })
821
- .join(',\n');
822
- }
823
- function defaultValueFor(type) {
824
- switch (type) {
825
- case 'array':
826
- return '[]';
827
- case 'boolean':
828
- return 'false';
829
- case 'number':
830
- case 'integer':
831
- return 'null';
832
- default:
833
- return "''";
834
- }
835
- }
836
- function toLabel(value) {
837
- return String(value)
838
- .replace(/[_-]/g, ' ')
839
- .replace(/([a-z])([A-Z])/g, '$1 $2')
840
- .replace(/\b\w/g, char => char.toUpperCase());
841
- }
842
- function toPlaceholder(value) {
843
- return toLabel(value);
844
- }
845
- function normalizeWhitespace(value) {
846
- return String(value).replace(/\s+/g, ' ').trim();
847
- }
848
- function resolveFieldHelp(rawHint, label) {
849
- const hint = normalizeWhitespace(rawHint || '');
850
- if (!hint)
851
- return { hint: undefined, info: undefined };
852
- if (hint.length > 120) {
853
- return { hint: undefined, info: hint };
854
- }
855
- return { hint, info: undefined };
856
- }
857
- function escapeAttr(value) {
858
- return String(value).replace(/"/g, '&quot;');
859
- }
860
- function defaultActionLabel(method, hasParams) {
861
- switch (method) {
862
- case 'get':
863
- return hasParams ? 'Buscar' : 'Carregar';
864
- case 'post':
865
- return 'Criar';
866
- case 'put':
867
- case 'patch':
868
- return 'Salvar';
869
- case 'delete':
870
- return 'Excluir';
871
- default:
872
- return 'Executar';
873
- }
874
2438
  }
875
- function buildComponentHtml(options) {
876
- const buttonVariant = options.method === 'delete' ? 'danger' : 'primary';
877
- if (!options.hasForm) {
878
- return `
879
- <div class="page">
880
- <ui-card title="${options.title}" subtitle="${options.subtitle}">
881
- <div class="actions">
882
- <ui-button
883
- variant="${buttonVariant}"
884
- [disabled]="form.invalid"
885
- (click)="submit()"
886
- >
887
- ${options.actionLabel}
888
- </ui-button>
889
- </div>
890
- </ui-card>
891
-
892
- <div class="result-table" *ngIf="isArrayResult()">
893
- <table class="data-table">
894
- <thead>
895
- <tr>
896
- <th *ngFor="let column of getColumns()">
897
- {{ formatHeader(column) }}
898
- </th>
899
- </tr>
900
- </thead>
901
- <tbody>
902
- <tr *ngFor="let row of getRows()">
903
- <td *ngFor="let column of getColumns()">
904
- <img
905
- *ngIf="isImageCell(row, column)"
906
- [src]="getCellValue(row, column)"
907
- [alt]="formatHeader(column)"
908
- class="cell-image"
909
- />
910
- <span *ngIf="!isImageCell(row, column)">
911
- {{ getCellValue(row, column) }}
912
- </span>
913
- </td>
914
- </tr>
915
- </tbody>
916
- </table>
917
- </div>
918
-
919
- <div class="result-error" *ngIf="error">
920
- <strong>Request failed.</strong>
921
- <div class="result-error__body">
922
- {{ error?.message || (error | json) }}
923
- </div>
924
- </div>
925
-
926
- <div class="result-card" *ngIf="!isArrayResult() && hasObjectRows()">
927
- <div class="result-card__grid">
928
- <div class="result-card__row" *ngFor="let row of getObjectRows()">
929
- <span class="result-card__label">
930
- {{ formatHeader(row.key) }}
931
- </span>
932
- <span class="result-card__value">
933
- {{ row.value }}
934
- </span>
935
- </div>
936
- </div>
937
- </div>
938
-
939
- <div class="result-single" *ngIf="!isArrayResult() && isSingleValue()">
940
- <strong>Result</strong>
941
- <div class="result-single__value">
942
- {{ formatValue(result) }}
943
- </div>
944
- </div>
945
-
946
- <details class="result-raw" *ngIf="result">
947
- <summary>Raw response</summary>
948
- <pre>{{ result | json }}</pre>
949
- </details>
950
- </div>
951
- `;
952
- }
953
- return `
954
- <div class="page">
955
- <ui-card [title]="schema.entity || schema.api.operationId" [subtitle]="schema.api.method.toUpperCase() + ' ' + schema.api.endpoint">
956
- <p class="screen-description" *ngIf="schema.description">
957
- {{ schema.description }}
958
- </p>
959
- <form [formGroup]="form" (ngSubmit)="submit()">
960
- <div class="form-grid">
961
- <div class="form-field" *ngFor="let field of formFields">
962
- <ui-select
963
- *ngIf="isSelect(field)"
964
- [label]="field.label || field.name"
965
- [hint]="field.hint"
966
- [info]="field.info"
967
- [controlName]="field.name"
968
- [options]="getSelectOptions(field)"
969
- [invalid]="isInvalid(field)"
970
- ></ui-select>
971
-
972
- <ui-textarea
973
- *ngIf="isTextarea(field)"
974
- [label]="field.label || field.name"
975
- [hint]="field.hint"
976
- [info]="field.info"
977
- [controlName]="field.name"
978
- [rows]="3"
979
- [placeholder]="field.placeholder || field.label || field.name"
980
- [invalid]="isInvalid(field)"
981
- ></ui-textarea>
982
-
983
- <ui-checkbox
984
- *ngIf="isCheckbox(field)"
985
- [label]="field.label || field.name"
986
- [hint]="field.hint"
987
- [info]="field.info"
988
- [controlName]="field.name"
989
- [invalid]="isInvalid(field)"
990
- ></ui-checkbox>
991
-
992
- <ui-input
993
- *ngIf="!isSelect(field) && !isTextarea(field) && !isCheckbox(field)"
994
- [label]="field.label || field.name"
995
- [hint]="field.hint"
996
- [info]="field.info"
997
- [type]="inputType(field)"
998
- [controlName]="field.name"
999
- [placeholder]="field.placeholder || field.label || field.name"
1000
- [invalid]="isInvalid(field)"
1001
- ></ui-input>
1002
-
1003
- <span class="field-error" *ngIf="isInvalid(field)">
1004
- Campo obrigatório
1005
- </span>
1006
- </div>
1007
- </div>
1008
- <div class="actions">
1009
- <ui-button
1010
- type="submit"
1011
- variant="${buttonVariant}"
1012
- [disabled]="form.invalid"
1013
- >
1014
- ${options.actionLabel}
1015
- </ui-button>
1016
- </div>
1017
- </form>
1018
- </ui-card>
1019
-
1020
- <div class="result-table" *ngIf="isArrayResult()">
1021
- <table class="data-table">
1022
- <thead>
1023
- <tr>
1024
- <th *ngFor="let column of getColumns()">
1025
- {{ formatHeader(column) }}
1026
- </th>
1027
- </tr>
1028
- </thead>
1029
- <tbody>
1030
- <tr *ngFor="let row of getRows()">
1031
- <td *ngFor="let column of getColumns()">
1032
- <img
1033
- *ngIf="isImageCell(row, column)"
1034
- [src]="getCellValue(row, column)"
1035
- [alt]="formatHeader(column)"
1036
- class="cell-image"
1037
- />
1038
- <span *ngIf="!isImageCell(row, column)">
1039
- {{ getCellValue(row, column) }}
1040
- </span>
1041
- </td>
1042
- </tr>
1043
- </tbody>
1044
- </table>
1045
- </div>
1046
-
1047
- <div class="result-error" *ngIf="error">
1048
- <strong>Request failed.</strong>
1049
- <div class="result-error__body">
1050
- {{ error?.message || (error | json) }}
1051
- </div>
1052
- </div>
1053
2439
 
1054
- <div class="result-card" *ngIf="!isArrayResult() && hasObjectRows()">
1055
- <div class="result-card__grid">
1056
- <div class="result-card__row" *ngFor="let row of getObjectRows()">
1057
- <span class="result-card__label">
1058
- {{ formatHeader(row.key) }}
1059
- </span>
1060
- <span class="result-card__value">
1061
- {{ row.value }}
1062
- </span>
1063
- </div>
1064
- </div>
1065
- </div>
2440
+ @media (max-width: 820px) {
2441
+ :host {
2442
+ padding: 18px;
2443
+ }
1066
2444
 
1067
- <div class="result-single" *ngIf="!isArrayResult() && isSingleValue()">
1068
- <strong>Result</strong>
1069
- <div class="result-single__value">
1070
- {{ formatValue(result) }}
1071
- </div>
1072
- </div>
2445
+ .form-grid {
2446
+ grid-template-columns: 1fr;
2447
+ max-width: 100%;
2448
+ }
1073
2449
 
1074
- <details class="result-raw" *ngIf="result">
1075
- <summary>Raw response</summary>
1076
- <pre>{{ result | json }}</pre>
1077
- </details>
1078
- </div>
2450
+ .actions {
2451
+ justify-content: stretch;
2452
+ }
2453
+ }
1079
2454
  `;
1080
2455
  }
1081
2456
  function buildFieldHtml(field) {
@@ -1146,11 +2521,11 @@ export class UiCardComponent {
1146
2521
  }
1147
2522
 
1148
2523
  .ui-card {
1149
- border-radius: 22px;
1150
- background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
1151
- border: 1px solid rgba(15, 23, 42, 0.08);
1152
- box-shadow: var(--shadow-card);
1153
- padding: 30px;
2524
+ border-radius: 12px;
2525
+ background: #ffffff;
2526
+ border: 1px solid #e2e8f0;
2527
+ box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
2528
+ padding: 24px;
1154
2529
  position: relative;
1155
2530
  overflow: hidden;
1156
2531
  }
@@ -1159,9 +2534,9 @@ export class UiCardComponent {
1159
2534
  content: "";
1160
2535
  position: absolute;
1161
2536
  inset: 0 0 auto 0;
1162
- height: 6px;
1163
- background: linear-gradient(90deg, var(--color-primary), var(--color-primary-strong), var(--color-accent));
1164
- opacity: 0.65;
2537
+ height: 3px;
2538
+ background: linear-gradient(90deg, #1d4ed8, #2563eb);
2539
+ opacity: 0.85;
1165
2540
  }
1166
2541
 
1167
2542
  .ui-card__header {
@@ -1170,19 +2545,20 @@ export class UiCardComponent {
1170
2545
 
1171
2546
  .ui-card__title {
1172
2547
  margin: 0;
1173
- font-size: 26px;
1174
- font-weight: 700;
1175
- color: var(--bg-ink);
2548
+ font-size: 24px;
2549
+ font-weight: 600;
2550
+ color: #0f172a;
1176
2551
  letter-spacing: -0.02em;
2552
+ font-family: "Instrument Serif", Georgia, serif;
1177
2553
  }
1178
2554
 
1179
2555
  .ui-card__subtitle {
1180
2556
  margin: 8px 0 0;
1181
- font-size: 13px;
1182
- color: var(--color-muted);
1183
- letter-spacing: 0.16em;
2557
+ font-size: 11px;
2558
+ color: #64748b;
2559
+ letter-spacing: 0.14em;
1184
2560
  text-transform: uppercase;
1185
- font-family: "Space Mono", "Courier New", monospace;
2561
+ font-family: "DM Sans", "Segoe UI", sans-serif;
1186
2562
  }
1187
2563
  `
1188
2564
  },
@@ -1218,7 +2594,7 @@ export class UiCardComponent {
1218
2594
  class="ui-menu__group"
1219
2595
  *ngIf="menu.ungrouped.length"
1220
2596
  >
1221
- <h3 class="ui-menu__group-title">Outros</h3>
2597
+ <h3 class="ui-menu__group-title">Other</h3>
1222
2598
  <a
1223
2599
  class="ui-menu__item"
1224
2600
  *ngFor="let item of menu.ungrouped"
@@ -1241,10 +2617,10 @@ export class UiCardComponent {
1241
2617
  position: sticky;
1242
2618
  top: 24px;
1243
2619
  align-self: flex-start;
1244
- background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
1245
- border: 1px solid rgba(15, 23, 42, 0.08);
1246
- box-shadow: var(--shadow-card);
1247
- border-radius: 22px;
2620
+ background: #ffffff;
2621
+ border: 1px solid #e2e8f0;
2622
+ box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
2623
+ border-radius: 12px;
1248
2624
  padding: 22px;
1249
2625
  min-width: 220px;
1250
2626
  display: grid;
@@ -1256,7 +2632,7 @@ export class UiCardComponent {
1256
2632
  align-items: center;
1257
2633
  gap: 10px;
1258
2634
  font-weight: 700;
1259
- color: var(--bg-ink);
2635
+ color: #0f172a;
1260
2636
  font-size: 15px;
1261
2637
  }
1262
2638
 
@@ -1264,8 +2640,8 @@ export class UiCardComponent {
1264
2640
  width: 14px;
1265
2641
  height: 14px;
1266
2642
  border-radius: 999px;
1267
- background: linear-gradient(135deg, var(--color-primary), var(--color-primary-strong));
1268
- box-shadow: 0 6px 16px rgba(8, 145, 178, 0.35);
2643
+ background: linear-gradient(135deg, #1d4ed8, #2563eb);
2644
+ box-shadow: 0 8px 20px rgba(37, 99, 235, 0.24);
1269
2645
  }
1270
2646
 
1271
2647
  .ui-menu__group {
@@ -1276,10 +2652,10 @@ export class UiCardComponent {
1276
2652
  .ui-menu__group-title {
1277
2653
  margin: 0;
1278
2654
  font-size: 11px;
1279
- letter-spacing: 0.3em;
2655
+ letter-spacing: 0.16em;
1280
2656
  text-transform: uppercase;
1281
- color: var(--color-muted);
1282
- font-family: "Space Mono", "Courier New", monospace;
2657
+ color: #94a3b8;
2658
+ font-family: "DM Sans", "Segoe UI", sans-serif;
1283
2659
  }
1284
2660
 
1285
2661
  .ui-menu__item {
@@ -1289,8 +2665,8 @@ export class UiCardComponent {
1289
2665
  font-weight: 600;
1290
2666
  color: #1f2937;
1291
2667
  padding: 10px 14px;
1292
- border-radius: 14px;
1293
- transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
2668
+ border-radius: 8px;
2669
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease;
1294
2670
  border: 1px solid transparent;
1295
2671
  white-space: nowrap;
1296
2672
  overflow: hidden;
@@ -1298,14 +2674,15 @@ export class UiCardComponent {
1298
2674
  }
1299
2675
 
1300
2676
  .ui-menu__item:hover {
1301
- background: #f1f5f9;
2677
+ background: #eff6ff;
1302
2678
  transform: translateX(2px);
2679
+ color: #1d4ed8;
1303
2680
  }
1304
2681
 
1305
2682
  .ui-menu__item.active {
1306
- background: linear-gradient(135deg, var(--color-primary), var(--color-primary-strong));
2683
+ background: #1e293b;
1307
2684
  color: #ffffff;
1308
- box-shadow: 0 12px 24px rgba(8, 145, 178, 0.3);
2685
+ box-shadow: 0 10px 24px rgba(30, 41, 59, 0.24);
1309
2686
  }
1310
2687
 
1311
2688
  .ui-menu__item.hidden {
@@ -1378,6 +2755,7 @@ export class UiFieldComponent {
1378
2755
  gap: 10px;
1379
2756
  font-size: 13px;
1380
2757
  color: #1f2937;
2758
+ min-width: 0;
1381
2759
  }
1382
2760
 
1383
2761
  .ui-field__label {
@@ -1393,14 +2771,14 @@ export class UiFieldComponent {
1393
2771
  }
1394
2772
 
1395
2773
  .ui-field__info {
1396
- margin-left: 8px;
1397
- width: 18px;
1398
- height: 18px;
2774
+ margin-left: 6px;
2775
+ width: 16px;
2776
+ height: 16px;
1399
2777
  border-radius: 999px;
1400
- border: 1px solid rgba(15, 23, 42, 0.2);
2778
+ border: 1px solid var(--color-border);
1401
2779
  background: #ffffff;
1402
- color: #475569;
1403
- font-size: 11px;
2780
+ color: var(--color-primary-strong);
2781
+ font-size: 10px;
1404
2782
  line-height: 1;
1405
2783
  display: inline-flex;
1406
2784
  align-items: center;
@@ -1409,30 +2787,34 @@ export class UiFieldComponent {
1409
2787
  }
1410
2788
 
1411
2789
  .ui-field__info:hover {
1412
- background: #f8fafc;
2790
+ background: #ffffff;
1413
2791
  }
1414
2792
 
1415
2793
  .ui-field__info-panel {
1416
2794
  margin-top: 8px;
1417
2795
  padding: 10px 12px;
1418
- border-radius: 10px;
1419
- background: #f8fafc;
1420
- border: 1px solid #e2e8f0;
2796
+ border-radius: 14px;
2797
+ background: #ffffff;
2798
+ border: 1px solid var(--color-border);
1421
2799
  color: #475569;
1422
2800
  font-size: 12px;
1423
2801
  line-height: 1.4;
2802
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
1424
2803
  }
1425
2804
 
1426
2805
  :host ::ng-deep input,
1427
2806
  :host ::ng-deep textarea {
1428
2807
  width: 100%;
1429
- min-height: 3.4rem;
1430
- border-radius: 10px;
1431
- border: 1px solid rgba(15, 23, 42, 0.12);
2808
+ max-width: 100%;
2809
+ min-width: 0;
2810
+ min-height: 2.9rem;
2811
+ border-radius: 16px;
2812
+ border: 1px solid var(--color-border);
1432
2813
  background: #ffffff;
1433
- padding: 0.9rem 1.1rem;
1434
- font-size: 15px;
2814
+ padding: 0.7rem 0.95rem;
2815
+ font-size: 14px;
1435
2816
  font-weight: 500;
2817
+ box-sizing: border-box;
1436
2818
  box-shadow: none;
1437
2819
  outline: none;
1438
2820
  transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
@@ -1440,15 +2822,15 @@ export class UiFieldComponent {
1440
2822
 
1441
2823
  :host ::ng-deep input:focus,
1442
2824
  :host ::ng-deep textarea:focus {
1443
- border-color: var(--color-primary);
1444
- box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
2825
+ border-color: var(--color-primary-strong);
2826
+ box-shadow: 0 0 0 4px var(--color-primary-soft);
1445
2827
  transform: translateY(-1px);
1446
2828
  }
1447
2829
 
1448
2830
  :host ::ng-deep input.invalid,
1449
2831
  :host ::ng-deep textarea.invalid {
1450
- border-color: #ef4444;
1451
- box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
2832
+ border-color: var(--color-accent);
2833
+ box-shadow: 0 0 0 3px var(--color-accent-soft);
1452
2834
  }
1453
2835
 
1454
2836
  :host ::ng-deep input::placeholder,
@@ -1457,12 +2839,15 @@ export class UiFieldComponent {
1457
2839
  }
1458
2840
 
1459
2841
  :host ::ng-deep input[type='checkbox'] {
1460
- width: 20px;
1461
- height: 20px;
2842
+ width: 22px;
2843
+ height: 22px;
1462
2844
  padding: 0;
1463
- border-radius: 6px;
1464
- box-shadow: none;
1465
- accent-color: var(--color-primary);
2845
+ border-radius: 8px;
2846
+ border: 2px solid var(--color-border);
2847
+ background: #ffffff;
2848
+ box-shadow: 0 8px 18px rgba(99, 102, 241, 0.12);
2849
+ accent-color: var(--color-primary-strong);
2850
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
1466
2851
  }
1467
2852
 
1468
2853
  .field-error {
@@ -1503,31 +2888,32 @@ export class UiButtonComponent {
1503
2888
  `,
1504
2889
  scss: `
1505
2890
  .ui-button {
1506
- border: none;
1507
- border-radius: 999px;
1508
- padding: 12px 24px;
1509
- font-weight: 700;
1510
- font-size: 14px;
1511
- letter-spacing: 0.02em;
2891
+ border: 1px solid transparent;
2892
+ border-radius: 8px;
2893
+ padding: 10px 16px;
2894
+ font-weight: 600;
2895
+ font-size: 13px;
2896
+ letter-spacing: 0.01em;
1512
2897
  cursor: pointer;
1513
2898
  transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
1514
2899
  }
1515
2900
 
1516
2901
  .ui-button.primary {
1517
- background: linear-gradient(135deg, var(--color-primary), var(--color-primary-strong));
2902
+ background: #2563eb;
1518
2903
  color: #ffffff;
1519
- box-shadow: 0 12px 24px rgba(8, 145, 178, 0.3);
2904
+ box-shadow: 0 8px 20px rgba(37, 99, 235, 0.24);
1520
2905
  }
1521
2906
 
1522
2907
  .ui-button.ghost {
1523
- background: #f9fafb;
1524
- color: #111827;
2908
+ background: #ffffff;
2909
+ color: #334155;
2910
+ border-color: #cbd5e1;
1525
2911
  }
1526
2912
 
1527
2913
  .ui-button.danger {
1528
- background: linear-gradient(135deg, #ef4444, #f97316);
2914
+ background: #dc2626;
1529
2915
  color: #fff;
1530
- box-shadow: 0 10px 22px rgba(239, 68, 68, 0.25);
2916
+ box-shadow: 0 8px 20px rgba(220, 38, 38, 0.22);
1531
2917
  }
1532
2918
 
1533
2919
  .ui-button:hover:not(:disabled) {
@@ -1540,6 +2926,211 @@ export class UiButtonComponent {
1540
2926
  cursor: not-allowed;
1541
2927
  box-shadow: none;
1542
2928
  }
2929
+ `
2930
+ },
2931
+ {
2932
+ name: 'ui-search',
2933
+ template: `
2934
+ import { Component, EventEmitter, Input, Output } from '@angular/core'
2935
+
2936
+ @Component({
2937
+ selector: 'ui-search',
2938
+ standalone: true,
2939
+ templateUrl: './ui-search.component.html',
2940
+ styleUrls: ['./ui-search.component.scss']
2941
+ })
2942
+ export class UiSearchComponent {
2943
+ @Input() value = ''
2944
+ @Input() placeholder = 'Search'
2945
+ @Output() valueChange = new EventEmitter<string>()
2946
+
2947
+ onInput(event: Event) {
2948
+ const target = event.target as HTMLInputElement | null
2949
+ this.valueChange.emit(target?.value ?? '')
2950
+ }
2951
+ }
2952
+ `,
2953
+ html: `
2954
+ <label class="ui-search">
2955
+ <span class="ui-search__icon" aria-hidden="true">⌕</span>
2956
+ <input
2957
+ class="ui-search__input"
2958
+ type="search"
2959
+ [value]="value"
2960
+ [placeholder]="placeholder"
2961
+ (input)="onInput($event)"
2962
+ />
2963
+ </label>
2964
+ `,
2965
+ scss: `
2966
+ :host {
2967
+ display: block;
2968
+ width: 100%;
2969
+ }
2970
+
2971
+ .ui-search {
2972
+ display: flex;
2973
+ align-items: center;
2974
+ gap: 8px;
2975
+ min-height: 40px;
2976
+ border-radius: 10px;
2977
+ border: 1px solid #d1d5db;
2978
+ background: #ffffff;
2979
+ padding: 0 10px;
2980
+ }
2981
+
2982
+ .ui-search:focus-within {
2983
+ border-color: #2563eb;
2984
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
2985
+ }
2986
+
2987
+ .ui-search__icon {
2988
+ color: #64748b;
2989
+ font-size: 13px;
2990
+ }
2991
+
2992
+ .ui-search__input {
2993
+ border: none;
2994
+ outline: none;
2995
+ width: 100%;
2996
+ font-size: 13px;
2997
+ color: #0f172a;
2998
+ background: transparent;
2999
+ }
3000
+
3001
+ .ui-search__input::placeholder {
3002
+ color: #94a3b8;
3003
+ }
3004
+ `
3005
+ },
3006
+ {
3007
+ name: 'ui-stat-card',
3008
+ template: `
3009
+ import { Component, Input } from '@angular/core'
3010
+ import { NgIf } from '@angular/common'
3011
+
3012
+ @Component({
3013
+ selector: 'ui-stat-card',
3014
+ standalone: true,
3015
+ imports: [NgIf],
3016
+ templateUrl: './ui-stat-card.component.html',
3017
+ styleUrls: ['./ui-stat-card.component.scss']
3018
+ })
3019
+ export class UiStatCardComponent {
3020
+ @Input() title = ''
3021
+ @Input() value = ''
3022
+ @Input() meta = ''
3023
+ }
3024
+ `,
3025
+ html: `
3026
+ <section class="ui-stat-card">
3027
+ <div class="ui-stat-card__top">
3028
+ <div class="ui-stat-card__title">{{ title }}</div>
3029
+ <div class="ui-stat-card__icon">
3030
+ <ng-content></ng-content>
3031
+ </div>
3032
+ </div>
3033
+ <div class="ui-stat-card__value">{{ value }}</div>
3034
+ <div class="ui-stat-card__meta" *ngIf="meta">{{ meta }}</div>
3035
+ </section>
3036
+ `,
3037
+ scss: `
3038
+ :host {
3039
+ display: block;
3040
+ }
3041
+
3042
+ .ui-stat-card {
3043
+ border-radius: 12px;
3044
+ background: var(--bg-surface);
3045
+ border: 1px solid var(--color-border);
3046
+ padding: 16px;
3047
+ box-shadow: var(--shadow-card);
3048
+ display: grid;
3049
+ gap: 8px;
3050
+ }
3051
+
3052
+ .ui-stat-card__top {
3053
+ display: flex;
3054
+ align-items: center;
3055
+ justify-content: space-between;
3056
+ gap: 10px;
3057
+ }
3058
+
3059
+ .ui-stat-card__title {
3060
+ font-size: 12px;
3061
+ color: var(--color-muted);
3062
+ }
3063
+
3064
+ .ui-stat-card__value {
3065
+ font-size: 22px;
3066
+ font-weight: 700;
3067
+ color: var(--bg-ink);
3068
+ }
3069
+
3070
+ .ui-stat-card__meta {
3071
+ font-size: 11px;
3072
+ color: var(--color-muted);
3073
+ }
3074
+ `
3075
+ },
3076
+ {
3077
+ name: 'ui-badge',
3078
+ template: `
3079
+ import { Component, Input } from '@angular/core'
3080
+ import { NgClass } from '@angular/common'
3081
+
3082
+ @Component({
3083
+ selector: 'ui-badge',
3084
+ standalone: true,
3085
+ imports: [NgClass],
3086
+ templateUrl: './ui-badge.component.html',
3087
+ styleUrls: ['./ui-badge.component.scss']
3088
+ })
3089
+ export class UiBadgeComponent {
3090
+ @Input() variant: 'success' | 'warning' | 'danger' | 'neutral' = 'neutral'
3091
+ }
3092
+ `,
3093
+ html: `
3094
+ <span class="ui-badge" [ngClass]="variant">
3095
+ <ng-content></ng-content>
3096
+ </span>
3097
+ `,
3098
+ scss: `
3099
+ :host {
3100
+ display: inline-flex;
3101
+ }
3102
+
3103
+ .ui-badge {
3104
+ display: inline-flex;
3105
+ align-items: center;
3106
+ justify-content: center;
3107
+ border-radius: 999px;
3108
+ padding: 4px 10px;
3109
+ font-size: 11px;
3110
+ font-weight: 700;
3111
+ letter-spacing: 0.02em;
3112
+ text-transform: uppercase;
3113
+ }
3114
+
3115
+ .ui-badge.neutral {
3116
+ color: #334155;
3117
+ background: #eef2ff;
3118
+ }
3119
+
3120
+ .ui-badge.success {
3121
+ color: #047857;
3122
+ background: #d1fae5;
3123
+ }
3124
+
3125
+ .ui-badge.warning {
3126
+ color: #92400e;
3127
+ background: #fef3c7;
3128
+ }
3129
+
3130
+ .ui-badge.danger {
3131
+ color: #991b1b;
3132
+ background: #fee2e2;
3133
+ }
1543
3134
  `
1544
3135
  },
1545
3136
  {
@@ -1617,6 +3208,7 @@ export class UiInputComponent {
1617
3208
  gap: 10px;
1618
3209
  font-size: 13px;
1619
3210
  color: #1f2937;
3211
+ min-width: 0;
1620
3212
  }
1621
3213
 
1622
3214
  .ui-control__label {
@@ -1632,14 +3224,14 @@ export class UiInputComponent {
1632
3224
  }
1633
3225
 
1634
3226
  .ui-control__info {
1635
- margin-left: 8px;
1636
- width: 18px;
1637
- height: 18px;
3227
+ margin-left: 6px;
3228
+ width: 16px;
3229
+ height: 16px;
1638
3230
  border-radius: 999px;
1639
- border: 1px solid rgba(15, 23, 42, 0.2);
3231
+ border: 1px solid var(--color-border);
1640
3232
  background: #ffffff;
1641
- color: #475569;
1642
- font-size: 11px;
3233
+ color: var(--color-primary-strong);
3234
+ font-size: 10px;
1643
3235
  line-height: 1;
1644
3236
  display: inline-flex;
1645
3237
  align-items: center;
@@ -1648,43 +3240,47 @@ export class UiInputComponent {
1648
3240
  }
1649
3241
 
1650
3242
  .ui-control__info:hover {
1651
- background: #f8fafc;
3243
+ background: #ffffff;
1652
3244
  }
1653
3245
 
1654
3246
  .ui-control__info-panel {
1655
3247
  margin-top: 8px;
1656
3248
  padding: 10px 12px;
1657
- border-radius: 10px;
1658
- background: #f8fafc;
1659
- border: 1px solid #e2e8f0;
3249
+ border-radius: 14px;
3250
+ background: #ffffff;
3251
+ border: 1px solid var(--color-border);
1660
3252
  color: #475569;
1661
3253
  font-size: 12px;
1662
3254
  line-height: 1.4;
3255
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
1663
3256
  }
1664
3257
 
1665
3258
  .ui-control__input {
1666
3259
  width: 100%;
1667
- min-height: 3.4rem;
1668
- border-radius: 10px;
1669
- border: 1px solid rgba(15, 23, 42, 0.12);
3260
+ max-width: 100%;
3261
+ min-width: 0;
3262
+ min-height: 2.9rem;
3263
+ border-radius: 14px;
3264
+ border: 1px solid var(--color-border);
1670
3265
  background: #ffffff;
1671
- padding: 0.9rem 1.1rem;
1672
- font-size: 15px;
3266
+ padding: 0.7rem 0.95rem;
3267
+ font-size: 14px;
1673
3268
  font-weight: 500;
3269
+ box-sizing: border-box;
1674
3270
  box-shadow: none;
1675
3271
  outline: none;
1676
3272
  transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
1677
3273
  }
1678
3274
 
1679
3275
  .ui-control__input:focus {
1680
- border-color: var(--color-primary);
1681
- box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
3276
+ border-color: var(--color-primary-strong);
3277
+ box-shadow: 0 0 0 4px var(--color-primary-soft);
1682
3278
  transform: translateY(-1px);
1683
3279
  }
1684
3280
 
1685
3281
  .ui-control__input.invalid {
1686
- border-color: #ef4444;
1687
- box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
3282
+ border-color: var(--color-accent);
3283
+ box-shadow: 0 0 0 3px var(--color-accent-soft);
1688
3284
  }
1689
3285
 
1690
3286
  .ui-control__input::placeholder {
@@ -1767,6 +3363,7 @@ export class UiTextareaComponent {
1767
3363
  gap: 10px;
1768
3364
  font-size: 13px;
1769
3365
  color: #1f2937;
3366
+ min-width: 0;
1770
3367
  }
1771
3368
 
1772
3369
  .ui-control__label {
@@ -1782,14 +3379,14 @@ export class UiTextareaComponent {
1782
3379
  }
1783
3380
 
1784
3381
  .ui-control__info {
1785
- margin-left: 8px;
1786
- width: 18px;
1787
- height: 18px;
3382
+ margin-left: 6px;
3383
+ width: 16px;
3384
+ height: 16px;
1788
3385
  border-radius: 999px;
1789
- border: 1px solid rgba(15, 23, 42, 0.2);
3386
+ border: 1px solid var(--color-border);
1790
3387
  background: #ffffff;
1791
- color: #475569;
1792
- font-size: 11px;
3388
+ color: var(--color-primary-strong);
3389
+ font-size: 10px;
1793
3390
  line-height: 1;
1794
3391
  display: inline-flex;
1795
3392
  align-items: center;
@@ -1798,43 +3395,47 @@ export class UiTextareaComponent {
1798
3395
  }
1799
3396
 
1800
3397
  .ui-control__info:hover {
1801
- background: #f8fafc;
3398
+ background: #ffffff;
1802
3399
  }
1803
3400
 
1804
3401
  .ui-control__info-panel {
1805
3402
  margin-top: 8px;
1806
3403
  padding: 10px 12px;
1807
- border-radius: 10px;
1808
- background: #f8fafc;
1809
- border: 1px solid #e2e8f0;
3404
+ border-radius: 14px;
3405
+ background: #ffffff;
3406
+ border: 1px solid var(--color-border);
1810
3407
  color: #475569;
1811
3408
  font-size: 12px;
1812
3409
  line-height: 1.4;
3410
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
1813
3411
  }
1814
3412
 
1815
3413
  .ui-control__input {
1816
3414
  width: 100%;
1817
- min-height: 3.4rem;
1818
- border-radius: 10px;
1819
- border: 1px solid rgba(15, 23, 42, 0.12);
3415
+ max-width: 100%;
3416
+ min-width: 0;
3417
+ min-height: 2.9rem;
3418
+ border-radius: 14px;
3419
+ border: 1px solid var(--color-border);
1820
3420
  background: #ffffff;
1821
- padding: 0.9rem 1.1rem;
1822
- font-size: 15px;
3421
+ padding: 0.7rem 0.95rem;
3422
+ font-size: 14px;
1823
3423
  font-weight: 500;
3424
+ box-sizing: border-box;
1824
3425
  box-shadow: none;
1825
3426
  outline: none;
1826
3427
  transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
1827
3428
  }
1828
3429
 
1829
3430
  .ui-control__input:focus {
1830
- border-color: var(--color-primary);
1831
- box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
3431
+ border-color: var(--color-primary-strong);
3432
+ box-shadow: 0 0 0 4px var(--color-primary-soft);
1832
3433
  transform: translateY(-1px);
1833
3434
  }
1834
3435
 
1835
3436
  .ui-control__input.invalid {
1836
- border-color: #ef4444;
1837
- box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
3437
+ border-color: var(--color-accent);
3438
+ box-shadow: 0 0 0 3px var(--color-accent-soft);
1838
3439
  }
1839
3440
 
1840
3441
  .ui-control__input::placeholder {
@@ -1918,6 +3519,7 @@ export class UiSelectComponent {
1918
3519
  gap: 10px;
1919
3520
  font-size: 13px;
1920
3521
  color: #1f2937;
3522
+ min-width: 0;
1921
3523
  }
1922
3524
 
1923
3525
  .ui-control__label {
@@ -1933,14 +3535,14 @@ export class UiSelectComponent {
1933
3535
  }
1934
3536
 
1935
3537
  .ui-control__info {
1936
- margin-left: 8px;
1937
- width: 18px;
1938
- height: 18px;
3538
+ margin-left: 6px;
3539
+ width: 16px;
3540
+ height: 16px;
1939
3541
  border-radius: 999px;
1940
- border: 1px solid rgba(15, 23, 42, 0.2);
3542
+ border: 1px solid var(--color-border);
1941
3543
  background: #ffffff;
1942
- color: #475569;
1943
- font-size: 11px;
3544
+ color: var(--color-primary-strong);
3545
+ font-size: 10px;
1944
3546
  line-height: 1;
1945
3547
  display: inline-flex;
1946
3548
  align-items: center;
@@ -1949,29 +3551,33 @@ export class UiSelectComponent {
1949
3551
  }
1950
3552
 
1951
3553
  .ui-control__info:hover {
1952
- background: #f8fafc;
3554
+ background: #ffffff;
1953
3555
  }
1954
3556
 
1955
3557
  .ui-control__info-panel {
1956
3558
  margin-top: 8px;
1957
3559
  padding: 10px 12px;
1958
- border-radius: 10px;
1959
- background: #f8fafc;
1960
- border: 1px solid #e2e8f0;
3560
+ border-radius: 14px;
3561
+ background: #ffffff;
3562
+ border: 1px solid var(--color-border);
1961
3563
  color: #475569;
1962
3564
  font-size: 12px;
1963
3565
  line-height: 1.4;
3566
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
1964
3567
  }
1965
3568
 
1966
3569
  .ui-control__select {
1967
3570
  width: 100%;
1968
- min-height: 3.4rem;
1969
- border-radius: 10px;
1970
- border: 1px solid rgba(15, 23, 42, 0.12);
3571
+ max-width: 100%;
3572
+ min-width: 0;
3573
+ min-height: 2.9rem;
3574
+ border-radius: 14px;
3575
+ border: 1px solid var(--color-border);
1971
3576
  background: #ffffff;
1972
- padding: 0.9rem 2.6rem 0.9rem 1.1rem;
1973
- font-size: 15px;
3577
+ padding: 0.7rem 2.3rem 0.7rem 0.95rem;
3578
+ font-size: 14px;
1974
3579
  font-weight: 500;
3580
+ box-sizing: border-box;
1975
3581
  box-shadow: none;
1976
3582
  outline: none;
1977
3583
  transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
@@ -1983,14 +3589,14 @@ export class UiSelectComponent {
1983
3589
  }
1984
3590
 
1985
3591
  .ui-control__select:focus {
1986
- border-color: var(--color-primary);
1987
- box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
3592
+ border-color: var(--color-primary-strong);
3593
+ box-shadow: 0 0 0 4px var(--color-primary-soft);
1988
3594
  transform: translateY(-1px);
1989
3595
  }
1990
3596
 
1991
3597
  .ui-control__select.invalid {
1992
- border-color: #ef4444;
1993
- box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
3598
+ border-color: var(--color-accent);
3599
+ box-shadow: 0 0 0 3px var(--color-accent-soft);
1994
3600
  }
1995
3601
  `
1996
3602
  },
@@ -2066,6 +3672,7 @@ export class UiCheckboxComponent {
2066
3672
  gap: 10px;
2067
3673
  font-size: 13px;
2068
3674
  color: #1f2937;
3675
+ min-width: 0;
2069
3676
  }
2070
3677
 
2071
3678
  .ui-control__label {
@@ -2081,14 +3688,14 @@ export class UiCheckboxComponent {
2081
3688
  }
2082
3689
 
2083
3690
  .ui-control__info {
2084
- margin-left: 8px;
2085
- width: 18px;
2086
- height: 18px;
3691
+ margin-left: 6px;
3692
+ width: 16px;
3693
+ height: 16px;
2087
3694
  border-radius: 999px;
2088
- border: 1px solid rgba(15, 23, 42, 0.2);
3695
+ border: 1px solid var(--color-border);
2089
3696
  background: #ffffff;
2090
- color: #475569;
2091
- font-size: 11px;
3697
+ color: var(--color-primary-strong);
3698
+ font-size: 10px;
2092
3699
  line-height: 1;
2093
3700
  display: inline-flex;
2094
3701
  align-items: center;
@@ -2097,27 +3704,31 @@ export class UiCheckboxComponent {
2097
3704
  }
2098
3705
 
2099
3706
  .ui-control__info:hover {
2100
- background: #f8fafc;
3707
+ background: #ffffff;
2101
3708
  }
2102
3709
 
2103
3710
  .ui-control__info-panel {
2104
3711
  margin-top: 8px;
2105
3712
  padding: 10px 12px;
2106
- border-radius: 10px;
2107
- background: #f8fafc;
2108
- border: 1px solid #e2e8f0;
3713
+ border-radius: 14px;
3714
+ background: #ffffff;
3715
+ border: 1px solid var(--color-border);
2109
3716
  color: #475569;
2110
3717
  font-size: 12px;
2111
3718
  line-height: 1.4;
3719
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
2112
3720
  }
2113
3721
 
2114
3722
  .ui-control__checkbox {
2115
- width: 20px;
2116
- height: 20px;
3723
+ width: 22px;
3724
+ height: 22px;
2117
3725
  padding: 0;
2118
- border-radius: 6px;
2119
- box-shadow: none;
2120
- accent-color: var(--color-primary);
3726
+ border-radius: 8px;
3727
+ border: 2px solid var(--color-border);
3728
+ background: #ffffff;
3729
+ box-shadow: 0 8px 18px rgba(99, 102, 241, 0.12);
3730
+ accent-color: var(--color-primary-strong);
3731
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
2121
3732
  }
2122
3733
  `
2123
3734
  }
@@ -2184,7 +3795,12 @@ function toFolderName(operationId) {
2184
3795
  return toFileBase(operationId);
2185
3796
  }
2186
3797
  function toFileBase(operationId) {
2187
- return operationId;
3798
+ return String(operationId).replace(/[\\/]/g, '');
3799
+ }
3800
+ function toSafeFileName(value) {
3801
+ return String(value)
3802
+ .replace(/[\\/]/g, '-')
3803
+ .replace(/\s+/g, '-');
2188
3804
  }
2189
3805
  function toKebab(value) {
2190
3806
  return value
@@ -2201,6 +3817,15 @@ function toPascalCase(value) {
2201
3817
  .map(part => part[0].toUpperCase() + part.slice(1))
2202
3818
  .join('');
2203
3819
  }
3820
+ function normalizeRoutePath(value) {
3821
+ const trimmed = String(value ?? '').trim();
3822
+ if (!trimmed)
3823
+ return trimmed;
3824
+ if (trimmed.includes('/'))
3825
+ return trimmed.replace(/^\//, '');
3826
+ const pascal = toPascalCase(trimmed);
3827
+ return toRouteSegment(pascal);
3828
+ }
2204
3829
  function buildSchemaImportPath(featureDir, schemasRoot, rawName) {
2205
3830
  const schemaFile = path_1.default.join(schemasRoot, 'overlays', `${rawName}.screen.json`);
2206
3831
  let relativePath = path_1.default.relative(featureDir, schemaFile);