generate-ui-cli 1.0.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.
- package/README.md +32 -0
- package/dist/commands/angular.js +39 -0
- package/dist/commands/generate.js +207 -0
- package/dist/commands/login.js +132 -0
- package/dist/generators/angular/feature.generator.js +1211 -0
- package/dist/generators/angular/routes.generator.js +45 -0
- package/dist/generators/form.generator.js +70 -0
- package/dist/generators/infer-entity.js +20 -0
- package/dist/generators/infer-submit-wrapper.js +14 -0
- package/dist/generators/screen.generator.js +134 -0
- package/dist/generators/screen.merge.js +202 -0
- package/dist/index.js +105 -0
- package/dist/license/device.js +58 -0
- package/dist/license/guard.js +9 -0
- package/dist/license/permissions.js +93 -0
- package/dist/license/token.js +46 -0
- package/dist/openapi/load-openapi.js +10 -0
- package/dist/overlay/sync-overlay.js +26 -0
- package/dist/runtime/config.js +20 -0
- package/dist/runtime/open-browser.js +26 -0
- package/dist/telemetry.js +40 -0
- package/package.json +53 -0
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateFeature = generateFeature;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function generateFeature(schema, root) {
|
|
10
|
+
const rawName = schema.api.operationId;
|
|
11
|
+
const name = toPascalCase(rawName);
|
|
12
|
+
const folder = toFolderName(name);
|
|
13
|
+
const fileBase = toFileBase(name);
|
|
14
|
+
const featureDir = path_1.default.join(root, folder);
|
|
15
|
+
fs_1.default.mkdirSync(featureDir, { recursive: true });
|
|
16
|
+
const appRoot = path_1.default.resolve(root, '..');
|
|
17
|
+
ensureUiComponents(appRoot);
|
|
18
|
+
const method = String(schema.api.method || '').toLowerCase();
|
|
19
|
+
const endpoint = String(schema.api.endpoint || '');
|
|
20
|
+
const baseUrl = String(schema.api.baseUrl || 'https://api.realworld.io/api');
|
|
21
|
+
const pathParams = extractPathParams(endpoint);
|
|
22
|
+
const queryParams = normalizeQueryParams(schema.api?.queryParams || []);
|
|
23
|
+
const bodyFields = normalizeFields(schema.fields || []);
|
|
24
|
+
const paramFields = pathParams.map(param => ({
|
|
25
|
+
name: param,
|
|
26
|
+
type: 'string',
|
|
27
|
+
required: true,
|
|
28
|
+
label: toLabel(param),
|
|
29
|
+
placeholder: toPlaceholder(param),
|
|
30
|
+
source: 'path'
|
|
31
|
+
}));
|
|
32
|
+
const normalizedQueryFields = queryParams.map(field => ({
|
|
33
|
+
...field,
|
|
34
|
+
source: 'query'
|
|
35
|
+
}));
|
|
36
|
+
const includeBody = ['post', 'put', 'patch'].includes(method);
|
|
37
|
+
const includeParams = pathParams.length > 0 || queryParams.length > 0;
|
|
38
|
+
const formFields = [
|
|
39
|
+
...(includeParams
|
|
40
|
+
? [...paramFields, ...normalizedQueryFields]
|
|
41
|
+
: []),
|
|
42
|
+
...(includeBody ? bodyFields : [])
|
|
43
|
+
];
|
|
44
|
+
const actionLabel = schema.actions?.primary?.label ||
|
|
45
|
+
defaultActionLabel(method, includeParams);
|
|
46
|
+
const title = schema.entity && String(schema.entity).trim()
|
|
47
|
+
? String(schema.entity).trim()
|
|
48
|
+
: rawName;
|
|
49
|
+
const subtitle = `${method.toUpperCase()} ${endpoint}`;
|
|
50
|
+
/**
|
|
51
|
+
* 1️⃣ Component (sempre sobrescreve)
|
|
52
|
+
*/
|
|
53
|
+
const componentPath = path_1.default.join(featureDir, `${fileBase}.component.ts`);
|
|
54
|
+
fs_1.default.writeFileSync(componentPath, `
|
|
55
|
+
import { Component } from '@angular/core'
|
|
56
|
+
import { JsonPipe, NgFor, NgIf } from '@angular/common'
|
|
57
|
+
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'
|
|
58
|
+
import { UiCardComponent } from '../../ui/ui-card/ui-card.component'
|
|
59
|
+
import { UiFieldComponent } from '../../ui/ui-field/ui-field.component'
|
|
60
|
+
import { UiButtonComponent } from '../../ui/ui-button/ui-button.component'
|
|
61
|
+
import { ${name}Service } from './${fileBase}.service.gen'
|
|
62
|
+
import { ${name}Gen } from './${fileBase}.gen'
|
|
63
|
+
import screenSchema from '../../assets/generate-ui/overlays/${rawName}.screen.json'
|
|
64
|
+
|
|
65
|
+
@Component({
|
|
66
|
+
selector: 'app-${toKebab(name)}',
|
|
67
|
+
standalone: true,
|
|
68
|
+
imports: [
|
|
69
|
+
NgIf,
|
|
70
|
+
NgFor,
|
|
71
|
+
JsonPipe,
|
|
72
|
+
ReactiveFormsModule,
|
|
73
|
+
UiCardComponent,
|
|
74
|
+
UiFieldComponent,
|
|
75
|
+
UiButtonComponent
|
|
76
|
+
],
|
|
77
|
+
templateUrl: './${fileBase}.component.html',
|
|
78
|
+
styleUrls: ['./${fileBase}.component.scss']
|
|
79
|
+
})
|
|
80
|
+
export class ${name}Component extends ${name}Gen {
|
|
81
|
+
constructor(
|
|
82
|
+
protected override fb: FormBuilder,
|
|
83
|
+
protected override service: ${name}Service
|
|
84
|
+
) {
|
|
85
|
+
super(fb, service)
|
|
86
|
+
this.setSchema(screenSchema as any)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
submit() {
|
|
90
|
+
const value = this.form.getRawValue()
|
|
91
|
+
const pathParams = this.pick(value, this.pathParamNames)
|
|
92
|
+
const queryParams = this.pick(value, this.queryParamNames)
|
|
93
|
+
const body = this.pick(value, this.bodyFieldNames)
|
|
94
|
+
|
|
95
|
+
this.loading = true
|
|
96
|
+
this.error = null
|
|
97
|
+
|
|
98
|
+
this.service
|
|
99
|
+
.execute(pathParams, queryParams, body)
|
|
100
|
+
.subscribe({
|
|
101
|
+
next: result => {
|
|
102
|
+
this.result = result
|
|
103
|
+
this.loading = false
|
|
104
|
+
},
|
|
105
|
+
error: error => {
|
|
106
|
+
this.error = error
|
|
107
|
+
this.loading = false
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
isArrayResult() {
|
|
113
|
+
return this.getRows().length > 0
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getRows() {
|
|
117
|
+
const value = this.result
|
|
118
|
+
if (Array.isArray(value)) return value
|
|
119
|
+
if (!value || typeof value !== 'object') return []
|
|
120
|
+
|
|
121
|
+
const commonKeys = ['data', 'items', 'results', 'list', 'records']
|
|
122
|
+
for (const key of commonKeys) {
|
|
123
|
+
if (Array.isArray(value[key])) return value[key]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const key of Object.keys(value)) {
|
|
127
|
+
if (Array.isArray(value[key])) return value[key]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return []
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getColumns() {
|
|
134
|
+
const raw = this.form.get('fields')?.value
|
|
135
|
+
if (typeof raw === 'string' && raw.trim().length > 0) {
|
|
136
|
+
return raw
|
|
137
|
+
.split(',')
|
|
138
|
+
.map((value: string) => value.trim())
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const rows = this.getRows()
|
|
143
|
+
if (rows.length > 0 && rows[0] && typeof rows[0] === 'object') {
|
|
144
|
+
return Object.keys(rows[0])
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return []
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
formatHeader(value: string) {
|
|
151
|
+
return value
|
|
152
|
+
.replace(/[_-]/g, ' ')
|
|
153
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
154
|
+
.replace(/\b\w/g, char => char.toUpperCase())
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getCellValue(row: any, column: string) {
|
|
158
|
+
if (!row || !column) return ''
|
|
159
|
+
|
|
160
|
+
if (column.includes('.')) {
|
|
161
|
+
return column
|
|
162
|
+
.split('.')
|
|
163
|
+
.reduce((acc, key) => (acc ? acc[key] : undefined), row) ?? ''
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const value = row[column]
|
|
167
|
+
return this.formatValue(value)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
isImageCell(row: any, column: string) {
|
|
171
|
+
const value = this.getCellValue(row, column)
|
|
172
|
+
return (
|
|
173
|
+
typeof value === 'string' &&
|
|
174
|
+
/^https?:\\/\\//.test(value) &&
|
|
175
|
+
/(\\.png|\\.jpg|\\.jpeg|\\.svg)/i.test(value)
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private formatValue(value: any): string {
|
|
180
|
+
if (value === null || value === undefined) return ''
|
|
181
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
182
|
+
return String(value)
|
|
183
|
+
}
|
|
184
|
+
if (typeof value === 'boolean') {
|
|
185
|
+
return value ? 'Yes' : 'No'
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(value)) {
|
|
188
|
+
return value
|
|
189
|
+
.map((item: any) => this.formatValue(item))
|
|
190
|
+
.join(', ')
|
|
191
|
+
}
|
|
192
|
+
if (typeof value === 'object') {
|
|
193
|
+
if (typeof value.common === 'string') return value.common
|
|
194
|
+
if (typeof value.official === 'string') return value.official
|
|
195
|
+
if (typeof value.name === 'string') return value.name
|
|
196
|
+
if (typeof value.label === 'string') return value.label
|
|
197
|
+
return JSON.stringify(value)
|
|
198
|
+
}
|
|
199
|
+
return String(value)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getObjectRows() {
|
|
203
|
+
const value = this.result
|
|
204
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
205
|
+
return []
|
|
206
|
+
}
|
|
207
|
+
return this.flattenObject(value)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private flattenObject(
|
|
211
|
+
value: Record<string, any>,
|
|
212
|
+
prefix = ''
|
|
213
|
+
) {
|
|
214
|
+
const rows: Array<{ key: string; value: string }> = []
|
|
215
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
216
|
+
const fullKey = prefix ? prefix + '.' + key : key
|
|
217
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
218
|
+
rows.push(...this.flattenObject(raw, fullKey))
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
rows.push({ key: fullKey, value: this.formatValue(raw) })
|
|
222
|
+
}
|
|
223
|
+
return rows
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
}
|
|
227
|
+
`);
|
|
228
|
+
/**
|
|
229
|
+
* 2️⃣ Arquivo gerado (sempre sobrescreve)
|
|
230
|
+
*/
|
|
231
|
+
const genTsPath = path_1.default.join(featureDir, `${fileBase}.gen.ts`);
|
|
232
|
+
fs_1.default.writeFileSync(genTsPath, `
|
|
233
|
+
import { FormBuilder, FormGroup, Validators } from '@angular/forms'
|
|
234
|
+
import { Injectable } from '@angular/core'
|
|
235
|
+
import { ${name}Service } from './${fileBase}.service.gen'
|
|
236
|
+
|
|
237
|
+
@Injectable()
|
|
238
|
+
export class ${name}Gen {
|
|
239
|
+
form!: FormGroup
|
|
240
|
+
formFields: any[] = []
|
|
241
|
+
protected pathParamNames: string[] = []
|
|
242
|
+
protected queryParamNames: string[] = []
|
|
243
|
+
protected bodyFieldNames: string[] = []
|
|
244
|
+
schema: any
|
|
245
|
+
|
|
246
|
+
loading = false
|
|
247
|
+
result: any = null
|
|
248
|
+
error: any = null
|
|
249
|
+
|
|
250
|
+
constructor(
|
|
251
|
+
protected fb: FormBuilder,
|
|
252
|
+
protected service: ${name}Service
|
|
253
|
+
) {
|
|
254
|
+
this.form = this.fb.group({})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
setSchema(schema: any) {
|
|
258
|
+
this.schema = schema
|
|
259
|
+
this.formFields = this.buildFormFields(schema)
|
|
260
|
+
this.form = this.fb.group({})
|
|
261
|
+
for (const field of this.formFields) {
|
|
262
|
+
const value = this.resolveDefault(field)
|
|
263
|
+
const validators = field.required ? [Validators.required] : []
|
|
264
|
+
this.form.addControl(
|
|
265
|
+
field.name,
|
|
266
|
+
this.fb.control(value, validators)
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
protected pick(source: Record<string, any>, keys: string[]) {
|
|
272
|
+
const out: Record<string, any> = {}
|
|
273
|
+
for (const key of keys) {
|
|
274
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
275
|
+
out[key] = source[key]
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return out
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
protected isSelect(field: any) {
|
|
282
|
+
return Array.isArray(field.options) && field.options.length > 0
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
protected isCheckbox(field: any) {
|
|
286
|
+
return field.type === 'boolean'
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
protected isTextarea(field: any) {
|
|
290
|
+
return /body|description|content/i.test(field.name)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
protected inputType(field: any) {
|
|
294
|
+
switch (field.type) {
|
|
295
|
+
case 'number':
|
|
296
|
+
case 'integer':
|
|
297
|
+
return 'number'
|
|
298
|
+
default:
|
|
299
|
+
return 'text'
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
protected isInvalid(field: any) {
|
|
304
|
+
const control = this.form.get(field.name)
|
|
305
|
+
return !!(control?.invalid && (control.touched || control.dirty))
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private buildFormFields(schema: any) {
|
|
309
|
+
const fields = Array.isArray(schema?.fields)
|
|
310
|
+
? schema.fields
|
|
311
|
+
.filter(
|
|
312
|
+
(field: any) =>
|
|
313
|
+
!field?.hidden && !field?.meta?.userRemoved
|
|
314
|
+
)
|
|
315
|
+
: []
|
|
316
|
+
|
|
317
|
+
const queryParams = Array.isArray(schema?.api?.queryParams)
|
|
318
|
+
? schema.api.queryParams.filter(
|
|
319
|
+
(field: any) =>
|
|
320
|
+
!field?.hidden && !field?.meta?.userRemoved
|
|
321
|
+
)
|
|
322
|
+
: []
|
|
323
|
+
|
|
324
|
+
const pathParamsSource = Array.isArray(schema?.api?.pathParams)
|
|
325
|
+
? schema.api.pathParams
|
|
326
|
+
: this.extractPathParams(schema?.api?.endpoint ?? '').map(
|
|
327
|
+
(name: string) => ({
|
|
328
|
+
name,
|
|
329
|
+
type: 'string',
|
|
330
|
+
required: true,
|
|
331
|
+
label: name,
|
|
332
|
+
placeholder: name,
|
|
333
|
+
source: 'path'
|
|
334
|
+
})
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
const pathParams = pathParamsSource.filter(
|
|
338
|
+
(field: any) =>
|
|
339
|
+
!field?.hidden && !field?.meta?.userRemoved
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
this.pathParamNames = pathParams.map((p: any) => p.name)
|
|
343
|
+
this.queryParamNames = queryParams.map((p: any) => p.name)
|
|
344
|
+
this.bodyFieldNames = fields.map((f: any) => f.name)
|
|
345
|
+
|
|
346
|
+
return [...pathParams, ...queryParams, ...fields]
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private extractPathParams(endpoint: string) {
|
|
350
|
+
const params = []
|
|
351
|
+
const regex = /{([^}]+)}/g
|
|
352
|
+
let match = regex.exec(endpoint)
|
|
353
|
+
while (match) {
|
|
354
|
+
params.push(match[1])
|
|
355
|
+
match = regex.exec(endpoint)
|
|
356
|
+
}
|
|
357
|
+
return params
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private resolveDefault(field: any) {
|
|
361
|
+
if (field.defaultValue !== null && field.defaultValue !== undefined) {
|
|
362
|
+
return field.defaultValue
|
|
363
|
+
}
|
|
364
|
+
switch (field.type) {
|
|
365
|
+
case 'array':
|
|
366
|
+
return []
|
|
367
|
+
case 'boolean':
|
|
368
|
+
return false
|
|
369
|
+
case 'number':
|
|
370
|
+
case 'integer':
|
|
371
|
+
return null
|
|
372
|
+
default:
|
|
373
|
+
return ''
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
`);
|
|
378
|
+
/**
|
|
379
|
+
* 3️⃣ Service gerado
|
|
380
|
+
*/
|
|
381
|
+
const wrap = schema.api?.submit?.wrap;
|
|
382
|
+
const servicePath = path_1.default.join(featureDir, `${fileBase}.service.gen.ts`);
|
|
383
|
+
const httpCall = httpCallForMethod(method);
|
|
384
|
+
fs_1.default.writeFileSync(servicePath, `
|
|
385
|
+
import { Injectable } from '@angular/core'
|
|
386
|
+
import { HttpClient } from '@angular/common/http'
|
|
387
|
+
|
|
388
|
+
@Injectable({ providedIn: 'root' })
|
|
389
|
+
export class ${name}Service {
|
|
390
|
+
private readonly baseUrl = '${baseUrl}'
|
|
391
|
+
private readonly endpoint = '${endpoint}'
|
|
392
|
+
private readonly pathParams = ${JSON.stringify(pathParams)}
|
|
393
|
+
|
|
394
|
+
constructor(private http: HttpClient) {}
|
|
395
|
+
|
|
396
|
+
execute(
|
|
397
|
+
pathParams: Record<string, any>,
|
|
398
|
+
queryParams: Record<string, any>,
|
|
399
|
+
payload: Record<string, any>
|
|
400
|
+
) {
|
|
401
|
+
const url = this.buildUrl(pathParams, queryParams)
|
|
402
|
+
const body = this.buildBody(payload)
|
|
403
|
+
${httpCall}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private buildUrl(
|
|
407
|
+
pathParams: Record<string, any>,
|
|
408
|
+
queryParams: Record<string, any>
|
|
409
|
+
) {
|
|
410
|
+
let url = \`\${this.baseUrl}\${this.endpoint}\`
|
|
411
|
+
for (const key of this.pathParams) {
|
|
412
|
+
const value = pathParams?.[key]
|
|
413
|
+
url = url.replace(\`{\${key}}\`, encodeURIComponent(String(value)))
|
|
414
|
+
}
|
|
415
|
+
const query = this.buildQuery(queryParams)
|
|
416
|
+
if (query) {
|
|
417
|
+
url += \`?\${query}\`
|
|
418
|
+
}
|
|
419
|
+
return url
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private buildQuery(queryParams: Record<string, any>) {
|
|
423
|
+
const params = new URLSearchParams()
|
|
424
|
+
for (const key of Object.keys(queryParams || {})) {
|
|
425
|
+
const value = queryParams[key]
|
|
426
|
+
if (value === undefined || value === null || value === '') continue
|
|
427
|
+
const out = Array.isArray(value) ? value.join(',') : String(value)
|
|
428
|
+
params.set(key, out)
|
|
429
|
+
}
|
|
430
|
+
return params.toString()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private buildBody(payload: Record<string, any>) {
|
|
434
|
+
const cleaned = payload ?? {}
|
|
435
|
+
${wrap ? `return { ${wrap}: cleaned }` : 'return cleaned'}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
`);
|
|
439
|
+
/**
|
|
440
|
+
* 4️⃣ HTML base (sempre sobrescreve)
|
|
441
|
+
*/
|
|
442
|
+
const htmlPath = path_1.default.join(featureDir, `${fileBase}.component.html`);
|
|
443
|
+
fs_1.default.writeFileSync(htmlPath, buildComponentHtml({
|
|
444
|
+
title,
|
|
445
|
+
subtitle,
|
|
446
|
+
formFields,
|
|
447
|
+
actionLabel,
|
|
448
|
+
method,
|
|
449
|
+
hasForm: formFields.length > 0
|
|
450
|
+
}));
|
|
451
|
+
/**
|
|
452
|
+
* 5️⃣ SCSS base
|
|
453
|
+
*/
|
|
454
|
+
const scssPath = path_1.default.join(featureDir, `${fileBase}.component.scss`);
|
|
455
|
+
fs_1.default.writeFileSync(scssPath, `
|
|
456
|
+
:host {
|
|
457
|
+
display: block;
|
|
458
|
+
padding: 24px;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.page {
|
|
462
|
+
display: grid;
|
|
463
|
+
gap: 16px;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.screen-description {
|
|
467
|
+
margin: 0 0 18px;
|
|
468
|
+
color: #6b7280;
|
|
469
|
+
font-size: 14px;
|
|
470
|
+
line-height: 1.5;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.form-grid {
|
|
474
|
+
display: grid;
|
|
475
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
476
|
+
gap: 18px;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.actions {
|
|
480
|
+
display: flex;
|
|
481
|
+
justify-content: flex-end;
|
|
482
|
+
gap: 14px;
|
|
483
|
+
margin-top: 20px;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.result {
|
|
487
|
+
margin-top: 20px;
|
|
488
|
+
padding: 16px;
|
|
489
|
+
border-radius: 12px;
|
|
490
|
+
background: #0f172a;
|
|
491
|
+
color: #e2e8f0;
|
|
492
|
+
font-size: 12px;
|
|
493
|
+
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.25);
|
|
494
|
+
overflow: auto;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.result-table {
|
|
498
|
+
margin-top: 20px;
|
|
499
|
+
overflow: hidden;
|
|
500
|
+
border-radius: 16px;
|
|
501
|
+
border: 1px solid #e2e8f0;
|
|
502
|
+
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.result-card {
|
|
506
|
+
margin-top: 20px;
|
|
507
|
+
border-radius: 16px;
|
|
508
|
+
border: 1px solid #e2e8f0;
|
|
509
|
+
background: #ffffff;
|
|
510
|
+
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
|
|
511
|
+
padding: 18px;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.result-card__grid {
|
|
515
|
+
display: grid;
|
|
516
|
+
gap: 12px;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.result-card__row {
|
|
520
|
+
display: flex;
|
|
521
|
+
justify-content: space-between;
|
|
522
|
+
gap: 16px;
|
|
523
|
+
border-bottom: 1px solid #e2e8f0;
|
|
524
|
+
padding-bottom: 10px;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.result-card__row:last-child {
|
|
528
|
+
border-bottom: none;
|
|
529
|
+
padding-bottom: 0;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.result-card__label {
|
|
533
|
+
font-weight: 600;
|
|
534
|
+
color: #475569;
|
|
535
|
+
font-size: 12px;
|
|
536
|
+
letter-spacing: 0.08em;
|
|
537
|
+
text-transform: uppercase;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.result-card__value {
|
|
541
|
+
color: #0f172a;
|
|
542
|
+
font-weight: 600;
|
|
543
|
+
text-align: right;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.data-table {
|
|
547
|
+
width: 100%;
|
|
548
|
+
border-collapse: collapse;
|
|
549
|
+
background: #ffffff;
|
|
550
|
+
font-size: 14px;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.data-table thead {
|
|
554
|
+
background: #f8fafc;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.data-table th,
|
|
558
|
+
.data-table td {
|
|
559
|
+
padding: 12px 14px;
|
|
560
|
+
text-align: left;
|
|
561
|
+
border-bottom: 1px solid #e2e8f0;
|
|
562
|
+
color: #0f172a;
|
|
563
|
+
vertical-align: middle;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.data-table th {
|
|
567
|
+
font-weight: 700;
|
|
568
|
+
font-size: 12px;
|
|
569
|
+
letter-spacing: 0.08em;
|
|
570
|
+
text-transform: uppercase;
|
|
571
|
+
color: #475569;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.data-table tbody tr:hover {
|
|
575
|
+
background: #f1f5f9;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.cell-image {
|
|
579
|
+
width: 44px;
|
|
580
|
+
height: 28px;
|
|
581
|
+
object-fit: cover;
|
|
582
|
+
border-radius: 6px;
|
|
583
|
+
box-shadow: 0 6px 12px rgba(15, 23, 42, 0.16);
|
|
584
|
+
}
|
|
585
|
+
`);
|
|
586
|
+
return {
|
|
587
|
+
path: toRouteSegment(name),
|
|
588
|
+
component: `${name}Component`,
|
|
589
|
+
folder,
|
|
590
|
+
fileBase
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function normalizeFields(fields) {
|
|
594
|
+
return fields.map(field => ({
|
|
595
|
+
name: field.name,
|
|
596
|
+
type: field.type || 'string',
|
|
597
|
+
required: Boolean(field.required),
|
|
598
|
+
label: field.label || toLabel(field.name),
|
|
599
|
+
placeholder: field.placeholder || toPlaceholder(field.name),
|
|
600
|
+
hint: field.hint || undefined,
|
|
601
|
+
info: field.info || undefined,
|
|
602
|
+
options: field.options || null,
|
|
603
|
+
defaultValue: field.defaultValue ?? null,
|
|
604
|
+
source: 'body'
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
607
|
+
function normalizeQueryParams(params) {
|
|
608
|
+
return params.map(param => {
|
|
609
|
+
const labelText = param.label || param.name;
|
|
610
|
+
const hintText = param.hint ||
|
|
611
|
+
(typeof labelText === 'string' && labelText.length > 60
|
|
612
|
+
? labelText
|
|
613
|
+
: '');
|
|
614
|
+
const help = resolveFieldHelp(hintText, labelText);
|
|
615
|
+
return {
|
|
616
|
+
name: param.name,
|
|
617
|
+
type: param.type || 'string',
|
|
618
|
+
required: Boolean(param.required),
|
|
619
|
+
label: toLabel(param.name),
|
|
620
|
+
placeholder: param.placeholder || toPlaceholder(param.name),
|
|
621
|
+
hint: help.hint,
|
|
622
|
+
info: help.info,
|
|
623
|
+
options: param.options || null,
|
|
624
|
+
defaultValue: param.defaultValue ?? null,
|
|
625
|
+
source: 'query'
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
function extractPathParams(endpoint) {
|
|
630
|
+
const params = [];
|
|
631
|
+
const regex = /{([^}]+)}/g;
|
|
632
|
+
let match = regex.exec(endpoint);
|
|
633
|
+
while (match) {
|
|
634
|
+
params.push(match[1]);
|
|
635
|
+
match = regex.exec(endpoint);
|
|
636
|
+
}
|
|
637
|
+
return params;
|
|
638
|
+
}
|
|
639
|
+
function buildFormControls(fields) {
|
|
640
|
+
if (fields.length === 0)
|
|
641
|
+
return '';
|
|
642
|
+
return fields
|
|
643
|
+
.map(field => {
|
|
644
|
+
const value = field.defaultValue !== null && field.defaultValue !== undefined
|
|
645
|
+
? JSON.stringify(field.defaultValue)
|
|
646
|
+
: defaultValueFor(field.type);
|
|
647
|
+
const validators = field.required ? ', Validators.required' : '';
|
|
648
|
+
return ` ${field.name}: [${value}${validators}]`;
|
|
649
|
+
})
|
|
650
|
+
.join(',\n');
|
|
651
|
+
}
|
|
652
|
+
function defaultValueFor(type) {
|
|
653
|
+
switch (type) {
|
|
654
|
+
case 'array':
|
|
655
|
+
return '[]';
|
|
656
|
+
case 'boolean':
|
|
657
|
+
return 'false';
|
|
658
|
+
case 'number':
|
|
659
|
+
case 'integer':
|
|
660
|
+
return 'null';
|
|
661
|
+
default:
|
|
662
|
+
return "''";
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function toLabel(value) {
|
|
666
|
+
return String(value)
|
|
667
|
+
.replace(/[_-]/g, ' ')
|
|
668
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
669
|
+
.replace(/\b\w/g, char => char.toUpperCase());
|
|
670
|
+
}
|
|
671
|
+
function toPlaceholder(value) {
|
|
672
|
+
return toLabel(value);
|
|
673
|
+
}
|
|
674
|
+
function normalizeWhitespace(value) {
|
|
675
|
+
return String(value).replace(/\s+/g, ' ').trim();
|
|
676
|
+
}
|
|
677
|
+
function resolveFieldHelp(rawHint, label) {
|
|
678
|
+
const hint = normalizeWhitespace(rawHint || '');
|
|
679
|
+
if (!hint)
|
|
680
|
+
return { hint: undefined, info: undefined };
|
|
681
|
+
if (hint.length > 120) {
|
|
682
|
+
return { hint: undefined, info: hint };
|
|
683
|
+
}
|
|
684
|
+
return { hint, info: undefined };
|
|
685
|
+
}
|
|
686
|
+
function escapeAttr(value) {
|
|
687
|
+
return String(value).replace(/"/g, '"');
|
|
688
|
+
}
|
|
689
|
+
function defaultActionLabel(method, hasParams) {
|
|
690
|
+
switch (method) {
|
|
691
|
+
case 'get':
|
|
692
|
+
return hasParams ? 'Buscar' : 'Carregar';
|
|
693
|
+
case 'post':
|
|
694
|
+
return 'Criar';
|
|
695
|
+
case 'put':
|
|
696
|
+
case 'patch':
|
|
697
|
+
return 'Salvar';
|
|
698
|
+
case 'delete':
|
|
699
|
+
return 'Excluir';
|
|
700
|
+
default:
|
|
701
|
+
return 'Executar';
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function buildComponentHtml(options) {
|
|
705
|
+
const buttonVariant = options.method === 'delete' ? 'danger' : 'primary';
|
|
706
|
+
if (!options.hasForm) {
|
|
707
|
+
return `
|
|
708
|
+
<div class="page">
|
|
709
|
+
<ui-card title="${options.title}" subtitle="${options.subtitle}">
|
|
710
|
+
<div class="actions">
|
|
711
|
+
<ui-button
|
|
712
|
+
variant="${buttonVariant}"
|
|
713
|
+
[disabled]="form.invalid"
|
|
714
|
+
(click)="submit()"
|
|
715
|
+
>
|
|
716
|
+
${options.actionLabel}
|
|
717
|
+
</ui-button>
|
|
718
|
+
</div>
|
|
719
|
+
</ui-card>
|
|
720
|
+
</div>
|
|
721
|
+
`;
|
|
722
|
+
}
|
|
723
|
+
return `
|
|
724
|
+
<div class="page">
|
|
725
|
+
<ui-card [title]="schema.entity || schema.api.operationId" [subtitle]="schema.api.method.toUpperCase() + ' ' + schema.api.endpoint">
|
|
726
|
+
<p class="screen-description" *ngIf="schema.description">
|
|
727
|
+
{{ schema.description }}
|
|
728
|
+
</p>
|
|
729
|
+
<form [formGroup]="form" (ngSubmit)="submit()">
|
|
730
|
+
<div class="form-grid">
|
|
731
|
+
<ui-field
|
|
732
|
+
*ngFor="let field of formFields"
|
|
733
|
+
[label]="field.label || field.name"
|
|
734
|
+
[hint]="field.hint"
|
|
735
|
+
[info]="field.info"
|
|
736
|
+
>
|
|
737
|
+
<select
|
|
738
|
+
*ngIf="isSelect(field)"
|
|
739
|
+
[formControlName]="field.name"
|
|
740
|
+
[class.invalid]="isInvalid(field)"
|
|
741
|
+
>
|
|
742
|
+
<option
|
|
743
|
+
*ngFor="let option of field.options"
|
|
744
|
+
[value]="option"
|
|
745
|
+
>
|
|
746
|
+
{{ option }}
|
|
747
|
+
</option>
|
|
748
|
+
</select>
|
|
749
|
+
|
|
750
|
+
<textarea
|
|
751
|
+
*ngIf="isTextarea(field)"
|
|
752
|
+
rows="4"
|
|
753
|
+
[formControlName]="field.name"
|
|
754
|
+
[placeholder]="field.placeholder || field.label || field.name"
|
|
755
|
+
[class.invalid]="isInvalid(field)"
|
|
756
|
+
></textarea>
|
|
757
|
+
|
|
758
|
+
<input
|
|
759
|
+
*ngIf="isCheckbox(field)"
|
|
760
|
+
type="checkbox"
|
|
761
|
+
[formControlName]="field.name"
|
|
762
|
+
/>
|
|
763
|
+
|
|
764
|
+
<input
|
|
765
|
+
*ngIf="!isSelect(field) && !isTextarea(field) && !isCheckbox(field)"
|
|
766
|
+
[type]="inputType(field)"
|
|
767
|
+
[formControlName]="field.name"
|
|
768
|
+
[placeholder]="field.placeholder || field.label || field.name"
|
|
769
|
+
[class.invalid]="isInvalid(field)"
|
|
770
|
+
/>
|
|
771
|
+
|
|
772
|
+
<span class="field-error" *ngIf="isInvalid(field)">
|
|
773
|
+
Campo obrigatório
|
|
774
|
+
</span>
|
|
775
|
+
</ui-field>
|
|
776
|
+
</div>
|
|
777
|
+
<div class="actions">
|
|
778
|
+
<ui-button
|
|
779
|
+
type="submit"
|
|
780
|
+
variant="${buttonVariant}"
|
|
781
|
+
[disabled]="form.invalid"
|
|
782
|
+
>
|
|
783
|
+
${options.actionLabel}
|
|
784
|
+
</ui-button>
|
|
785
|
+
</div>
|
|
786
|
+
</form>
|
|
787
|
+
</ui-card>
|
|
788
|
+
|
|
789
|
+
<div class="result-table" *ngIf="isArrayResult()">
|
|
790
|
+
<table class="data-table">
|
|
791
|
+
<thead>
|
|
792
|
+
<tr>
|
|
793
|
+
<th *ngFor="let column of getColumns()">
|
|
794
|
+
{{ formatHeader(column) }}
|
|
795
|
+
</th>
|
|
796
|
+
</tr>
|
|
797
|
+
</thead>
|
|
798
|
+
<tbody>
|
|
799
|
+
<tr *ngFor="let row of getRows()">
|
|
800
|
+
<td *ngFor="let column of getColumns()">
|
|
801
|
+
<img
|
|
802
|
+
*ngIf="isImageCell(row, column)"
|
|
803
|
+
[src]="getCellValue(row, column)"
|
|
804
|
+
[alt]="formatHeader(column)"
|
|
805
|
+
class="cell-image"
|
|
806
|
+
/>
|
|
807
|
+
<span *ngIf="!isImageCell(row, column)">
|
|
808
|
+
{{ getCellValue(row, column) }}
|
|
809
|
+
</span>
|
|
810
|
+
</td>
|
|
811
|
+
</tr>
|
|
812
|
+
</tbody>
|
|
813
|
+
</table>
|
|
814
|
+
</div>
|
|
815
|
+
|
|
816
|
+
<div class="result-card" *ngIf="!isArrayResult() && result">
|
|
817
|
+
<div class="result-card__grid">
|
|
818
|
+
<div class="result-card__row" *ngFor="let row of getObjectRows()">
|
|
819
|
+
<span class="result-card__label">
|
|
820
|
+
{{ formatHeader(row.key) }}
|
|
821
|
+
</span>
|
|
822
|
+
<span class="result-card__value">
|
|
823
|
+
{{ row.value }}
|
|
824
|
+
</span>
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
`;
|
|
830
|
+
}
|
|
831
|
+
function buildFieldHtml(field) {
|
|
832
|
+
return '';
|
|
833
|
+
}
|
|
834
|
+
function inputTypeFor(type) {
|
|
835
|
+
switch (type) {
|
|
836
|
+
case 'number':
|
|
837
|
+
case 'integer':
|
|
838
|
+
return 'number';
|
|
839
|
+
case 'boolean':
|
|
840
|
+
return 'checkbox';
|
|
841
|
+
default:
|
|
842
|
+
return 'text';
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function httpCallForMethod(method) {
|
|
846
|
+
switch (method) {
|
|
847
|
+
case 'get':
|
|
848
|
+
return 'return this.http.get(url)';
|
|
849
|
+
case 'delete':
|
|
850
|
+
return 'return this.http.delete(url)';
|
|
851
|
+
case 'post':
|
|
852
|
+
return 'return this.http.post(url, body)';
|
|
853
|
+
case 'put':
|
|
854
|
+
return 'return this.http.put(url, body)';
|
|
855
|
+
case 'patch':
|
|
856
|
+
return 'return this.http.patch(url, body)';
|
|
857
|
+
default:
|
|
858
|
+
return 'return this.http.get(url)';
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function ensureUiComponents(appRoot) {
|
|
862
|
+
const uiRoot = path_1.default.join(appRoot, 'ui');
|
|
863
|
+
const components = [
|
|
864
|
+
{
|
|
865
|
+
name: 'ui-card',
|
|
866
|
+
template: `
|
|
867
|
+
import { Component, Input } from '@angular/core'
|
|
868
|
+
import { NgIf } from '@angular/common'
|
|
869
|
+
|
|
870
|
+
@Component({
|
|
871
|
+
selector: 'ui-card',
|
|
872
|
+
standalone: true,
|
|
873
|
+
imports: [NgIf],
|
|
874
|
+
templateUrl: './ui-card.component.html',
|
|
875
|
+
styleUrls: ['./ui-card.component.scss']
|
|
876
|
+
})
|
|
877
|
+
export class UiCardComponent {
|
|
878
|
+
@Input() title?: string
|
|
879
|
+
@Input() subtitle?: string
|
|
880
|
+
}
|
|
881
|
+
`,
|
|
882
|
+
html: `
|
|
883
|
+
<section class="ui-card">
|
|
884
|
+
<header class="ui-card__header" *ngIf="title || subtitle">
|
|
885
|
+
<h2 class="ui-card__title" *ngIf="title">{{ title }}</h2>
|
|
886
|
+
<p class="ui-card__subtitle" *ngIf="subtitle">{{ subtitle }}</p>
|
|
887
|
+
</header>
|
|
888
|
+
<div class="ui-card__body">
|
|
889
|
+
<ng-content></ng-content>
|
|
890
|
+
</div>
|
|
891
|
+
</section>
|
|
892
|
+
`,
|
|
893
|
+
scss: `
|
|
894
|
+
:host {
|
|
895
|
+
display: block;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.ui-card {
|
|
899
|
+
border-radius: 20px;
|
|
900
|
+
background: #ffffff;
|
|
901
|
+
border: 1px solid #e5e7eb;
|
|
902
|
+
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
|
|
903
|
+
padding: 28px;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.ui-card__header {
|
|
907
|
+
margin-bottom: 18px;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.ui-card__title {
|
|
911
|
+
margin: 0;
|
|
912
|
+
font-size: 22px;
|
|
913
|
+
font-weight: 700;
|
|
914
|
+
color: #0f172a;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.ui-card__subtitle {
|
|
918
|
+
margin: 8px 0 0;
|
|
919
|
+
font-size: 12px;
|
|
920
|
+
color: #6b7280;
|
|
921
|
+
letter-spacing: 0.04em;
|
|
922
|
+
text-transform: uppercase;
|
|
923
|
+
}
|
|
924
|
+
`
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
name: 'ui-field',
|
|
928
|
+
template: `
|
|
929
|
+
import { Component, Input } from '@angular/core'
|
|
930
|
+
import { NgIf } from '@angular/common'
|
|
931
|
+
|
|
932
|
+
@Component({
|
|
933
|
+
selector: 'ui-field',
|
|
934
|
+
standalone: true,
|
|
935
|
+
imports: [NgIf],
|
|
936
|
+
templateUrl: './ui-field.component.html',
|
|
937
|
+
styleUrls: ['./ui-field.component.scss']
|
|
938
|
+
})
|
|
939
|
+
export class UiFieldComponent {
|
|
940
|
+
@Input() label = ''
|
|
941
|
+
@Input() hint = ''
|
|
942
|
+
@Input() info = ''
|
|
943
|
+
infoOpen = false
|
|
944
|
+
|
|
945
|
+
toggleInfo(event: MouseEvent) {
|
|
946
|
+
event.preventDefault()
|
|
947
|
+
event.stopPropagation()
|
|
948
|
+
this.infoOpen = !this.infoOpen
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
`,
|
|
952
|
+
html: `
|
|
953
|
+
<label class="ui-field">
|
|
954
|
+
<span class="ui-field__label" *ngIf="label">
|
|
955
|
+
{{ label }}
|
|
956
|
+
<button
|
|
957
|
+
class="ui-field__info"
|
|
958
|
+
type="button"
|
|
959
|
+
*ngIf="info"
|
|
960
|
+
(click)="toggleInfo($event)"
|
|
961
|
+
[attr.aria-expanded]="infoOpen"
|
|
962
|
+
>
|
|
963
|
+
i
|
|
964
|
+
</button>
|
|
965
|
+
</span>
|
|
966
|
+
<div class="ui-field__info-panel" *ngIf="info && infoOpen">
|
|
967
|
+
{{ info }}
|
|
968
|
+
</div>
|
|
969
|
+
<ng-content></ng-content>
|
|
970
|
+
<span class="ui-field__hint" *ngIf="hint && !info">{{ hint }}</span>
|
|
971
|
+
</label>
|
|
972
|
+
`,
|
|
973
|
+
scss: `
|
|
974
|
+
:host {
|
|
975
|
+
display: block;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.ui-field {
|
|
979
|
+
display: grid;
|
|
980
|
+
gap: 10px;
|
|
981
|
+
font-size: 13px;
|
|
982
|
+
color: #374151;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.ui-field__label {
|
|
986
|
+
font-weight: 700;
|
|
987
|
+
line-height: 1.4;
|
|
988
|
+
word-break: break-word;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.ui-field__hint {
|
|
992
|
+
color: #94a3b8;
|
|
993
|
+
font-size: 12px;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.ui-field__info {
|
|
997
|
+
margin-left: 8px;
|
|
998
|
+
width: 18px;
|
|
999
|
+
height: 18px;
|
|
1000
|
+
border-radius: 999px;
|
|
1001
|
+
border: 1px solid #cbd5f5;
|
|
1002
|
+
background: #ffffff;
|
|
1003
|
+
color: #475569;
|
|
1004
|
+
font-size: 11px;
|
|
1005
|
+
line-height: 1;
|
|
1006
|
+
display: inline-flex;
|
|
1007
|
+
align-items: center;
|
|
1008
|
+
justify-content: center;
|
|
1009
|
+
cursor: pointer;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.ui-field__info:hover {
|
|
1013
|
+
background: #f8fafc;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
.ui-field__info-panel {
|
|
1017
|
+
margin-top: 8px;
|
|
1018
|
+
padding: 10px 12px;
|
|
1019
|
+
border-radius: 10px;
|
|
1020
|
+
background: #f8fafc;
|
|
1021
|
+
border: 1px solid #e2e8f0;
|
|
1022
|
+
color: #475569;
|
|
1023
|
+
font-size: 12px;
|
|
1024
|
+
line-height: 1.4;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
:host ::ng-deep input,
|
|
1028
|
+
:host ::ng-deep textarea,
|
|
1029
|
+
:host ::ng-deep select {
|
|
1030
|
+
width: 100%;
|
|
1031
|
+
min-height: 3.4rem;
|
|
1032
|
+
border-radius: 10px;
|
|
1033
|
+
border: 1px solid #e5e7eb;
|
|
1034
|
+
background: #ffffff;
|
|
1035
|
+
padding: 0.9rem 1.1rem;
|
|
1036
|
+
font-size: 15px;
|
|
1037
|
+
font-weight: 500;
|
|
1038
|
+
box-shadow: none;
|
|
1039
|
+
outline: none;
|
|
1040
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
:host ::ng-deep input:focus,
|
|
1044
|
+
:host ::ng-deep textarea:focus,
|
|
1045
|
+
:host ::ng-deep select:focus {
|
|
1046
|
+
border-color: #6366f1;
|
|
1047
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
:host ::ng-deep input.invalid,
|
|
1051
|
+
:host ::ng-deep textarea.invalid,
|
|
1052
|
+
:host ::ng-deep select.invalid {
|
|
1053
|
+
border-color: #ef4444;
|
|
1054
|
+
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.18);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
:host ::ng-deep input::placeholder,
|
|
1058
|
+
:host ::ng-deep textarea::placeholder {
|
|
1059
|
+
color: #94a3b8;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
:host ::ng-deep select {
|
|
1063
|
+
padding-right: 2.6rem;
|
|
1064
|
+
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>");
|
|
1065
|
+
background-repeat: no-repeat;
|
|
1066
|
+
background-position: right 0.9rem center;
|
|
1067
|
+
background-size: 14px 8px;
|
|
1068
|
+
appearance: none;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
:host ::ng-deep input[type='checkbox'] {
|
|
1072
|
+
width: 20px;
|
|
1073
|
+
height: 20px;
|
|
1074
|
+
padding: 0;
|
|
1075
|
+
border-radius: 6px;
|
|
1076
|
+
box-shadow: none;
|
|
1077
|
+
accent-color: #6366f1;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.field-error {
|
|
1081
|
+
color: #ef4444;
|
|
1082
|
+
font-size: 12px;
|
|
1083
|
+
margin-top: -4px;
|
|
1084
|
+
}
|
|
1085
|
+
`
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
name: 'ui-button',
|
|
1089
|
+
template: `
|
|
1090
|
+
import { Component, Input } from '@angular/core'
|
|
1091
|
+
import { NgClass } from '@angular/common'
|
|
1092
|
+
|
|
1093
|
+
@Component({
|
|
1094
|
+
selector: 'ui-button',
|
|
1095
|
+
standalone: true,
|
|
1096
|
+
imports: [NgClass],
|
|
1097
|
+
templateUrl: './ui-button.component.html',
|
|
1098
|
+
styleUrls: ['./ui-button.component.scss']
|
|
1099
|
+
})
|
|
1100
|
+
export class UiButtonComponent {
|
|
1101
|
+
@Input() type: 'button' | 'submit' | 'reset' = 'button'
|
|
1102
|
+
@Input() variant: 'primary' | 'ghost' | 'danger' = 'primary'
|
|
1103
|
+
@Input() disabled = false
|
|
1104
|
+
}
|
|
1105
|
+
`,
|
|
1106
|
+
html: `
|
|
1107
|
+
<button
|
|
1108
|
+
class="ui-button"
|
|
1109
|
+
[ngClass]="variant"
|
|
1110
|
+
[attr.type]="type"
|
|
1111
|
+
[disabled]="disabled"
|
|
1112
|
+
>
|
|
1113
|
+
<ng-content></ng-content>
|
|
1114
|
+
</button>
|
|
1115
|
+
`,
|
|
1116
|
+
scss: `
|
|
1117
|
+
.ui-button {
|
|
1118
|
+
border: none;
|
|
1119
|
+
border-radius: 10px;
|
|
1120
|
+
padding: 12px 22px;
|
|
1121
|
+
font-weight: 700;
|
|
1122
|
+
font-size: 14px;
|
|
1123
|
+
cursor: pointer;
|
|
1124
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.ui-button.primary {
|
|
1128
|
+
background: linear-gradient(135deg, #6366f1, #818cf8);
|
|
1129
|
+
color: #ffffff;
|
|
1130
|
+
box-shadow: 0 8px 18px rgba(99, 102, 241, 0.22);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.ui-button.ghost {
|
|
1134
|
+
background: #f9fafb;
|
|
1135
|
+
color: #111827;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
.ui-button.danger {
|
|
1139
|
+
background: linear-gradient(135deg, #ef4444, #f97316);
|
|
1140
|
+
color: #fff;
|
|
1141
|
+
box-shadow: 0 8px 18px rgba(239, 68, 68, 0.25);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.ui-button:hover:not(:disabled) {
|
|
1145
|
+
transform: translateY(-1px);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
.ui-button:disabled {
|
|
1149
|
+
opacity: 0.6;
|
|
1150
|
+
cursor: not-allowed;
|
|
1151
|
+
box-shadow: none;
|
|
1152
|
+
}
|
|
1153
|
+
`
|
|
1154
|
+
}
|
|
1155
|
+
];
|
|
1156
|
+
fs_1.default.mkdirSync(uiRoot, { recursive: true });
|
|
1157
|
+
for (const component of components) {
|
|
1158
|
+
const componentDir = path_1.default.join(uiRoot, component.name);
|
|
1159
|
+
fs_1.default.mkdirSync(componentDir, { recursive: true });
|
|
1160
|
+
const base = component.name;
|
|
1161
|
+
const tsPath = path_1.default.join(componentDir, `${base}.component.ts`);
|
|
1162
|
+
const htmlPath = path_1.default.join(componentDir, `${base}.component.html`);
|
|
1163
|
+
const scssPath = path_1.default.join(componentDir, `${base}.component.scss`);
|
|
1164
|
+
const needsUiFieldUpdate = component.name === 'ui-field';
|
|
1165
|
+
const shouldOverwrite = (filePath, marker) => {
|
|
1166
|
+
if (!fs_1.default.existsSync(filePath))
|
|
1167
|
+
return true;
|
|
1168
|
+
if (needsUiFieldUpdate)
|
|
1169
|
+
return true;
|
|
1170
|
+
if (!marker)
|
|
1171
|
+
return true;
|
|
1172
|
+
const existing = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
1173
|
+
return !existing.includes(marker);
|
|
1174
|
+
};
|
|
1175
|
+
if (shouldOverwrite(tsPath, 'infoOpen')) {
|
|
1176
|
+
fs_1.default.writeFileSync(tsPath, component.template.trimStart());
|
|
1177
|
+
}
|
|
1178
|
+
if (shouldOverwrite(htmlPath, 'ui-field__info-panel')) {
|
|
1179
|
+
fs_1.default.writeFileSync(htmlPath, component.html.trimStart());
|
|
1180
|
+
}
|
|
1181
|
+
if (shouldOverwrite(scssPath, 'ui-field__info-panel')) {
|
|
1182
|
+
fs_1.default.writeFileSync(scssPath, component.scss.trimStart());
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function toRouteSegment(operationId) {
|
|
1187
|
+
if (!operationId)
|
|
1188
|
+
return operationId;
|
|
1189
|
+
return operationId[0].toLowerCase() + operationId.slice(1);
|
|
1190
|
+
}
|
|
1191
|
+
function toFolderName(operationId) {
|
|
1192
|
+
return toFileBase(operationId);
|
|
1193
|
+
}
|
|
1194
|
+
function toFileBase(operationId) {
|
|
1195
|
+
return operationId;
|
|
1196
|
+
}
|
|
1197
|
+
function toKebab(value) {
|
|
1198
|
+
return value
|
|
1199
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
1200
|
+
.replace(/[_\s]+/g, '-')
|
|
1201
|
+
.toLowerCase();
|
|
1202
|
+
}
|
|
1203
|
+
function toPascalCase(value) {
|
|
1204
|
+
if (!value)
|
|
1205
|
+
return 'Generated';
|
|
1206
|
+
return String(value)
|
|
1207
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
1208
|
+
.filter(Boolean)
|
|
1209
|
+
.map(part => part[0].toUpperCase() + part.slice(1))
|
|
1210
|
+
.join('');
|
|
1211
|
+
}
|