generate-ui-cli 2.1.7 → 2.2.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.
@@ -14,7 +14,7 @@ function generateFeature(schema, root, schemasRoot) {
14
14
  const featureDir = path_1.default.join(root, folder);
15
15
  fs_1.default.mkdirSync(featureDir, { recursive: true });
16
16
  const appRoot = path_1.default.resolve(root, '..');
17
- ensureUiComponents(appRoot);
17
+ ensureUiComponents(appRoot, schemasRoot);
18
18
  const method = String(schema.api.method || '').toLowerCase();
19
19
  const endpoint = String(schema.api.endpoint || '');
20
20
  const baseUrl = String(schema.api.baseUrl || 'https://api.realworld.io/api');
@@ -54,11 +54,14 @@ function generateFeature(schema, root, schemasRoot) {
54
54
  const componentPath = path_1.default.join(featureDir, `${fileBase}.component.ts`);
55
55
  fs_1.default.writeFileSync(componentPath, `
56
56
  import { Component } from '@angular/core'
57
- import { JsonPipe, NgFor, NgIf } from '@angular/common'
57
+ import { CommonModule } from '@angular/common'
58
58
  import { FormBuilder, ReactiveFormsModule } from '@angular/forms'
59
59
  import { UiCardComponent } from '../../ui/ui-card/ui-card.component'
60
- import { UiFieldComponent } from '../../ui/ui-field/ui-field.component'
61
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'
62
65
  import { ${name}Service } from './${fileBase}.service.gen'
63
66
  import { ${name}Gen } from './${fileBase}.gen'
64
67
  import screenSchema from '${schemaImportPath}'
@@ -67,13 +70,14 @@ import screenSchema from '${schemaImportPath}'
67
70
  selector: 'app-${toKebab(name)}',
68
71
  standalone: true,
69
72
  imports: [
70
- NgIf,
71
- NgFor,
72
- JsonPipe,
73
+ CommonModule,
73
74
  ReactiveFormsModule,
74
75
  UiCardComponent,
75
- UiFieldComponent,
76
- UiButtonComponent
76
+ UiButtonComponent,
77
+ UiSelectComponent,
78
+ UiCheckboxComponent,
79
+ UiInputComponent,
80
+ UiTextareaComponent
77
81
  ],
78
82
  templateUrl: './${fileBase}.component.html',
79
83
  styleUrls: ['./${fileBase}.component.scss']
@@ -100,7 +104,11 @@ export class ${name}Component extends ${name}Gen {
100
104
  .execute(pathParams, queryParams, body)
101
105
  .subscribe({
102
106
  next: result => {
103
- this.result = result
107
+ const normalized =
108
+ result && typeof result === 'object' && 'body' in result
109
+ ? (result as any).body
110
+ : result
111
+ this.result = normalized
104
112
  this.loading = false
105
113
  },
106
114
  error: error => {
@@ -115,20 +123,17 @@ export class ${name}Component extends ${name}Gen {
115
123
  }
116
124
 
117
125
  getRows() {
118
- const value = this.result
126
+ const value = this.unwrapResult(this.result)
119
127
  if (Array.isArray(value)) return value
120
128
  if (!value || typeof value !== 'object') return []
121
129
 
122
- const commonKeys = ['data', 'items', 'results', 'list', 'records']
130
+ const commonKeys = ['data', 'items', 'results', 'list', 'records', 'products']
123
131
  for (const key of commonKeys) {
124
132
  if (Array.isArray(value[key])) return value[key]
125
133
  }
126
134
 
127
- for (const key of Object.keys(value)) {
128
- if (Array.isArray(value[key])) return value[key]
129
- }
130
-
131
- return []
135
+ const found = this.findFirstArray(value, 0, 5)
136
+ return found ?? []
132
137
  }
133
138
 
134
139
  getColumns() {
@@ -152,7 +157,7 @@ export class ${name}Component extends ${name}Gen {
152
157
  return value
153
158
  .replace(/[_-]/g, ' ')
154
159
  .replace(/([a-z])([A-Z])/g, '$1 $2')
155
- .replace(/\b\w/g, char => char.toUpperCase())
160
+ .replace(/\\b\\w/g, char => char.toUpperCase())
156
161
  }
157
162
 
158
163
  getCellValue(row: any, column: string) {
@@ -177,7 +182,7 @@ export class ${name}Component extends ${name}Gen {
177
182
  )
178
183
  }
179
184
 
180
- private formatValue(value: any): string {
185
+ formatValue(value: any): string {
181
186
  if (value === null || value === undefined) return ''
182
187
  if (typeof value === 'string' || typeof value === 'number') {
183
188
  return String(value)
@@ -201,13 +206,42 @@ export class ${name}Component extends ${name}Gen {
201
206
  }
202
207
 
203
208
  getObjectRows() {
204
- const value = this.result
209
+ const value = this.unwrapResult(this.result)
205
210
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
206
211
  return []
207
212
  }
208
213
  return this.flattenObject(value)
209
214
  }
210
215
 
216
+ hasObjectRows() {
217
+ return this.getObjectRows().length > 0
218
+ }
219
+
220
+ isSingleValue() {
221
+ const value = this.unwrapResult(this.result)
222
+ return (
223
+ value !== null &&
224
+ value !== undefined &&
225
+ (typeof value === 'string' ||
226
+ typeof value === 'number' ||
227
+ typeof value === 'boolean')
228
+ )
229
+ }
230
+
231
+ private unwrapResult(value: any) {
232
+ if (!value || typeof value !== 'object') return value
233
+ if (Object.prototype.hasOwnProperty.call(value, 'data')) {
234
+ return value.data
235
+ }
236
+ if (Object.prototype.hasOwnProperty.call(value, 'result')) {
237
+ return value.result
238
+ }
239
+ if (Object.prototype.hasOwnProperty.call(value, 'body')) {
240
+ return value.body
241
+ }
242
+ return value
243
+ }
244
+
211
245
  private flattenObject(
212
246
  value: Record<string, any>,
213
247
  prefix = ''
@@ -224,6 +258,22 @@ export class ${name}Component extends ${name}Gen {
224
258
  return rows
225
259
  }
226
260
 
261
+ private findFirstArray(
262
+ value: any,
263
+ depth: number,
264
+ maxDepth: number
265
+ ): any[] | null {
266
+ if (!value || depth > maxDepth) return null
267
+ if (Array.isArray(value)) return value
268
+ if (typeof value !== 'object') return null
269
+
270
+ for (const key of Object.keys(value)) {
271
+ const found = this.findFirstArray(value[key], depth + 1, maxDepth)
272
+ if (found) return found
273
+ }
274
+ return null
275
+ }
276
+
227
277
  }
228
278
  `);
229
279
  /**
@@ -280,10 +330,19 @@ export class ${name}Gen {
280
330
  }
281
331
 
282
332
  protected isSelect(field: any) {
333
+ if (field?.ui === 'select' || field?.ui === 'dropdown') return true
283
334
  return Array.isArray(field.options) && field.options.length > 0
284
335
  }
285
336
 
337
+ protected getSelectOptions(field: any) {
338
+ if (Array.isArray(field.options) && field.options.length > 0) {
339
+ return field.options
340
+ }
341
+ return []
342
+ }
343
+
286
344
  protected isCheckbox(field: any) {
345
+ if (field?.ui === 'select' || field?.ui === 'dropdown') return false
287
346
  return field.type === 'boolean'
288
347
  }
289
348
 
@@ -457,11 +516,13 @@ export class ${name}Service {
457
516
  :host {
458
517
  display: block;
459
518
  padding: 24px;
519
+ min-height: 100vh;
460
520
  }
461
521
 
462
522
  .page {
463
523
  display: grid;
464
524
  gap: 16px;
525
+ min-height: calc(100vh - 48px);
465
526
  }
466
527
 
467
528
  .screen-description {
@@ -480,6 +541,17 @@ export class ${name}Service {
480
541
  margin: 0 auto;
481
542
  }
482
543
 
544
+ .form-field {
545
+ display: grid;
546
+ gap: 8px;
547
+ }
548
+
549
+ .field-error {
550
+ color: #ef4444;
551
+ font-size: 12px;
552
+ margin-top: -4px;
553
+ }
554
+
483
555
  .actions {
484
556
  display: flex;
485
557
  justify-content: flex-end;
@@ -501,10 +573,12 @@ export class ${name}Service {
501
573
 
502
574
  .result-table {
503
575
  margin-top: 20px;
504
- overflow: hidden;
576
+ max-width: 100%;
577
+ overflow: auto;
505
578
  border-radius: 16px;
506
579
  border: 1px solid #e2e8f0;
507
580
  box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
581
+ -webkit-overflow-scrolling: touch;
508
582
  }
509
583
 
510
584
  .result-card {
@@ -548,8 +622,74 @@ export class ${name}Service {
548
622
  text-align: right;
549
623
  }
550
624
 
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
+ }
636
+
637
+ .result-error__body {
638
+ font-size: 13px;
639
+ color: #7f1d1d;
640
+ word-break: break-word;
641
+ }
642
+
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
+ }
653
+
654
+ .result-raw summary {
655
+ cursor: pointer;
656
+ font-weight: 600;
657
+ color: #334155;
658
+ }
659
+
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);
666
+ font-size: 12px;
667
+ line-height: 1.4;
668
+ white-space: pre-wrap;
669
+ word-break: break-word;
670
+ }
671
+
672
+ .result-single {
673
+ margin-top: 24px;
674
+ padding: 16px 18px;
675
+ border-radius: 16px;
676
+ border: 1px solid rgba(15, 23, 42, 0.08);
677
+ background: #ffffff;
678
+ color: #0f172a;
679
+ display: grid;
680
+ gap: 8px;
681
+ box-shadow: 0 8px 18px rgba(15, 23, 42, 0.08);
682
+ }
683
+
684
+ .result-single__value {
685
+ font-size: 18px;
686
+ font-weight: 700;
687
+ color: #0f172a;
688
+ }
689
+
551
690
  .data-table {
552
- width: 100%;
691
+ width: max-content;
692
+ min-width: 100%;
553
693
  border-collapse: collapse;
554
694
  background: #ffffff;
555
695
  font-size: 14px;
@@ -588,6 +728,17 @@ export class ${name}Service {
588
728
  box-shadow: 0 6px 12px rgba(15, 23, 42, 0.16);
589
729
  }
590
730
 
731
+ :host ::ng-deep ui-card .ui-card {
732
+ display: flex;
733
+ flex-direction: column;
734
+ max-height: calc(100vh - 160px);
735
+ }
736
+
737
+ :host ::ng-deep ui-card .ui-card__body {
738
+ overflow: auto;
739
+ min-height: 0;
740
+ }
741
+
591
742
  @media (max-width: 720px) {
592
743
  :host {
593
744
  padding: 18px;
@@ -737,6 +888,65 @@ function buildComponentHtml(options) {
737
888
  </ui-button>
738
889
  </div>
739
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>
740
950
  </div>
741
951
  `;
742
952
  }
@@ -748,51 +958,52 @@ function buildComponentHtml(options) {
748
958
  </p>
749
959
  <form [formGroup]="form" (ngSubmit)="submit()">
750
960
  <div class="form-grid">
751
- <ui-field
752
- *ngFor="let field of formFields"
753
- [label]="field.label || field.name"
754
- [hint]="field.hint"
755
- [info]="field.info"
756
- >
757
- <select
961
+ <div class="form-field" *ngFor="let field of formFields">
962
+ <ui-select
758
963
  *ngIf="isSelect(field)"
759
- [formControlName]="field.name"
760
- [class.invalid]="isInvalid(field)"
761
- >
762
- <option
763
- *ngFor="let option of field.options"
764
- [value]="option"
765
- >
766
- {{ option }}
767
- </option>
768
- </select>
769
-
770
- <textarea
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
771
973
  *ngIf="isTextarea(field)"
772
- rows="3"
773
- [formControlName]="field.name"
974
+ [label]="field.label || field.name"
975
+ [hint]="field.hint"
976
+ [info]="field.info"
977
+ [controlName]="field.name"
978
+ [rows]="3"
774
979
  [placeholder]="field.placeholder || field.label || field.name"
775
- [class.invalid]="isInvalid(field)"
776
- ></textarea>
980
+ [invalid]="isInvalid(field)"
981
+ ></ui-textarea>
777
982
 
778
- <input
983
+ <ui-checkbox
779
984
  *ngIf="isCheckbox(field)"
780
- type="checkbox"
781
- [formControlName]="field.name"
782
- />
783
-
784
- <input
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
785
993
  *ngIf="!isSelect(field) && !isTextarea(field) && !isCheckbox(field)"
994
+ [label]="field.label || field.name"
995
+ [hint]="field.hint"
996
+ [info]="field.info"
786
997
  [type]="inputType(field)"
787
- [formControlName]="field.name"
998
+ [controlName]="field.name"
788
999
  [placeholder]="field.placeholder || field.label || field.name"
789
- [class.invalid]="isInvalid(field)"
790
- />
1000
+ [invalid]="isInvalid(field)"
1001
+ ></ui-input>
791
1002
 
792
1003
  <span class="field-error" *ngIf="isInvalid(field)">
793
1004
  Campo obrigatório
794
1005
  </span>
795
- </ui-field>
1006
+ </div>
796
1007
  </div>
797
1008
  <div class="actions">
798
1009
  <ui-button
@@ -833,7 +1044,14 @@ function buildComponentHtml(options) {
833
1044
  </table>
834
1045
  </div>
835
1046
 
836
- <div class="result-card" *ngIf="!isArrayResult() && result">
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
+
1054
+ <div class="result-card" *ngIf="!isArrayResult() && hasObjectRows()">
837
1055
  <div class="result-card__grid">
838
1056
  <div class="result-card__row" *ngFor="let row of getObjectRows()">
839
1057
  <span class="result-card__label">
@@ -845,6 +1063,18 @@ function buildComponentHtml(options) {
845
1063
  </div>
846
1064
  </div>
847
1065
  </div>
1066
+
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>
1073
+
1074
+ <details class="result-raw" *ngIf="result">
1075
+ <summary>Raw response</summary>
1076
+ <pre>{{ result | json }}</pre>
1077
+ </details>
848
1078
  </div>
849
1079
  `;
850
1080
  }
@@ -878,7 +1108,7 @@ function httpCallForMethod(method) {
878
1108
  return 'return this.http.get(url)';
879
1109
  }
880
1110
  }
881
- function ensureUiComponents(appRoot) {
1111
+ function ensureUiComponents(appRoot, schemasRoot) {
882
1112
  const uiRoot = path_1.default.join(appRoot, 'ui');
883
1113
  const components = [
884
1114
  {
@@ -916,11 +1146,22 @@ export class UiCardComponent {
916
1146
  }
917
1147
 
918
1148
  .ui-card {
919
- border-radius: 20px;
920
- background: #ffffff;
921
- border: 1px solid #e5e7eb;
922
- box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
923
- padding: 28px;
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;
1154
+ position: relative;
1155
+ overflow: hidden;
1156
+ }
1157
+
1158
+ .ui-card::before {
1159
+ content: "";
1160
+ position: absolute;
1161
+ 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;
924
1165
  }
925
1166
 
926
1167
  .ui-card__header {
@@ -929,17 +1170,154 @@ export class UiCardComponent {
929
1170
 
930
1171
  .ui-card__title {
931
1172
  margin: 0;
932
- font-size: 22px;
1173
+ font-size: 26px;
933
1174
  font-weight: 700;
934
- color: #0f172a;
1175
+ color: var(--bg-ink);
1176
+ letter-spacing: -0.02em;
935
1177
  }
936
1178
 
937
1179
  .ui-card__subtitle {
938
1180
  margin: 8px 0 0;
939
- font-size: 12px;
940
- color: #6b7280;
941
- letter-spacing: 0.04em;
1181
+ font-size: 13px;
1182
+ color: var(--color-muted);
1183
+ letter-spacing: 0.16em;
1184
+ text-transform: uppercase;
1185
+ font-family: "Space Mono", "Courier New", monospace;
1186
+ }
1187
+ `
1188
+ },
1189
+ {
1190
+ name: 'ui-menu',
1191
+ template: '',
1192
+ html: `
1193
+ <nav class="ui-menu">
1194
+ <div class="ui-menu__brand">
1195
+ <span class="ui-menu__brand-pill"></span>
1196
+ <span class="ui-menu__brand-title">{{ title }}</span>
1197
+ </div>
1198
+
1199
+ <ng-container *ngFor="let group of menu.groups">
1200
+ <section
1201
+ class="ui-menu__group"
1202
+ *ngIf="!group.hidden && group.items.length"
1203
+ >
1204
+ <h3 class="ui-menu__group-title">{{ group.label }}</h3>
1205
+ <a
1206
+ class="ui-menu__item"
1207
+ *ngFor="let item of group.items"
1208
+ [routerLink]="item.route"
1209
+ routerLinkActive="active"
1210
+ [class.hidden]="item.hidden"
1211
+ >
1212
+ {{ item.label }}
1213
+ </a>
1214
+ </section>
1215
+ </ng-container>
1216
+
1217
+ <section
1218
+ class="ui-menu__group"
1219
+ *ngIf="menu.ungrouped.length"
1220
+ >
1221
+ <h3 class="ui-menu__group-title">Outros</h3>
1222
+ <a
1223
+ class="ui-menu__item"
1224
+ *ngFor="let item of menu.ungrouped"
1225
+ [routerLink]="item.route"
1226
+ routerLinkActive="active"
1227
+ [class.hidden]="item.hidden"
1228
+ >
1229
+ {{ item.label }}
1230
+ </a>
1231
+ </section>
1232
+ </nav>
1233
+ `,
1234
+ scss: `
1235
+ :host {
1236
+ display: block;
1237
+ height: 100%;
1238
+ }
1239
+
1240
+ .ui-menu {
1241
+ position: sticky;
1242
+ top: 24px;
1243
+ 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;
1248
+ padding: 22px;
1249
+ min-width: 220px;
1250
+ display: grid;
1251
+ gap: 22px;
1252
+ }
1253
+
1254
+ .ui-menu__brand {
1255
+ display: flex;
1256
+ align-items: center;
1257
+ gap: 10px;
1258
+ font-weight: 700;
1259
+ color: var(--bg-ink);
1260
+ font-size: 15px;
1261
+ }
1262
+
1263
+ .ui-menu__brand-pill {
1264
+ width: 14px;
1265
+ height: 14px;
1266
+ 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);
1269
+ }
1270
+
1271
+ .ui-menu__group {
1272
+ display: grid;
1273
+ gap: 10px;
1274
+ }
1275
+
1276
+ .ui-menu__group-title {
1277
+ margin: 0;
1278
+ font-size: 11px;
1279
+ letter-spacing: 0.3em;
942
1280
  text-transform: uppercase;
1281
+ color: var(--color-muted);
1282
+ font-family: "Space Mono", "Courier New", monospace;
1283
+ }
1284
+
1285
+ .ui-menu__item {
1286
+ display: block;
1287
+ text-decoration: none;
1288
+ font-size: 14px;
1289
+ font-weight: 600;
1290
+ color: #1f2937;
1291
+ padding: 10px 14px;
1292
+ border-radius: 14px;
1293
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
1294
+ border: 1px solid transparent;
1295
+ white-space: nowrap;
1296
+ overflow: hidden;
1297
+ text-overflow: ellipsis;
1298
+ }
1299
+
1300
+ .ui-menu__item:hover {
1301
+ background: #f1f5f9;
1302
+ transform: translateX(2px);
1303
+ }
1304
+
1305
+ .ui-menu__item.active {
1306
+ background: linear-gradient(135deg, var(--color-primary), var(--color-primary-strong));
1307
+ color: #ffffff;
1308
+ box-shadow: 0 12px 24px rgba(8, 145, 178, 0.3);
1309
+ }
1310
+
1311
+ .ui-menu__item.hidden {
1312
+ display: none;
1313
+ }
1314
+
1315
+ @media (max-width: 900px) {
1316
+ .ui-menu {
1317
+ position: static;
1318
+ width: 100%;
1319
+ min-width: auto;
1320
+ }
943
1321
  }
944
1322
  `
945
1323
  },
@@ -997,15 +1375,16 @@ export class UiFieldComponent {
997
1375
 
998
1376
  .ui-field {
999
1377
  display: grid;
1000
- gap: 6px;
1001
- font-size: 12px;
1002
- color: #374151;
1378
+ gap: 10px;
1379
+ font-size: 13px;
1380
+ color: #1f2937;
1003
1381
  }
1004
1382
 
1005
1383
  .ui-field__label {
1006
1384
  font-weight: 700;
1007
1385
  line-height: 1.4;
1008
1386
  word-break: break-word;
1387
+ letter-spacing: 0.01em;
1009
1388
  }
1010
1389
 
1011
1390
  .ui-field__hint {
@@ -1018,7 +1397,7 @@ export class UiFieldComponent {
1018
1397
  width: 18px;
1019
1398
  height: 18px;
1020
1399
  border-radius: 999px;
1021
- border: 1px solid #cbd5f5;
1400
+ border: 1px solid rgba(15, 23, 42, 0.2);
1022
1401
  background: #ffffff;
1023
1402
  color: #475569;
1024
1403
  font-size: 11px;
@@ -1045,31 +1424,29 @@ export class UiFieldComponent {
1045
1424
  }
1046
1425
 
1047
1426
  :host ::ng-deep input,
1048
- :host ::ng-deep textarea,
1049
- :host ::ng-deep select {
1427
+ :host ::ng-deep textarea {
1050
1428
  width: 100%;
1051
- min-height: 2.6rem;
1052
- border-radius: 8px;
1053
- border: 1px solid #e5e7eb;
1429
+ min-height: 3.4rem;
1430
+ border-radius: 10px;
1431
+ border: 1px solid rgba(15, 23, 42, 0.12);
1054
1432
  background: #ffffff;
1055
- padding: 0.6rem 0.8rem;
1056
- font-size: 14px;
1433
+ padding: 0.9rem 1.1rem;
1434
+ font-size: 15px;
1057
1435
  font-weight: 500;
1058
1436
  box-shadow: none;
1059
1437
  outline: none;
1060
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
1438
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
1061
1439
  }
1062
1440
 
1063
1441
  :host ::ng-deep input:focus,
1064
- :host ::ng-deep textarea:focus,
1065
- :host ::ng-deep select:focus {
1066
- border-color: #6366f1;
1067
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
1442
+ :host ::ng-deep textarea:focus {
1443
+ border-color: var(--color-primary);
1444
+ box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
1445
+ transform: translateY(-1px);
1068
1446
  }
1069
1447
 
1070
1448
  :host ::ng-deep input.invalid,
1071
- :host ::ng-deep textarea.invalid,
1072
- :host ::ng-deep select.invalid {
1449
+ :host ::ng-deep textarea.invalid {
1073
1450
  border-color: #ef4444;
1074
1451
  box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
1075
1452
  }
@@ -1079,27 +1456,13 @@ export class UiFieldComponent {
1079
1456
  color: #94a3b8;
1080
1457
  }
1081
1458
 
1082
- :host ::ng-deep select {
1083
- padding-right: 2.2rem;
1084
- background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='8' viewBox='0 0 14 8' fill='none'><path d='M1 1.5L7 6.5L13 1.5' stroke='%236b7280' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
1085
- background-repeat: no-repeat;
1086
- background-position: right 0.7rem center;
1087
- background-size: 14px 8px;
1088
- appearance: none;
1089
- }
1090
-
1091
- :host ::ng-deep textarea {
1092
- min-height: 5.5rem;
1093
- resize: vertical;
1094
- }
1095
-
1096
1459
  :host ::ng-deep input[type='checkbox'] {
1097
1460
  width: 20px;
1098
1461
  height: 20px;
1099
1462
  padding: 0;
1100
1463
  border-radius: 6px;
1101
1464
  box-shadow: none;
1102
- accent-color: #6366f1;
1465
+ accent-color: var(--color-primary);
1103
1466
  }
1104
1467
 
1105
1468
  .field-error {
@@ -1141,18 +1504,19 @@ export class UiButtonComponent {
1141
1504
  scss: `
1142
1505
  .ui-button {
1143
1506
  border: none;
1144
- border-radius: 10px;
1145
- padding: 12px 22px;
1507
+ border-radius: 999px;
1508
+ padding: 12px 24px;
1146
1509
  font-weight: 700;
1147
1510
  font-size: 14px;
1511
+ letter-spacing: 0.02em;
1148
1512
  cursor: pointer;
1149
- transition: transform 0.2s ease, box-shadow 0.2s ease;
1513
+ transition: transform 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
1150
1514
  }
1151
1515
 
1152
1516
  .ui-button.primary {
1153
- background: linear-gradient(135deg, #6366f1, #818cf8);
1517
+ background: linear-gradient(135deg, var(--color-primary), var(--color-primary-strong));
1154
1518
  color: #ffffff;
1155
- box-shadow: 0 8px 18px rgba(99, 102, 241, 0.22);
1519
+ box-shadow: 0 12px 24px rgba(8, 145, 178, 0.3);
1156
1520
  }
1157
1521
 
1158
1522
  .ui-button.ghost {
@@ -1163,11 +1527,12 @@ export class UiButtonComponent {
1163
1527
  .ui-button.danger {
1164
1528
  background: linear-gradient(135deg, #ef4444, #f97316);
1165
1529
  color: #fff;
1166
- box-shadow: 0 8px 18px rgba(239, 68, 68, 0.25);
1530
+ box-shadow: 0 10px 22px rgba(239, 68, 68, 0.25);
1167
1531
  }
1168
1532
 
1169
1533
  .ui-button:hover:not(:disabled) {
1170
1534
  transform: translateY(-1px);
1535
+ filter: brightness(1.02);
1171
1536
  }
1172
1537
 
1173
1538
  .ui-button:disabled {
@@ -1176,39 +1541,641 @@ export class UiButtonComponent {
1176
1541
  box-shadow: none;
1177
1542
  }
1178
1543
  `
1179
- }
1180
- ];
1181
- fs_1.default.mkdirSync(uiRoot, { recursive: true });
1182
- for (const component of components) {
1183
- const componentDir = path_1.default.join(uiRoot, component.name);
1184
- fs_1.default.mkdirSync(componentDir, { recursive: true });
1185
- const base = component.name;
1186
- const tsPath = path_1.default.join(componentDir, `${base}.component.ts`);
1187
- const htmlPath = path_1.default.join(componentDir, `${base}.component.html`);
1188
- const scssPath = path_1.default.join(componentDir, `${base}.component.scss`);
1189
- const needsUiFieldUpdate = component.name === 'ui-field';
1190
- const shouldOverwrite = (filePath, marker) => {
1191
- if (!fs_1.default.existsSync(filePath))
1192
- return true;
1193
- if (needsUiFieldUpdate)
1194
- return true;
1195
- if (!marker)
1196
- return true;
1197
- const existing = fs_1.default.readFileSync(filePath, 'utf-8');
1198
- return !existing.includes(marker);
1199
- };
1200
- if (shouldOverwrite(tsPath, 'infoOpen')) {
1201
- fs_1.default.writeFileSync(tsPath, component.template.trimStart());
1202
- }
1203
- if (shouldOverwrite(htmlPath, 'ui-field__info-panel')) {
1204
- fs_1.default.writeFileSync(htmlPath, component.html.trimStart());
1205
- }
1206
- if (shouldOverwrite(scssPath, 'ui-field__info-panel')) {
1207
- fs_1.default.writeFileSync(scssPath, component.scss.trimStart());
1208
- }
1209
- }
1544
+ },
1545
+ {
1546
+ name: 'ui-input',
1547
+ template: `
1548
+ import { Component, Input } from '@angular/core'
1549
+ import { NgIf } from '@angular/common'
1550
+ import {
1551
+ ControlContainer,
1552
+ FormGroupDirective,
1553
+ ReactiveFormsModule
1554
+ } from '@angular/forms'
1555
+
1556
+ @Component({
1557
+ selector: 'ui-input',
1558
+ standalone: true,
1559
+ imports: [NgIf, ReactiveFormsModule],
1560
+ viewProviders: [
1561
+ { provide: ControlContainer, useExisting: FormGroupDirective }
1562
+ ],
1563
+ templateUrl: './ui-input.component.html',
1564
+ styleUrls: ['./ui-input.component.scss']
1565
+ })
1566
+ export class UiInputComponent {
1567
+ @Input() label = ''
1568
+ @Input() hint = ''
1569
+ @Input() info = ''
1570
+ @Input() controlName = ''
1571
+ @Input() placeholder = ''
1572
+ @Input() type: 'text' | 'number' | 'email' | 'password' | 'search' | 'tel' | 'url' = 'text'
1573
+ @Input() invalid = false
1574
+ infoOpen = false
1575
+
1576
+ toggleInfo(event: MouseEvent) {
1577
+ event.preventDefault()
1578
+ event.stopPropagation()
1579
+ this.infoOpen = !this.infoOpen
1580
+ }
1210
1581
  }
1211
- function toRouteSegment(operationId) {
1582
+ `,
1583
+ html: `
1584
+ <label class="ui-control">
1585
+ <span class="ui-control__label" *ngIf="label">
1586
+ {{ label }}
1587
+ <button
1588
+ class="ui-control__info"
1589
+ type="button"
1590
+ *ngIf="info"
1591
+ (click)="toggleInfo($event)"
1592
+ [attr.aria-expanded]="infoOpen"
1593
+ >
1594
+ i
1595
+ </button>
1596
+ </span>
1597
+ <div class="ui-control__info-panel" *ngIf="info && infoOpen">
1598
+ {{ info }}
1599
+ </div>
1600
+ <input
1601
+ class="ui-control__input"
1602
+ [type]="type"
1603
+ [formControlName]="controlName"
1604
+ [placeholder]="placeholder"
1605
+ [class.invalid]="invalid"
1606
+ />
1607
+ <span class="ui-control__hint" *ngIf="hint && !info">{{ hint }}</span>
1608
+ </label>
1609
+ `,
1610
+ scss: `
1611
+ :host {
1612
+ display: block;
1613
+ }
1614
+
1615
+ .ui-control {
1616
+ display: grid;
1617
+ gap: 10px;
1618
+ font-size: 13px;
1619
+ color: #1f2937;
1620
+ }
1621
+
1622
+ .ui-control__label {
1623
+ font-weight: 700;
1624
+ line-height: 1.4;
1625
+ word-break: break-word;
1626
+ letter-spacing: 0.01em;
1627
+ }
1628
+
1629
+ .ui-control__hint {
1630
+ color: #94a3b8;
1631
+ font-size: 12px;
1632
+ }
1633
+
1634
+ .ui-control__info {
1635
+ margin-left: 8px;
1636
+ width: 18px;
1637
+ height: 18px;
1638
+ border-radius: 999px;
1639
+ border: 1px solid rgba(15, 23, 42, 0.2);
1640
+ background: #ffffff;
1641
+ color: #475569;
1642
+ font-size: 11px;
1643
+ line-height: 1;
1644
+ display: inline-flex;
1645
+ align-items: center;
1646
+ justify-content: center;
1647
+ cursor: pointer;
1648
+ }
1649
+
1650
+ .ui-control__info:hover {
1651
+ background: #f8fafc;
1652
+ }
1653
+
1654
+ .ui-control__info-panel {
1655
+ margin-top: 8px;
1656
+ padding: 10px 12px;
1657
+ border-radius: 10px;
1658
+ background: #f8fafc;
1659
+ border: 1px solid #e2e8f0;
1660
+ color: #475569;
1661
+ font-size: 12px;
1662
+ line-height: 1.4;
1663
+ }
1664
+
1665
+ .ui-control__input {
1666
+ width: 100%;
1667
+ min-height: 3.4rem;
1668
+ border-radius: 10px;
1669
+ border: 1px solid rgba(15, 23, 42, 0.12);
1670
+ background: #ffffff;
1671
+ padding: 0.9rem 1.1rem;
1672
+ font-size: 15px;
1673
+ font-weight: 500;
1674
+ box-shadow: none;
1675
+ outline: none;
1676
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
1677
+ }
1678
+
1679
+ .ui-control__input:focus {
1680
+ border-color: var(--color-primary);
1681
+ box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
1682
+ transform: translateY(-1px);
1683
+ }
1684
+
1685
+ .ui-control__input.invalid {
1686
+ border-color: #ef4444;
1687
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
1688
+ }
1689
+
1690
+ .ui-control__input::placeholder {
1691
+ color: #94a3b8;
1692
+ }
1693
+ `
1694
+ },
1695
+ {
1696
+ name: 'ui-textarea',
1697
+ template: `
1698
+ import { Component, Input } from '@angular/core'
1699
+ import { NgIf } from '@angular/common'
1700
+ import {
1701
+ ControlContainer,
1702
+ FormGroupDirective,
1703
+ ReactiveFormsModule
1704
+ } from '@angular/forms'
1705
+
1706
+ @Component({
1707
+ selector: 'ui-textarea',
1708
+ standalone: true,
1709
+ imports: [NgIf, ReactiveFormsModule],
1710
+ viewProviders: [
1711
+ { provide: ControlContainer, useExisting: FormGroupDirective }
1712
+ ],
1713
+ templateUrl: './ui-textarea.component.html',
1714
+ styleUrls: ['./ui-textarea.component.scss']
1715
+ })
1716
+ export class UiTextareaComponent {
1717
+ @Input() label = ''
1718
+ @Input() hint = ''
1719
+ @Input() info = ''
1720
+ @Input() controlName = ''
1721
+ @Input() placeholder = ''
1722
+ @Input() rows = 3
1723
+ @Input() invalid = false
1724
+ infoOpen = false
1725
+
1726
+ toggleInfo(event: MouseEvent) {
1727
+ event.preventDefault()
1728
+ event.stopPropagation()
1729
+ this.infoOpen = !this.infoOpen
1730
+ }
1731
+ }
1732
+ `,
1733
+ html: `
1734
+ <label class="ui-control">
1735
+ <span class="ui-control__label" *ngIf="label">
1736
+ {{ label }}
1737
+ <button
1738
+ class="ui-control__info"
1739
+ type="button"
1740
+ *ngIf="info"
1741
+ (click)="toggleInfo($event)"
1742
+ [attr.aria-expanded]="infoOpen"
1743
+ >
1744
+ i
1745
+ </button>
1746
+ </span>
1747
+ <div class="ui-control__info-panel" *ngIf="info && infoOpen">
1748
+ {{ info }}
1749
+ </div>
1750
+ <textarea
1751
+ class="ui-control__input"
1752
+ [formControlName]="controlName"
1753
+ [rows]="rows"
1754
+ [placeholder]="placeholder"
1755
+ [class.invalid]="invalid"
1756
+ ></textarea>
1757
+ <span class="ui-control__hint" *ngIf="hint && !info">{{ hint }}</span>
1758
+ </label>
1759
+ `,
1760
+ scss: `
1761
+ :host {
1762
+ display: block;
1763
+ }
1764
+
1765
+ .ui-control {
1766
+ display: grid;
1767
+ gap: 10px;
1768
+ font-size: 13px;
1769
+ color: #1f2937;
1770
+ }
1771
+
1772
+ .ui-control__label {
1773
+ font-weight: 700;
1774
+ line-height: 1.4;
1775
+ word-break: break-word;
1776
+ letter-spacing: 0.01em;
1777
+ }
1778
+
1779
+ .ui-control__hint {
1780
+ color: #94a3b8;
1781
+ font-size: 12px;
1782
+ }
1783
+
1784
+ .ui-control__info {
1785
+ margin-left: 8px;
1786
+ width: 18px;
1787
+ height: 18px;
1788
+ border-radius: 999px;
1789
+ border: 1px solid rgba(15, 23, 42, 0.2);
1790
+ background: #ffffff;
1791
+ color: #475569;
1792
+ font-size: 11px;
1793
+ line-height: 1;
1794
+ display: inline-flex;
1795
+ align-items: center;
1796
+ justify-content: center;
1797
+ cursor: pointer;
1798
+ }
1799
+
1800
+ .ui-control__info:hover {
1801
+ background: #f8fafc;
1802
+ }
1803
+
1804
+ .ui-control__info-panel {
1805
+ margin-top: 8px;
1806
+ padding: 10px 12px;
1807
+ border-radius: 10px;
1808
+ background: #f8fafc;
1809
+ border: 1px solid #e2e8f0;
1810
+ color: #475569;
1811
+ font-size: 12px;
1812
+ line-height: 1.4;
1813
+ }
1814
+
1815
+ .ui-control__input {
1816
+ width: 100%;
1817
+ min-height: 3.4rem;
1818
+ border-radius: 10px;
1819
+ border: 1px solid rgba(15, 23, 42, 0.12);
1820
+ background: #ffffff;
1821
+ padding: 0.9rem 1.1rem;
1822
+ font-size: 15px;
1823
+ font-weight: 500;
1824
+ box-shadow: none;
1825
+ outline: none;
1826
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
1827
+ }
1828
+
1829
+ .ui-control__input:focus {
1830
+ border-color: var(--color-primary);
1831
+ box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
1832
+ transform: translateY(-1px);
1833
+ }
1834
+
1835
+ .ui-control__input.invalid {
1836
+ border-color: #ef4444;
1837
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
1838
+ }
1839
+
1840
+ .ui-control__input::placeholder {
1841
+ color: #94a3b8;
1842
+ }
1843
+ `
1844
+ },
1845
+ {
1846
+ name: 'ui-select',
1847
+ template: `
1848
+ import { Component, Input } from '@angular/core'
1849
+ import { NgFor, NgIf } from '@angular/common'
1850
+ import {
1851
+ ControlContainer,
1852
+ FormGroupDirective,
1853
+ ReactiveFormsModule
1854
+ } from '@angular/forms'
1855
+
1856
+ @Component({
1857
+ selector: 'ui-select',
1858
+ standalone: true,
1859
+ imports: [NgFor, NgIf, ReactiveFormsModule],
1860
+ viewProviders: [
1861
+ { provide: ControlContainer, useExisting: FormGroupDirective }
1862
+ ],
1863
+ templateUrl: './ui-select.component.html',
1864
+ styleUrls: ['./ui-select.component.scss']
1865
+ })
1866
+ export class UiSelectComponent {
1867
+ @Input() label = ''
1868
+ @Input() hint = ''
1869
+ @Input() info = ''
1870
+ @Input() controlName = ''
1871
+ @Input() options: any[] = []
1872
+ @Input() invalid = false
1873
+ infoOpen = false
1874
+
1875
+ toggleInfo(event: MouseEvent) {
1876
+ event.preventDefault()
1877
+ event.stopPropagation()
1878
+ this.infoOpen = !this.infoOpen
1879
+ }
1880
+ }
1881
+ `,
1882
+ html: `
1883
+ <label class="ui-control">
1884
+ <span class="ui-control__label" *ngIf="label">
1885
+ {{ label }}
1886
+ <button
1887
+ class="ui-control__info"
1888
+ type="button"
1889
+ *ngIf="info"
1890
+ (click)="toggleInfo($event)"
1891
+ [attr.aria-expanded]="infoOpen"
1892
+ >
1893
+ i
1894
+ </button>
1895
+ </span>
1896
+ <div class="ui-control__info-panel" *ngIf="info && infoOpen">
1897
+ {{ info }}
1898
+ </div>
1899
+ <select
1900
+ class="ui-control__select"
1901
+ [formControlName]="controlName"
1902
+ [class.invalid]="invalid"
1903
+ >
1904
+ <option *ngFor="let option of options" [value]="option">
1905
+ {{ option }}
1906
+ </option>
1907
+ </select>
1908
+ <span class="ui-control__hint" *ngIf="hint && !info">{{ hint }}</span>
1909
+ </label>
1910
+ `,
1911
+ scss: `
1912
+ :host {
1913
+ display: block;
1914
+ }
1915
+
1916
+ .ui-control {
1917
+ display: grid;
1918
+ gap: 10px;
1919
+ font-size: 13px;
1920
+ color: #1f2937;
1921
+ }
1922
+
1923
+ .ui-control__label {
1924
+ font-weight: 700;
1925
+ line-height: 1.4;
1926
+ word-break: break-word;
1927
+ letter-spacing: 0.01em;
1928
+ }
1929
+
1930
+ .ui-control__hint {
1931
+ color: #94a3b8;
1932
+ font-size: 12px;
1933
+ }
1934
+
1935
+ .ui-control__info {
1936
+ margin-left: 8px;
1937
+ width: 18px;
1938
+ height: 18px;
1939
+ border-radius: 999px;
1940
+ border: 1px solid rgba(15, 23, 42, 0.2);
1941
+ background: #ffffff;
1942
+ color: #475569;
1943
+ font-size: 11px;
1944
+ line-height: 1;
1945
+ display: inline-flex;
1946
+ align-items: center;
1947
+ justify-content: center;
1948
+ cursor: pointer;
1949
+ }
1950
+
1951
+ .ui-control__info:hover {
1952
+ background: #f8fafc;
1953
+ }
1954
+
1955
+ .ui-control__info-panel {
1956
+ margin-top: 8px;
1957
+ padding: 10px 12px;
1958
+ border-radius: 10px;
1959
+ background: #f8fafc;
1960
+ border: 1px solid #e2e8f0;
1961
+ color: #475569;
1962
+ font-size: 12px;
1963
+ line-height: 1.4;
1964
+ }
1965
+
1966
+ .ui-control__select {
1967
+ width: 100%;
1968
+ min-height: 3.4rem;
1969
+ border-radius: 10px;
1970
+ border: 1px solid rgba(15, 23, 42, 0.12);
1971
+ background: #ffffff;
1972
+ padding: 0.9rem 2.6rem 0.9rem 1.1rem;
1973
+ font-size: 15px;
1974
+ font-weight: 500;
1975
+ box-shadow: none;
1976
+ outline: none;
1977
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
1978
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='8' viewBox='0 0 14 8' fill='none'><path d='M1 1.5L7 6.5L13 1.5' stroke='%236b7280' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
1979
+ background-repeat: no-repeat;
1980
+ background-position: right 0.9rem center;
1981
+ background-size: 14px 8px;
1982
+ appearance: none;
1983
+ }
1984
+
1985
+ .ui-control__select:focus {
1986
+ border-color: var(--color-primary);
1987
+ box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
1988
+ transform: translateY(-1px);
1989
+ }
1990
+
1991
+ .ui-control__select.invalid {
1992
+ border-color: #ef4444;
1993
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
1994
+ }
1995
+ `
1996
+ },
1997
+ {
1998
+ name: 'ui-checkbox',
1999
+ template: `
2000
+ import { Component, Input } from '@angular/core'
2001
+ import { NgIf } from '@angular/common'
2002
+ import {
2003
+ ControlContainer,
2004
+ FormGroupDirective,
2005
+ ReactiveFormsModule
2006
+ } from '@angular/forms'
2007
+
2008
+ @Component({
2009
+ selector: 'ui-checkbox',
2010
+ standalone: true,
2011
+ imports: [NgIf, ReactiveFormsModule],
2012
+ viewProviders: [
2013
+ { provide: ControlContainer, useExisting: FormGroupDirective }
2014
+ ],
2015
+ templateUrl: './ui-checkbox.component.html',
2016
+ styleUrls: ['./ui-checkbox.component.scss']
2017
+ })
2018
+ export class UiCheckboxComponent {
2019
+ @Input() label = ''
2020
+ @Input() hint = ''
2021
+ @Input() info = ''
2022
+ @Input() controlName = ''
2023
+ @Input() invalid = false
2024
+ infoOpen = false
2025
+
2026
+ toggleInfo(event: MouseEvent) {
2027
+ event.preventDefault()
2028
+ event.stopPropagation()
2029
+ this.infoOpen = !this.infoOpen
2030
+ }
2031
+ }
2032
+ `,
2033
+ html: `
2034
+ <label class="ui-control">
2035
+ <span class="ui-control__label" *ngIf="label">
2036
+ {{ label }}
2037
+ <button
2038
+ class="ui-control__info"
2039
+ type="button"
2040
+ *ngIf="info"
2041
+ (click)="toggleInfo($event)"
2042
+ [attr.aria-expanded]="infoOpen"
2043
+ >
2044
+ i
2045
+ </button>
2046
+ </span>
2047
+ <div class="ui-control__info-panel" *ngIf="info && infoOpen">
2048
+ {{ info }}
2049
+ </div>
2050
+ <input
2051
+ class="ui-control__checkbox"
2052
+ type="checkbox"
2053
+ [formControlName]="controlName"
2054
+ [class.invalid]="invalid"
2055
+ />
2056
+ <span class="ui-control__hint" *ngIf="hint && !info">{{ hint }}</span>
2057
+ </label>
2058
+ `,
2059
+ scss: `
2060
+ :host {
2061
+ display: block;
2062
+ }
2063
+
2064
+ .ui-control {
2065
+ display: grid;
2066
+ gap: 10px;
2067
+ font-size: 13px;
2068
+ color: #1f2937;
2069
+ }
2070
+
2071
+ .ui-control__label {
2072
+ font-weight: 700;
2073
+ line-height: 1.4;
2074
+ word-break: break-word;
2075
+ letter-spacing: 0.01em;
2076
+ }
2077
+
2078
+ .ui-control__hint {
2079
+ color: #94a3b8;
2080
+ font-size: 12px;
2081
+ }
2082
+
2083
+ .ui-control__info {
2084
+ margin-left: 8px;
2085
+ width: 18px;
2086
+ height: 18px;
2087
+ border-radius: 999px;
2088
+ border: 1px solid rgba(15, 23, 42, 0.2);
2089
+ background: #ffffff;
2090
+ color: #475569;
2091
+ font-size: 11px;
2092
+ line-height: 1;
2093
+ display: inline-flex;
2094
+ align-items: center;
2095
+ justify-content: center;
2096
+ cursor: pointer;
2097
+ }
2098
+
2099
+ .ui-control__info:hover {
2100
+ background: #f8fafc;
2101
+ }
2102
+
2103
+ .ui-control__info-panel {
2104
+ margin-top: 8px;
2105
+ padding: 10px 12px;
2106
+ border-radius: 10px;
2107
+ background: #f8fafc;
2108
+ border: 1px solid #e2e8f0;
2109
+ color: #475569;
2110
+ font-size: 12px;
2111
+ line-height: 1.4;
2112
+ }
2113
+
2114
+ .ui-control__checkbox {
2115
+ width: 20px;
2116
+ height: 20px;
2117
+ padding: 0;
2118
+ border-radius: 6px;
2119
+ box-shadow: none;
2120
+ accent-color: var(--color-primary);
2121
+ }
2122
+ `
2123
+ }
2124
+ ];
2125
+ fs_1.default.mkdirSync(uiRoot, { recursive: true });
2126
+ for (const component of components) {
2127
+ const componentDir = path_1.default.join(uiRoot, component.name);
2128
+ fs_1.default.mkdirSync(componentDir, { recursive: true });
2129
+ const base = component.name;
2130
+ const tsPath = path_1.default.join(componentDir, `${base}.component.ts`);
2131
+ const htmlPath = path_1.default.join(componentDir, `${base}.component.html`);
2132
+ const scssPath = path_1.default.join(componentDir, `${base}.component.scss`);
2133
+ const needsUiFieldUpdate = component.name === 'ui-field';
2134
+ const shouldOverwrite = (filePath, marker) => {
2135
+ if (!fs_1.default.existsSync(filePath))
2136
+ return true;
2137
+ if (needsUiFieldUpdate)
2138
+ return true;
2139
+ if (!marker)
2140
+ return true;
2141
+ const existing = fs_1.default.readFileSync(filePath, 'utf-8');
2142
+ return !existing.includes(marker);
2143
+ };
2144
+ if (component.name === 'ui-menu') {
2145
+ const menuImportPath = schemasRoot
2146
+ ? buildRelativeImportPath(componentDir, path_1.default.join(schemasRoot, 'menu.gen'))
2147
+ : './menu.gen';
2148
+ component.template = `
2149
+ import { Component, Input } from '@angular/core'
2150
+ import { NgFor, NgIf } from '@angular/common'
2151
+ import { RouterLink, RouterLinkActive } from '@angular/router'
2152
+ import { GeneratedMenu, generatedMenu } from '${menuImportPath}'
2153
+
2154
+ @Component({
2155
+ selector: 'ui-menu',
2156
+ standalone: true,
2157
+ imports: [NgIf, NgFor, RouterLink, RouterLinkActive],
2158
+ templateUrl: './ui-menu.component.html',
2159
+ styleUrls: ['./ui-menu.component.scss']
2160
+ })
2161
+ export class UiMenuComponent {
2162
+ @Input() menu: GeneratedMenu = generatedMenu
2163
+ @Input() title = 'Generate UI'
2164
+ }
2165
+ `;
2166
+ }
2167
+ if (shouldOverwrite(tsPath, 'infoOpen')) {
2168
+ fs_1.default.writeFileSync(tsPath, component.template.trimStart());
2169
+ }
2170
+ if (shouldOverwrite(htmlPath, 'ui-control__info-panel')) {
2171
+ fs_1.default.writeFileSync(htmlPath, component.html.trimStart());
2172
+ }
2173
+ if (shouldOverwrite(scssPath, 'ui-control__info-panel')) {
2174
+ fs_1.default.writeFileSync(scssPath, component.scss.trimStart());
2175
+ }
2176
+ }
2177
+ }
2178
+ function toRouteSegment(operationId) {
1212
2179
  if (!operationId)
1213
2180
  return operationId;
1214
2181
  return operationId[0].toLowerCase() + operationId.slice(1);
@@ -1243,6 +2210,14 @@ function buildSchemaImportPath(featureDir, schemasRoot, rawName) {
1243
2210
  }
1244
2211
  return relativePath;
1245
2212
  }
2213
+ function buildRelativeImportPath(fromDir, targetFile) {
2214
+ let relativePath = path_1.default.relative(fromDir, targetFile);
2215
+ relativePath = toPosixPath(relativePath);
2216
+ if (!relativePath.startsWith('.')) {
2217
+ relativePath = `./${relativePath}`;
2218
+ }
2219
+ return relativePath;
2220
+ }
1246
2221
  function toPosixPath(value) {
1247
2222
  return value.split(path_1.default.sep).join(path_1.default.posix.sep);
1248
2223
  }