@praxisui/list 1.0.0-beta.61 → 1.0.0-beta.63
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 +9 -4
- package/fesm2022/praxisui-list.mjs +1098 -253
- package/fesm2022/praxisui-list.mjs.map +1 -1
- package/index.d.ts +134 -9
- package/package.json +6 -4
|
@@ -25,10 +25,10 @@ import * as i12 from '@angular/material/tooltip';
|
|
|
25
25
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
26
26
|
import * as i2 from '@angular/forms';
|
|
27
27
|
import { FormsModule, FormControl, ReactiveFormsModule, FormGroup } from '@angular/forms';
|
|
28
|
-
import { BehaviorSubject, combineLatest, of, Subject, debounceTime
|
|
29
|
-
import { auditTime, switchMap, map, catchError, finalize, shareReplay, debounceTime, distinctUntilChanged, tap, take, takeUntil } from 'rxjs/operators';
|
|
28
|
+
import { BehaviorSubject, combineLatest, of, Subject, debounceTime, takeUntil, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
|
|
29
|
+
import { auditTime, switchMap, map, catchError, finalize, shareReplay, debounceTime as debounceTime$1, distinctUntilChanged, tap, take, takeUntil as takeUntil$1 } from 'rxjs/operators';
|
|
30
30
|
import { SETTINGS_PANEL_DATA, SettingsPanelService } from '@praxisui/settings-panel';
|
|
31
|
-
import * as i3$
|
|
31
|
+
import * as i3$3 from '@angular/material/tabs';
|
|
32
32
|
import { MatTabsModule } from '@angular/material/tabs';
|
|
33
33
|
import * as i8 from '@angular/material/slide-toggle';
|
|
34
34
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
@@ -41,6 +41,8 @@ import { MatExpansionModule } from '@angular/material/expansion';
|
|
|
41
41
|
import { PdxColorPickerComponent } from '@praxisui/dynamic-fields';
|
|
42
42
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
43
43
|
import { produce, setAutoFreeze } from 'immer';
|
|
44
|
+
import * as i3$2 from '@angular/material/card';
|
|
45
|
+
import { MatCardModule } from '@angular/material/card';
|
|
44
46
|
import { BaseAiAdapter, PraxisAiAssistantComponent } from '@praxisui/ai';
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -414,7 +416,7 @@ class ListDataService {
|
|
|
414
416
|
}
|
|
415
417
|
safeSerialize(value) {
|
|
416
418
|
try {
|
|
417
|
-
return stableSerialize(value);
|
|
419
|
+
return stableSerialize$1(value);
|
|
418
420
|
}
|
|
419
421
|
catch {
|
|
420
422
|
return String(value);
|
|
@@ -442,7 +444,7 @@ class ListDataService {
|
|
|
442
444
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ListDataService, decorators: [{
|
|
443
445
|
type: Injectable
|
|
444
446
|
}] });
|
|
445
|
-
function stableSerialize(value) {
|
|
447
|
+
function stableSerialize$1(value) {
|
|
446
448
|
if (value === null || value === undefined)
|
|
447
449
|
return String(value);
|
|
448
450
|
if (typeof value === 'number' ||
|
|
@@ -455,10 +457,10 @@ function stableSerialize(value) {
|
|
|
455
457
|
if (value instanceof Date)
|
|
456
458
|
return JSON.stringify(value.toISOString());
|
|
457
459
|
if (Array.isArray(value))
|
|
458
|
-
return `[${value.map((entry) => stableSerialize(entry)).join(',')}]`;
|
|
460
|
+
return `[${value.map((entry) => stableSerialize$1(entry)).join(',')}]`;
|
|
459
461
|
if (typeof value === 'object') {
|
|
460
462
|
const keys = Object.keys(value).sort();
|
|
461
|
-
const entries = keys.map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`);
|
|
463
|
+
const entries = keys.map((key) => `${JSON.stringify(key)}:${stableSerialize$1(value[key])}`);
|
|
462
464
|
return `{${entries.join(',')}}`;
|
|
463
465
|
}
|
|
464
466
|
return JSON.stringify(String(value));
|
|
@@ -1162,6 +1164,885 @@ function makeChip(field) {
|
|
|
1162
1164
|
return { type: 'chip', expr: '${item.' + field + '}' };
|
|
1163
1165
|
}
|
|
1164
1166
|
|
|
1167
|
+
const DOCUMENT_KIND = 'praxis.list.editor';
|
|
1168
|
+
const DOCUMENT_VERSION = 1;
|
|
1169
|
+
const LAYOUT_VARIANTS = ['list', 'cards', 'tiles'];
|
|
1170
|
+
const LAYOUT_DENSITIES = ['default', 'comfortable', 'compact'];
|
|
1171
|
+
const LAYOUT_DIVIDERS = ['none', 'between', 'all'];
|
|
1172
|
+
const LAYOUT_MODELS = ['standard', 'media', 'hotel'];
|
|
1173
|
+
const SELECTION_MODES = ['none', 'single', 'multiple'];
|
|
1174
|
+
const SELECTION_RETURNS = ['value', 'item', 'id'];
|
|
1175
|
+
const SKIN_TYPES = [
|
|
1176
|
+
'pill-soft',
|
|
1177
|
+
'gradient-tile',
|
|
1178
|
+
'glass',
|
|
1179
|
+
'elevated',
|
|
1180
|
+
'outline',
|
|
1181
|
+
'flat',
|
|
1182
|
+
'neumorphism',
|
|
1183
|
+
'custom',
|
|
1184
|
+
];
|
|
1185
|
+
const FEATURES_MODES = ['icons+labels', 'icons-only', 'labels-only'];
|
|
1186
|
+
function createListAuthoringDocument(source) {
|
|
1187
|
+
return normalizeListAuthoringDocument({
|
|
1188
|
+
kind: DOCUMENT_KIND,
|
|
1189
|
+
version: DOCUMENT_VERSION,
|
|
1190
|
+
config: asListConfig(source?.config),
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
function parseLegacyOrListDocument(raw) {
|
|
1194
|
+
const obj = asRecord(raw);
|
|
1195
|
+
if ('document' in obj) {
|
|
1196
|
+
return parseLegacyOrListDocument(obj.document);
|
|
1197
|
+
}
|
|
1198
|
+
if (obj.kind === DOCUMENT_KIND && obj.version === DOCUMENT_VERSION) {
|
|
1199
|
+
return normalizeListAuthoringDocument({
|
|
1200
|
+
kind: DOCUMENT_KIND,
|
|
1201
|
+
version: DOCUMENT_VERSION,
|
|
1202
|
+
config: asListConfig(obj.config),
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
if (looksLikeLegacySettingsPayload(obj)) {
|
|
1206
|
+
return createListAuthoringDocument({ config: obj.config });
|
|
1207
|
+
}
|
|
1208
|
+
return createListAuthoringDocument({ config: raw });
|
|
1209
|
+
}
|
|
1210
|
+
function normalizeListAuthoringDocument(doc) {
|
|
1211
|
+
return {
|
|
1212
|
+
kind: DOCUMENT_KIND,
|
|
1213
|
+
version: DOCUMENT_VERSION,
|
|
1214
|
+
config: normalizeListConfig(doc?.config),
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function projectListAuthoringDocument(doc, context) {
|
|
1218
|
+
return inferListAuthoringDocument(normalizeListAuthoringDocument(doc), context);
|
|
1219
|
+
}
|
|
1220
|
+
function inferListAuthoringDocument(doc, context) {
|
|
1221
|
+
const normalized = normalizeListAuthoringDocument(doc);
|
|
1222
|
+
if (normalized.config.dataSource?.resourcePath &&
|
|
1223
|
+
!normalized.config.templating?.primary &&
|
|
1224
|
+
(context?.schemaFieldNames || []).length) {
|
|
1225
|
+
return normalizeListAuthoringDocument({
|
|
1226
|
+
...normalized,
|
|
1227
|
+
config: inferTemplatingFromSchema(normalized.config, context?.schemaFieldNames || []),
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
return normalized;
|
|
1231
|
+
}
|
|
1232
|
+
function normalizeListConfig(config) {
|
|
1233
|
+
const normalized = cloneJson(config || {});
|
|
1234
|
+
const out = {
|
|
1235
|
+
...normalized,
|
|
1236
|
+
dataSource: {
|
|
1237
|
+
data: Array.isArray(normalized.dataSource?.data)
|
|
1238
|
+
? normalized.dataSource?.data
|
|
1239
|
+
: undefined,
|
|
1240
|
+
resourcePath: trimToUndefined(normalized.dataSource?.resourcePath),
|
|
1241
|
+
query: normalizeQuery(normalized.dataSource?.query),
|
|
1242
|
+
sort: normalizeSort(normalized.dataSource?.sort),
|
|
1243
|
+
},
|
|
1244
|
+
layout: {
|
|
1245
|
+
variant: oneOf(normalized.layout?.variant, LAYOUT_VARIANTS, 'list'),
|
|
1246
|
+
density: oneOf(normalized.layout?.density, LAYOUT_DENSITIES, 'default'),
|
|
1247
|
+
lines: normalizeLines(normalized.layout?.lines),
|
|
1248
|
+
dividers: oneOf(normalized.layout?.dividers, LAYOUT_DIVIDERS, 'between'),
|
|
1249
|
+
model: oneOf(normalized.layout?.model, LAYOUT_MODELS, 'standard'),
|
|
1250
|
+
groupBy: trimToUndefined(normalized.layout?.groupBy),
|
|
1251
|
+
stickySectionHeader: readBoolean(normalized.layout?.stickySectionHeader),
|
|
1252
|
+
virtualScroll: readBoolean(normalized.layout?.virtualScroll),
|
|
1253
|
+
pageSize: normalizePositiveNumber(normalized.layout?.pageSize),
|
|
1254
|
+
},
|
|
1255
|
+
selection: {
|
|
1256
|
+
mode: oneOf(normalized.selection?.mode, SELECTION_MODES, 'none'),
|
|
1257
|
+
formControlName: trimToUndefined(normalized.selection?.formControlName),
|
|
1258
|
+
formControlPath: trimToUndefined(normalized.selection?.formControlPath),
|
|
1259
|
+
compareBy: trimToUndefined(normalized.selection?.compareBy),
|
|
1260
|
+
return: oneOf(normalized.selection?.return, SELECTION_RETURNS, 'value'),
|
|
1261
|
+
},
|
|
1262
|
+
skin: {
|
|
1263
|
+
type: oneOf(normalized.skin?.type, SKIN_TYPES, 'elevated'),
|
|
1264
|
+
gradient: {
|
|
1265
|
+
from: trimToEmpty(normalized.skin?.gradient?.from),
|
|
1266
|
+
to: trimToEmpty(normalized.skin?.gradient?.to),
|
|
1267
|
+
angle: normalizeAngle(normalized.skin?.gradient?.angle),
|
|
1268
|
+
},
|
|
1269
|
+
radius: trimToUndefined(normalized.skin?.radius),
|
|
1270
|
+
shadow: trimToUndefined(normalized.skin?.shadow),
|
|
1271
|
+
border: trimToUndefined(normalized.skin?.border),
|
|
1272
|
+
backdropBlur: trimToUndefined(normalized.skin?.backdropBlur),
|
|
1273
|
+
class: trimToUndefined(normalized.skin?.class),
|
|
1274
|
+
inlineStyle: trimToUndefined(normalized.skin?.inlineStyle),
|
|
1275
|
+
},
|
|
1276
|
+
templating: normalizeTemplating(normalized.templating),
|
|
1277
|
+
actions: normalizeListActionPayloads(normalized.actions),
|
|
1278
|
+
i18n: normalizeRecord(normalized.i18n),
|
|
1279
|
+
ui: normalizeUi(normalized.ui),
|
|
1280
|
+
a11y: normalizeA11y(normalized.a11y),
|
|
1281
|
+
events: normalizeRecord(normalized.events),
|
|
1282
|
+
};
|
|
1283
|
+
return stripUndefinedDeep(out);
|
|
1284
|
+
}
|
|
1285
|
+
function normalizeListActionPayloads(actions) {
|
|
1286
|
+
if (!Array.isArray(actions))
|
|
1287
|
+
return undefined;
|
|
1288
|
+
return actions.map((action) => {
|
|
1289
|
+
if (!action || typeof action !== 'object')
|
|
1290
|
+
return action;
|
|
1291
|
+
const next = cloneJson(action);
|
|
1292
|
+
if (typeof next.globalPayload === 'string') {
|
|
1293
|
+
const trimmed = next.globalPayload.trim();
|
|
1294
|
+
if (!trimmed) {
|
|
1295
|
+
next.globalPayload = undefined;
|
|
1296
|
+
}
|
|
1297
|
+
else if (looksLikeJsonPayload(trimmed)) {
|
|
1298
|
+
try {
|
|
1299
|
+
next.globalPayload = JSON.parse(trimmed);
|
|
1300
|
+
}
|
|
1301
|
+
catch {
|
|
1302
|
+
next.globalPayload = trimmed;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
next.globalPayload = trimmed;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return stripUndefinedDeep(next);
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
function validateListAuthoringDocument(doc, context) {
|
|
1313
|
+
const diagnostics = [];
|
|
1314
|
+
const raw = asRecord(doc);
|
|
1315
|
+
const rawConfig = asRecord(raw.config);
|
|
1316
|
+
const normalized = normalizeListAuthoringDocument(doc);
|
|
1317
|
+
const config = normalized.config;
|
|
1318
|
+
const rawLayout = asRecord(rawConfig.layout);
|
|
1319
|
+
const rawSelection = asRecord(rawConfig.selection);
|
|
1320
|
+
const rawSkin = asRecord(rawConfig.skin);
|
|
1321
|
+
if ('actions' in rawConfig && !Array.isArray(rawConfig.actions)) {
|
|
1322
|
+
diagnostics.push(errorDiagnostic('list.config.actions.invalid', 'config.actions must be an array', 'config.actions'));
|
|
1323
|
+
}
|
|
1324
|
+
validateRawEnum(diagnostics, rawLayout.variant, LAYOUT_VARIANTS, 'list.layout.variant.invalid', 'config.layout.variant must be list, cards or tiles', 'config.layout.variant');
|
|
1325
|
+
validateRawEnum(diagnostics, rawLayout.density, LAYOUT_DENSITIES, 'list.layout.density.invalid', 'config.layout.density must be default, comfortable or compact', 'config.layout.density');
|
|
1326
|
+
validateRawEnum(diagnostics, rawLayout.dividers, LAYOUT_DIVIDERS, 'list.layout.dividers.invalid', 'config.layout.dividers must be none, between or all', 'config.layout.dividers');
|
|
1327
|
+
validateRawEnum(diagnostics, rawLayout.model, LAYOUT_MODELS, 'list.layout.model.invalid', 'config.layout.model must be standard, media or hotel', 'config.layout.model');
|
|
1328
|
+
validateRawEnum(diagnostics, rawSelection.mode, SELECTION_MODES, 'list.selection.mode.invalid', 'config.selection.mode must be none, single or multiple', 'config.selection.mode');
|
|
1329
|
+
validateRawEnum(diagnostics, rawSelection.return, SELECTION_RETURNS, 'list.selection.return.invalid', 'config.selection.return must be value, item or id', 'config.selection.return');
|
|
1330
|
+
validateRawEnum(diagnostics, rawSkin.type, SKIN_TYPES, 'list.skin.type.invalid', 'config.skin.type is invalid', 'config.skin.type');
|
|
1331
|
+
if ('dataSource' in rawConfig) {
|
|
1332
|
+
const rawDataSource = asRecord(rawConfig.dataSource);
|
|
1333
|
+
if ('query' in rawDataSource &&
|
|
1334
|
+
rawDataSource.query !== undefined &&
|
|
1335
|
+
(rawDataSource.query === null ||
|
|
1336
|
+
typeof rawDataSource.query !== 'object' ||
|
|
1337
|
+
Array.isArray(rawDataSource.query))) {
|
|
1338
|
+
diagnostics.push(errorDiagnostic('list.dataSource.query.invalid', 'config.dataSource.query must be an object', 'config.dataSource.query'));
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if ('lines' in rawLayout && ![1, 2, 3].includes(Number(rawLayout.lines))) {
|
|
1342
|
+
diagnostics.push(errorDiagnostic('list.layout.lines.invalid', 'config.layout.lines must be 1, 2 or 3', 'config.layout.lines'));
|
|
1343
|
+
}
|
|
1344
|
+
if ('pageSize' in rawLayout &&
|
|
1345
|
+
rawLayout.pageSize !== undefined &&
|
|
1346
|
+
(!Number.isFinite(Number(rawLayout.pageSize)) ||
|
|
1347
|
+
Number(rawLayout.pageSize) < 1)) {
|
|
1348
|
+
diagnostics.push(errorDiagnostic('list.layout.pageSize.invalid', 'config.layout.pageSize must be a positive number', 'config.layout.pageSize'));
|
|
1349
|
+
}
|
|
1350
|
+
const schemaFields = new Set(context?.schemaFieldNames || []);
|
|
1351
|
+
const sort = config.dataSource?.sort?.[0];
|
|
1352
|
+
const sortField = trimToUndefined(sort?.split(',')[0]);
|
|
1353
|
+
if (sortField && schemaFields.size && !schemaFields.has(sortField)) {
|
|
1354
|
+
diagnostics.push(errorDiagnostic('list.dataSource.sort.field.unknown', 'config.dataSource.sort references an unknown schema field', 'config.dataSource.sort[0]'));
|
|
1355
|
+
}
|
|
1356
|
+
const compareBy = trimToUndefined(config.selection?.compareBy);
|
|
1357
|
+
if (compareBy && schemaFields.size && !schemaFields.has(compareBy)) {
|
|
1358
|
+
diagnostics.push(errorDiagnostic('list.selection.compareBy.unknown', 'config.selection.compareBy references an unknown schema field', 'config.selection.compareBy'));
|
|
1359
|
+
}
|
|
1360
|
+
const sortOptions = config.ui?.sortOptions || [];
|
|
1361
|
+
const seenSortOptions = new Set();
|
|
1362
|
+
sortOptions.forEach((option, index) => {
|
|
1363
|
+
const value = typeof option === 'string' ? option : trimToUndefined(option?.value);
|
|
1364
|
+
if (!value)
|
|
1365
|
+
return;
|
|
1366
|
+
if (seenSortOptions.has(value)) {
|
|
1367
|
+
diagnostics.push(errorDiagnostic('list.ui.sortOptions.duplicate', 'config.ui.sortOptions contains duplicated values', `config.ui.sortOptions[${index}]`));
|
|
1368
|
+
}
|
|
1369
|
+
seenSortOptions.add(value);
|
|
1370
|
+
});
|
|
1371
|
+
rawConfig.actions?.forEach((action, index) => {
|
|
1372
|
+
const rawAction = asRecord(action);
|
|
1373
|
+
const id = trimToUndefined(rawAction.id);
|
|
1374
|
+
if (!id) {
|
|
1375
|
+
diagnostics.push(errorDiagnostic('list.actions.id.required', 'Action id is required', `config.actions[${index}].id`));
|
|
1376
|
+
}
|
|
1377
|
+
const payload = rawAction.globalPayload;
|
|
1378
|
+
if (typeof payload === 'string' && looksLikeJsonPayload(payload.trim())) {
|
|
1379
|
+
try {
|
|
1380
|
+
JSON.parse(payload);
|
|
1381
|
+
}
|
|
1382
|
+
catch {
|
|
1383
|
+
diagnostics.push(errorDiagnostic('list.actions.globalPayload.invalid-json', 'Action globalPayload contains invalid JSON', `config.actions[${index}].globalPayload`));
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
validateRawTemplating(diagnostics, asRecord(rawConfig.templating), 'config.templating');
|
|
1388
|
+
if (config.dataSource?.resourcePath &&
|
|
1389
|
+
!config.templating?.primary &&
|
|
1390
|
+
!(context?.schemaFieldNames || []).length) {
|
|
1391
|
+
diagnostics.push(infoDiagnostic('list.templating.schema-inference.pending', 'Schema inference is pending because no schema fields were provided in context', 'config.templating.primary'));
|
|
1392
|
+
}
|
|
1393
|
+
return diagnostics;
|
|
1394
|
+
}
|
|
1395
|
+
function toCanonicalListConfig(doc, context) {
|
|
1396
|
+
const projected = projectListAuthoringDocument(doc, context);
|
|
1397
|
+
return normalizeListConfig(projected.config);
|
|
1398
|
+
}
|
|
1399
|
+
function buildListApplyPlan(doc, runtime, options) {
|
|
1400
|
+
const normalized = normalizeListAuthoringDocument(doc);
|
|
1401
|
+
const diagnostics = validateListAuthoringDocument(normalized, {
|
|
1402
|
+
schemaFieldNames: runtime?.schemaFieldNames,
|
|
1403
|
+
});
|
|
1404
|
+
const canonicalDocument = projectListAuthoringDocument(normalized, {
|
|
1405
|
+
schemaFieldNames: runtime?.schemaFieldNames,
|
|
1406
|
+
});
|
|
1407
|
+
const canonicalConfig = toCanonicalListConfig(canonicalDocument, {
|
|
1408
|
+
schemaFieldNames: runtime?.schemaFieldNames,
|
|
1409
|
+
});
|
|
1410
|
+
const diff = deriveConfigDiff(canonicalConfig, runtime?.currentConfig) || {};
|
|
1411
|
+
const schemaInference = buildSchemaInferencePlan(normalized, canonicalDocument, diff, runtime);
|
|
1412
|
+
return {
|
|
1413
|
+
canonicalDocument,
|
|
1414
|
+
canonicalConfig,
|
|
1415
|
+
persistence: {
|
|
1416
|
+
saveConfig: options?.saveConfig === true,
|
|
1417
|
+
},
|
|
1418
|
+
runtime: {
|
|
1419
|
+
applyConfig: true,
|
|
1420
|
+
rebindSelection: diff.selectionChanged || !runtime?.currentConfig,
|
|
1421
|
+
reapplySkin: diff.skinChanged || !runtime?.currentConfig,
|
|
1422
|
+
schemaInference,
|
|
1423
|
+
markForCheck: true,
|
|
1424
|
+
},
|
|
1425
|
+
diff,
|
|
1426
|
+
diagnostics,
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function serializeListAuthoringDocument(doc) {
|
|
1430
|
+
const normalized = normalizeListAuthoringDocument(doc);
|
|
1431
|
+
return stripUndefinedDeep({
|
|
1432
|
+
kind: DOCUMENT_KIND,
|
|
1433
|
+
version: DOCUMENT_VERSION,
|
|
1434
|
+
config: normalized.config,
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
function buildSchemaInferencePlan(sourceDocument, canonicalDocument, diff, runtime) {
|
|
1438
|
+
if (!diff.schemaInferenceRequested ||
|
|
1439
|
+
(runtime?.schemaFieldNames || []).length) {
|
|
1440
|
+
return undefined;
|
|
1441
|
+
}
|
|
1442
|
+
const resourcePath = canonicalDocument.config.dataSource?.resourcePath;
|
|
1443
|
+
if (!resourcePath)
|
|
1444
|
+
return undefined;
|
|
1445
|
+
return {
|
|
1446
|
+
resourcePath,
|
|
1447
|
+
sourceDocument,
|
|
1448
|
+
targetDocument: canonicalDocument,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
function validateRawTemplating(diagnostics, templating, basePath) {
|
|
1452
|
+
if (!Object.keys(templating).length)
|
|
1453
|
+
return;
|
|
1454
|
+
validateRawEnum(diagnostics, templating.metaPlacement, ['side', 'line'], 'list.templating.metaPlacement.invalid', 'config.templating.metaPlacement must be side or line', `${basePath}.metaPlacement`);
|
|
1455
|
+
validateRawEnum(diagnostics, templating.statusPosition, ['inline', 'top-right'], 'list.templating.statusPosition.invalid', 'config.templating.statusPosition must be inline or top-right', `${basePath}.statusPosition`);
|
|
1456
|
+
validateRawEnum(diagnostics, templating.featuresMode, FEATURES_MODES, 'list.templating.featuresMode.invalid', 'config.templating.featuresMode must be icons+labels, icons-only or labels-only', `${basePath}.featuresMode`);
|
|
1457
|
+
[
|
|
1458
|
+
'leading',
|
|
1459
|
+
'primary',
|
|
1460
|
+
'secondary',
|
|
1461
|
+
'meta',
|
|
1462
|
+
'trailing',
|
|
1463
|
+
'sectionHeader',
|
|
1464
|
+
'emptyState',
|
|
1465
|
+
].forEach((slot) => {
|
|
1466
|
+
validateRawTemplateDef(diagnostics, asRecord(templating[slot]), `${basePath}.${slot}`);
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
function validateRawTemplateDef(diagnostics, template, path) {
|
|
1470
|
+
if (!Object.keys(template).length)
|
|
1471
|
+
return;
|
|
1472
|
+
validateRawEnum(diagnostics, template.variant, ['filled', 'outlined'], 'list.templating.template.variant.invalid', `${path}.variant must be filled or outlined`, `${path}.variant`);
|
|
1473
|
+
if ('type' in template && trimToUndefined(template.type) === 'image') {
|
|
1474
|
+
const expr = trimToUndefined(template.expr);
|
|
1475
|
+
if (expr && !isAcceptedImageUrl(expr)) {
|
|
1476
|
+
diagnostics.push(errorDiagnostic('list.templating.template.image.expr.invalid', `${path}.expr must be a URL, asset path or template expression when type is image`, `${path}.expr`));
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
const badge = asRecord(template.badge);
|
|
1480
|
+
if (Object.keys(badge).length) {
|
|
1481
|
+
validateRawEnum(diagnostics, badge.variant, ['filled', 'outlined'], 'list.templating.template.badge.variant.invalid', `${path}.badge.variant must be filled or outlined`, `${path}.badge.variant`);
|
|
1482
|
+
}
|
|
1483
|
+
const rating = asRecord(asRecord(template.props).rating);
|
|
1484
|
+
if ('size' in rating && rating.size !== undefined) {
|
|
1485
|
+
const size = Number(rating.size);
|
|
1486
|
+
if (!Number.isFinite(size) || size < 10 || size > 32) {
|
|
1487
|
+
diagnostics.push(errorDiagnostic('list.templating.template.rating.size.invalid', `${path}.props.rating.size must be between 10 and 32`, `${path}.props.rating.size`));
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if ('max' in rating && rating.max !== undefined) {
|
|
1491
|
+
const max = Number(rating.max);
|
|
1492
|
+
if (!Number.isFinite(max) || max < 1 || max > 10) {
|
|
1493
|
+
diagnostics.push(errorDiagnostic('list.templating.template.rating.max.invalid', `${path}.props.rating.max must be between 1 and 10`, `${path}.props.rating.max`));
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
function normalizeTemplating(templating) {
|
|
1498
|
+
if (!templating || typeof templating !== 'object') {
|
|
1499
|
+
return {
|
|
1500
|
+
skeleton: { count: 3 },
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
return stripUndefinedDeep({
|
|
1504
|
+
...templating,
|
|
1505
|
+
leading: normalizeTemplateDef(templating.leading),
|
|
1506
|
+
primary: normalizeTemplateDef(templating.primary),
|
|
1507
|
+
secondary: normalizeTemplateDef(templating.secondary),
|
|
1508
|
+
meta: normalizeTemplateDef(templating.meta),
|
|
1509
|
+
trailing: normalizeTemplateDef(templating.trailing),
|
|
1510
|
+
sectionHeader: normalizeTemplateDef(templating.sectionHeader),
|
|
1511
|
+
emptyState: normalizeTemplateDef(templating.emptyState),
|
|
1512
|
+
metaPlacement: oneOf(templating.metaPlacement, ['side', 'line'], undefined),
|
|
1513
|
+
metaPrefixIcon: trimToUndefined(templating.metaPrefixIcon),
|
|
1514
|
+
statusPosition: oneOf(templating.statusPosition, ['inline', 'top-right'], undefined),
|
|
1515
|
+
chipColorMap: normalizeRecord(templating.chipColorMap),
|
|
1516
|
+
chipLabelMap: normalizeRecord(templating.chipLabelMap),
|
|
1517
|
+
iconColorMap: normalizeRecord(templating.iconColorMap),
|
|
1518
|
+
features: Array.isArray(templating.features)
|
|
1519
|
+
? templating.features.map((feature) => stripUndefinedDeep({
|
|
1520
|
+
...feature,
|
|
1521
|
+
icon: trimToUndefined(feature?.icon),
|
|
1522
|
+
expr: trimToEmpty(feature?.expr),
|
|
1523
|
+
class: trimToUndefined(feature?.class),
|
|
1524
|
+
style: trimToUndefined(feature?.style),
|
|
1525
|
+
}))
|
|
1526
|
+
: undefined,
|
|
1527
|
+
featuresVisible: readBoolean(templating.featuresVisible),
|
|
1528
|
+
featuresMode: oneOf(templating.featuresMode, FEATURES_MODES, templating.features?.length ? 'icons+labels' : undefined),
|
|
1529
|
+
skeleton: {
|
|
1530
|
+
count: normalizePositiveNumber(templating.skeleton?.count) ?? 3,
|
|
1531
|
+
},
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
function normalizeTemplateDef(template) {
|
|
1535
|
+
if (!template || typeof template !== 'object')
|
|
1536
|
+
return undefined;
|
|
1537
|
+
return stripUndefinedDeep({
|
|
1538
|
+
...template,
|
|
1539
|
+
expr: trimToEmpty(template.expr),
|
|
1540
|
+
class: trimToUndefined(template.class),
|
|
1541
|
+
style: trimToUndefined(template.style),
|
|
1542
|
+
color: trimToUndefined(template.color),
|
|
1543
|
+
variant: oneOf(template.variant, ['filled', 'outlined'], undefined),
|
|
1544
|
+
imageAlt: trimToUndefined(template.imageAlt),
|
|
1545
|
+
badge: template.badge
|
|
1546
|
+
? stripUndefinedDeep({
|
|
1547
|
+
expr: trimToEmpty(template.badge.expr),
|
|
1548
|
+
color: trimToUndefined(template.badge.color),
|
|
1549
|
+
variant: oneOf(template.badge.variant, ['filled', 'outlined'], undefined),
|
|
1550
|
+
})
|
|
1551
|
+
: undefined,
|
|
1552
|
+
props: template.props
|
|
1553
|
+
? stripUndefinedDeep({
|
|
1554
|
+
rating: template.props.rating
|
|
1555
|
+
? stripUndefinedDeep({
|
|
1556
|
+
max: normalizePositiveNumber(template.props.rating.max),
|
|
1557
|
+
size: normalizePositiveNumber(template.props.rating.size),
|
|
1558
|
+
color: trimToUndefined(template.props.rating.color),
|
|
1559
|
+
})
|
|
1560
|
+
: undefined,
|
|
1561
|
+
})
|
|
1562
|
+
: undefined,
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
function normalizeUi(ui) {
|
|
1566
|
+
return stripUndefinedDeep({
|
|
1567
|
+
showSearch: readBoolean(ui?.showSearch),
|
|
1568
|
+
searchField: trimToUndefined(ui?.searchField),
|
|
1569
|
+
searchPlaceholder: trimToUndefined(ui?.searchPlaceholder),
|
|
1570
|
+
showSort: readBoolean(ui?.showSort),
|
|
1571
|
+
sortOptions: Array.isArray(ui?.sortOptions)
|
|
1572
|
+
? ui?.sortOptions
|
|
1573
|
+
.map((option) => typeof option === 'string'
|
|
1574
|
+
? trimToUndefined(option)
|
|
1575
|
+
: stripUndefinedDeep({
|
|
1576
|
+
label: trimToUndefined(option?.label),
|
|
1577
|
+
value: trimToUndefined(option?.value),
|
|
1578
|
+
}))
|
|
1579
|
+
.filter((option) => typeof option === 'string' ||
|
|
1580
|
+
(!!option &&
|
|
1581
|
+
typeof option.label === 'string' &&
|
|
1582
|
+
typeof option.value === 'string'))
|
|
1583
|
+
: undefined,
|
|
1584
|
+
showRange: readBoolean(ui?.showRange),
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
function normalizeA11y(a11y) {
|
|
1588
|
+
return stripUndefinedDeep({
|
|
1589
|
+
ariaLabel: trimToUndefined(a11y?.ariaLabel),
|
|
1590
|
+
ariaLabelledBy: trimToUndefined(a11y?.ariaLabelledBy),
|
|
1591
|
+
highContrast: readBoolean(a11y?.highContrast),
|
|
1592
|
+
reduceMotion: readBoolean(a11y?.reduceMotion),
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
function normalizeRecord(value) {
|
|
1596
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1597
|
+
return undefined;
|
|
1598
|
+
}
|
|
1599
|
+
return stripUndefinedDeep(cloneJson(value));
|
|
1600
|
+
}
|
|
1601
|
+
function normalizeQuery(value) {
|
|
1602
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1603
|
+
return undefined;
|
|
1604
|
+
}
|
|
1605
|
+
return stripUndefinedDeep(cloneJson(value));
|
|
1606
|
+
}
|
|
1607
|
+
function normalizeSort(value) {
|
|
1608
|
+
if (!Array.isArray(value))
|
|
1609
|
+
return undefined;
|
|
1610
|
+
const normalized = value
|
|
1611
|
+
.map((entry) => trimToUndefined(entry))
|
|
1612
|
+
.filter((entry) => !!entry);
|
|
1613
|
+
return normalized.length ? normalized : undefined;
|
|
1614
|
+
}
|
|
1615
|
+
function deriveConfigDiff(next, current) {
|
|
1616
|
+
const normalizedCurrent = current ? normalizeListConfig(current) : undefined;
|
|
1617
|
+
const configChanged = stableSerialize(next) !== stableSerialize(normalizedCurrent);
|
|
1618
|
+
const dataSourceChanged = stableSerialize(next.dataSource) !== stableSerialize(normalizedCurrent?.dataSource) ||
|
|
1619
|
+
stableSerialize(next.layout) !== stableSerialize(normalizedCurrent?.layout) ||
|
|
1620
|
+
stableSerialize(next.ui) !== stableSerialize(normalizedCurrent?.ui);
|
|
1621
|
+
const selectionChanged = stableSerialize(next.selection) !== stableSerialize(normalizedCurrent?.selection);
|
|
1622
|
+
const skinChanged = stableSerialize(next.skin) !== stableSerialize(normalizedCurrent?.skin) ||
|
|
1623
|
+
stableSerialize(next.layout) !== stableSerialize(normalizedCurrent?.layout);
|
|
1624
|
+
const templatingChanged = stableSerialize(next.templating) !== stableSerialize(normalizedCurrent?.templating);
|
|
1625
|
+
const schemaInferenceRequested = !!(next.dataSource?.resourcePath && !next.templating?.primary);
|
|
1626
|
+
return {
|
|
1627
|
+
configChanged,
|
|
1628
|
+
dataSourceChanged,
|
|
1629
|
+
selectionChanged,
|
|
1630
|
+
skinChanged,
|
|
1631
|
+
templatingChanged,
|
|
1632
|
+
schemaInferenceRequested,
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function looksLikeLegacySettingsPayload(obj) {
|
|
1636
|
+
return 'config' in obj && !('kind' in obj);
|
|
1637
|
+
}
|
|
1638
|
+
function asListConfig(value) {
|
|
1639
|
+
if (value && typeof value === 'object') {
|
|
1640
|
+
return value;
|
|
1641
|
+
}
|
|
1642
|
+
return {};
|
|
1643
|
+
}
|
|
1644
|
+
function asRecord(value) {
|
|
1645
|
+
return value && typeof value === 'object'
|
|
1646
|
+
? value
|
|
1647
|
+
: {};
|
|
1648
|
+
}
|
|
1649
|
+
function oneOf(value, allowed, fallback) {
|
|
1650
|
+
const candidate = trimToUndefined(value);
|
|
1651
|
+
if (!candidate)
|
|
1652
|
+
return fallback;
|
|
1653
|
+
return allowed.includes(candidate) ? candidate : fallback;
|
|
1654
|
+
}
|
|
1655
|
+
function validateRawEnum(diagnostics, value, allowed, code, message, path) {
|
|
1656
|
+
if (value === undefined || value === null || value === '')
|
|
1657
|
+
return;
|
|
1658
|
+
const candidate = trimToUndefined(value);
|
|
1659
|
+
if (!candidate || !allowed.includes(candidate)) {
|
|
1660
|
+
diagnostics.push(errorDiagnostic(code, message, path));
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function trimToEmpty(value) {
|
|
1664
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
1665
|
+
}
|
|
1666
|
+
function trimToUndefined(value) {
|
|
1667
|
+
const trimmed = trimToEmpty(value);
|
|
1668
|
+
return trimmed || undefined;
|
|
1669
|
+
}
|
|
1670
|
+
function normalizeLines(value) {
|
|
1671
|
+
const num = Number(value);
|
|
1672
|
+
return num === 1 || num === 3 ? num : 2;
|
|
1673
|
+
}
|
|
1674
|
+
function normalizePositiveNumber(value) {
|
|
1675
|
+
if (value === undefined || value === null || value === '')
|
|
1676
|
+
return undefined;
|
|
1677
|
+
const num = Number(value);
|
|
1678
|
+
if (!Number.isFinite(num) || num < 1)
|
|
1679
|
+
return undefined;
|
|
1680
|
+
return num;
|
|
1681
|
+
}
|
|
1682
|
+
function normalizeAngle(value) {
|
|
1683
|
+
const num = Number(value);
|
|
1684
|
+
return Number.isFinite(num) ? num : 135;
|
|
1685
|
+
}
|
|
1686
|
+
function readBoolean(value) {
|
|
1687
|
+
if (value === true || value === false)
|
|
1688
|
+
return value;
|
|
1689
|
+
return undefined;
|
|
1690
|
+
}
|
|
1691
|
+
function looksLikeJsonPayload(value) {
|
|
1692
|
+
return (value.startsWith('{') ||
|
|
1693
|
+
value.startsWith('[') ||
|
|
1694
|
+
value.endsWith('}') ||
|
|
1695
|
+
value.endsWith(']'));
|
|
1696
|
+
}
|
|
1697
|
+
function isAcceptedImageUrl(value) {
|
|
1698
|
+
return ((value.startsWith('${') && value.endsWith('}')) ||
|
|
1699
|
+
value.startsWith('http://') ||
|
|
1700
|
+
value.startsWith('https://') ||
|
|
1701
|
+
value.startsWith('data:') ||
|
|
1702
|
+
value.startsWith('./') ||
|
|
1703
|
+
value.startsWith('../') ||
|
|
1704
|
+
value.startsWith('assets/') ||
|
|
1705
|
+
value.startsWith('/'));
|
|
1706
|
+
}
|
|
1707
|
+
function cloneJson(value) {
|
|
1708
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
1709
|
+
}
|
|
1710
|
+
function stripUndefinedDeep(value) {
|
|
1711
|
+
return JSON.parse(JSON.stringify(value));
|
|
1712
|
+
}
|
|
1713
|
+
function stableSerialize(value) {
|
|
1714
|
+
if (value === undefined)
|
|
1715
|
+
return 'undefined';
|
|
1716
|
+
if (value === null)
|
|
1717
|
+
return 'null';
|
|
1718
|
+
if (typeof value === 'number' ||
|
|
1719
|
+
typeof value === 'boolean' ||
|
|
1720
|
+
typeof value === 'bigint') {
|
|
1721
|
+
return String(value);
|
|
1722
|
+
}
|
|
1723
|
+
if (typeof value === 'string')
|
|
1724
|
+
return JSON.stringify(value);
|
|
1725
|
+
if (Array.isArray(value)) {
|
|
1726
|
+
return `[${value.map((entry) => stableSerialize(entry)).join(',')}]`;
|
|
1727
|
+
}
|
|
1728
|
+
if (typeof value === 'object') {
|
|
1729
|
+
const record = value;
|
|
1730
|
+
const keys = Object.keys(record).sort();
|
|
1731
|
+
return `{${keys
|
|
1732
|
+
.map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key])}`)
|
|
1733
|
+
.join(',')}}`;
|
|
1734
|
+
}
|
|
1735
|
+
return JSON.stringify(String(value));
|
|
1736
|
+
}
|
|
1737
|
+
function errorDiagnostic(code, message, path) {
|
|
1738
|
+
return { level: 'error', code, message, path };
|
|
1739
|
+
}
|
|
1740
|
+
function infoDiagnostic(code, message, path) {
|
|
1741
|
+
return { level: 'info', code, message, path };
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
class PraxisListJsonConfigEditorComponent {
|
|
1745
|
+
cdr;
|
|
1746
|
+
document = null;
|
|
1747
|
+
documentChange = new EventEmitter();
|
|
1748
|
+
validationChange = new EventEmitter();
|
|
1749
|
+
editorEvent = new EventEmitter();
|
|
1750
|
+
jsonText = '';
|
|
1751
|
+
isValidJson = true;
|
|
1752
|
+
jsonError = '';
|
|
1753
|
+
unknownTopKeys = [];
|
|
1754
|
+
hasPendingExternalUpdate = false;
|
|
1755
|
+
destroy$ = new Subject();
|
|
1756
|
+
jsonTextChanges$ = new Subject();
|
|
1757
|
+
lastSyncedJsonText = '';
|
|
1758
|
+
constructor(cdr) {
|
|
1759
|
+
this.cdr = cdr;
|
|
1760
|
+
this.jsonTextChanges$
|
|
1761
|
+
.pipe(debounceTime(300), takeUntil(this.destroy$))
|
|
1762
|
+
.subscribe((text) => this.validateJson(text));
|
|
1763
|
+
}
|
|
1764
|
+
ngOnInit() {
|
|
1765
|
+
if (this.document) {
|
|
1766
|
+
this.updateJsonFromDocument(this.document, true);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
ngOnChanges(changes) {
|
|
1770
|
+
if (changes['document'] && this.document) {
|
|
1771
|
+
this.updateJsonFromDocument(this.document);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
ngOnDestroy() {
|
|
1775
|
+
this.destroy$.next();
|
|
1776
|
+
this.destroy$.complete();
|
|
1777
|
+
}
|
|
1778
|
+
onJsonTextChange(text) {
|
|
1779
|
+
this.jsonTextChanges$.next(text);
|
|
1780
|
+
}
|
|
1781
|
+
applyJsonChanges() {
|
|
1782
|
+
if (!this.isValidJson)
|
|
1783
|
+
return;
|
|
1784
|
+
try {
|
|
1785
|
+
const document = parseLegacyOrListDocument(JSON.parse(this.jsonText));
|
|
1786
|
+
const diagnostics = validateListAuthoringDocument(document);
|
|
1787
|
+
this.lastSyncedJsonText = this.jsonText;
|
|
1788
|
+
this.hasPendingExternalUpdate = false;
|
|
1789
|
+
this.documentChange.emit(document);
|
|
1790
|
+
this.editorEvent.emit({
|
|
1791
|
+
type: 'apply',
|
|
1792
|
+
payload: {
|
|
1793
|
+
isValid: !diagnostics.some((item) => item.level === 'error'),
|
|
1794
|
+
document,
|
|
1795
|
+
diagnostics,
|
|
1796
|
+
},
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
catch {
|
|
1800
|
+
this.editorEvent.emit({
|
|
1801
|
+
type: 'apply',
|
|
1802
|
+
payload: {
|
|
1803
|
+
isValid: false,
|
|
1804
|
+
error: 'Erro ao aplicar documento JSON',
|
|
1805
|
+
},
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
formatJson() {
|
|
1810
|
+
if (!this.isValidJson)
|
|
1811
|
+
return;
|
|
1812
|
+
try {
|
|
1813
|
+
const document = parseLegacyOrListDocument(JSON.parse(this.jsonText));
|
|
1814
|
+
const diagnostics = validateListAuthoringDocument(document);
|
|
1815
|
+
this.syncJsonText(JSON.stringify(serializeListAuthoringDocument(document), null, 2));
|
|
1816
|
+
this.editorEvent.emit({
|
|
1817
|
+
type: 'format',
|
|
1818
|
+
payload: {
|
|
1819
|
+
isValid: !diagnostics.some((item) => item.level === 'error'),
|
|
1820
|
+
document,
|
|
1821
|
+
diagnostics,
|
|
1822
|
+
},
|
|
1823
|
+
});
|
|
1824
|
+
this.cdr.markForCheck();
|
|
1825
|
+
}
|
|
1826
|
+
catch {
|
|
1827
|
+
this.editorEvent.emit({
|
|
1828
|
+
type: 'format',
|
|
1829
|
+
payload: {
|
|
1830
|
+
isValid: false,
|
|
1831
|
+
error: 'Erro ao formatar JSON',
|
|
1832
|
+
},
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
updateJsonFromDocument(document, force = false) {
|
|
1837
|
+
const nextJsonText = JSON.stringify(serializeListAuthoringDocument(document), null, 2);
|
|
1838
|
+
if (!force && this.hasUnsavedLocalChanges()) {
|
|
1839
|
+
this.hasPendingExternalUpdate = true;
|
|
1840
|
+
this.cdr.markForCheck();
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
this.syncJsonText(nextJsonText);
|
|
1844
|
+
}
|
|
1845
|
+
reloadFromDocument() {
|
|
1846
|
+
if (!this.document)
|
|
1847
|
+
return;
|
|
1848
|
+
this.updateJsonFromDocument(this.document, true);
|
|
1849
|
+
}
|
|
1850
|
+
validateJson(text) {
|
|
1851
|
+
const result = {
|
|
1852
|
+
isValid: false,
|
|
1853
|
+
diagnostics: [],
|
|
1854
|
+
};
|
|
1855
|
+
if (!text.trim()) {
|
|
1856
|
+
result.error = 'JSON não pode estar vazio';
|
|
1857
|
+
this.updateValidationState(result);
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
try {
|
|
1861
|
+
const parsed = JSON.parse(text);
|
|
1862
|
+
const document = parseLegacyOrListDocument(parsed);
|
|
1863
|
+
const diagnostics = validateListAuthoringDocument(document);
|
|
1864
|
+
const hasErrors = diagnostics.some((item) => item.level === 'error');
|
|
1865
|
+
const allowed = ['kind', 'version', 'config', 'bindings'];
|
|
1866
|
+
result.isValid = !hasErrors;
|
|
1867
|
+
result.document = document;
|
|
1868
|
+
result.diagnostics = diagnostics;
|
|
1869
|
+
this.unknownTopKeys = Object.keys(parsed || {}).filter((key) => !allowed.includes(key));
|
|
1870
|
+
if (hasErrors) {
|
|
1871
|
+
result.error =
|
|
1872
|
+
diagnostics.find((item) => item.level === 'error')?.message ||
|
|
1873
|
+
'Documento de autoria inválido';
|
|
1874
|
+
}
|
|
1875
|
+
this.updateValidationState(result);
|
|
1876
|
+
}
|
|
1877
|
+
catch (error) {
|
|
1878
|
+
result.error =
|
|
1879
|
+
error instanceof Error ? error.message : 'Erro de sintaxe JSON';
|
|
1880
|
+
this.updateValidationState(result);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
updateValidationState(result) {
|
|
1884
|
+
this.isValidJson = result.isValid;
|
|
1885
|
+
this.jsonError = result.error || '';
|
|
1886
|
+
this.validationChange.emit(result);
|
|
1887
|
+
this.editorEvent.emit({
|
|
1888
|
+
type: 'validation',
|
|
1889
|
+
payload: result,
|
|
1890
|
+
});
|
|
1891
|
+
this.cdr.markForCheck();
|
|
1892
|
+
}
|
|
1893
|
+
syncJsonText(text) {
|
|
1894
|
+
this.jsonText = text;
|
|
1895
|
+
this.lastSyncedJsonText = text;
|
|
1896
|
+
this.hasPendingExternalUpdate = false;
|
|
1897
|
+
this.validateJson(this.jsonText);
|
|
1898
|
+
}
|
|
1899
|
+
hasUnsavedLocalChanges() {
|
|
1900
|
+
return this.jsonText.trim() !== this.lastSyncedJsonText.trim();
|
|
1901
|
+
}
|
|
1902
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisListJsonConfigEditorComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1903
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisListJsonConfigEditorComponent, isStandalone: true, selector: "praxis-list-json-config-editor", inputs: { document: "document" }, outputs: { documentChange: "documentChange", validationChange: "validationChange", editorEvent: "editorEvent" }, usesOnChanges: true, ngImport: i0, template: `
|
|
1904
|
+
<div class="json-config-editor">
|
|
1905
|
+
<mat-card class="educational-card">
|
|
1906
|
+
<mat-card-header>
|
|
1907
|
+
<mat-icon mat-card-avatar class="card-icon">data_object</mat-icon>
|
|
1908
|
+
<mat-card-title>Edição Avançada JSON</mat-card-title>
|
|
1909
|
+
</mat-card-header>
|
|
1910
|
+
<mat-card-content>
|
|
1911
|
+
<p>O contrato oficial de autoria da lista é <strong>ListAuthoringDocument</strong>. Esta aba edita esse documento diretamente.</p>
|
|
1912
|
+
</mat-card-content>
|
|
1913
|
+
</mat-card>
|
|
1914
|
+
|
|
1915
|
+
<div class="json-editor-section">
|
|
1916
|
+
<div class="json-editor-toolbar">
|
|
1917
|
+
<button mat-button (click)="formatJson()" [disabled]="!isValidJson">
|
|
1918
|
+
<mat-icon>format_align_left</mat-icon>
|
|
1919
|
+
Formatar JSON
|
|
1920
|
+
</button>
|
|
1921
|
+
<button mat-button color="primary" (click)="applyJsonChanges()" [disabled]="!isValidJson">
|
|
1922
|
+
<mat-icon>check</mat-icon>
|
|
1923
|
+
Aplicar JSON
|
|
1924
|
+
</button>
|
|
1925
|
+
@if (hasPendingExternalUpdate) {
|
|
1926
|
+
<button mat-button type="button" (click)="reloadFromDocument()">
|
|
1927
|
+
<mat-icon>refresh</mat-icon>
|
|
1928
|
+
Recarregar documento
|
|
1929
|
+
</button>
|
|
1930
|
+
}
|
|
1931
|
+
</div>
|
|
1932
|
+
@if (hasPendingExternalUpdate) {
|
|
1933
|
+
<p class="pending-update-note">
|
|
1934
|
+
O documento externo mudou enquanto havia edições locais não aplicadas. Revise e recarregue quando quiser substituir o texto atual.
|
|
1935
|
+
</p>
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
<mat-form-field appearance="outline" class="json-textarea-field">
|
|
1939
|
+
<mat-label>Documento JSON</mat-label>
|
|
1940
|
+
<textarea
|
|
1941
|
+
matInput
|
|
1942
|
+
[(ngModel)]="jsonText"
|
|
1943
|
+
(ngModelChange)="onJsonTextChange($event)"
|
|
1944
|
+
rows="20"
|
|
1945
|
+
spellcheck="false"
|
|
1946
|
+
class="json-textarea"
|
|
1947
|
+
placeholder="Edite o documento canônico da lista aqui...">
|
|
1948
|
+
</textarea>
|
|
1949
|
+
@if (isValidJson) {
|
|
1950
|
+
<mat-hint class="valid-hint">JSON válido</mat-hint>
|
|
1951
|
+
}
|
|
1952
|
+
@if (isValidJson && unknownTopKeys.length) {
|
|
1953
|
+
<mat-hint align="end" class="valid-hint warning-hint">
|
|
1954
|
+
Aviso: chaves desconhecidas no topo — {{ unknownTopKeys.join(', ') }}
|
|
1955
|
+
</mat-hint>
|
|
1956
|
+
}
|
|
1957
|
+
@if (!isValidJson && jsonText) {
|
|
1958
|
+
<mat-error>JSON inválido: {{ jsonError }}</mat-error>
|
|
1959
|
+
}
|
|
1960
|
+
</mat-form-field>
|
|
1961
|
+
</div>
|
|
1962
|
+
</div>
|
|
1963
|
+
`, isInline: true, styles: [".json-config-editor{display:flex;flex-direction:column;gap:16px}.educational-card{background-color:var(--md-sys-color-surface-container-low);border-left:4px solid var(--md-sys-color-primary)}.card-icon{background-color:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);font-size:20px;width:40px;height:40px;display:flex;align-items:center;justify-content:center}.json-editor-section{display:flex;flex-direction:column;gap:16px}.json-editor-toolbar{display:flex;gap:12px;padding:12px;background-color:var(--md-sys-color-surface-container-low);border-radius:8px;border:1px solid var(--md-sys-color-outline-variant)}.json-textarea-field{width:100%}.pending-update-note{margin:0;color:var(--md-sys-color-secondary);font-size:.875rem}.json-textarea{font-family:Monaco,Menlo,Ubuntu Mono,Consolas,monospace!important;font-size:13px!important;line-height:1.4!important;min-height:320px!important;white-space:pre!important;overflow-wrap:normal!important;overflow-x:auto!important;resize:none!important}.valid-hint{color:var(--md-sys-color-primary)!important}.warning-hint{color:var(--md-sys-color-secondary)!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$2.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$2.MatCardAvatar, selector: "[mat-card-avatar], [matCardAvatar]" }, { kind: "directive", type: i3$2.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$2.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$2.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1964
|
+
}
|
|
1965
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisListJsonConfigEditorComponent, decorators: [{
|
|
1966
|
+
type: Component,
|
|
1967
|
+
args: [{ selector: 'praxis-list-json-config-editor', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
1968
|
+
CommonModule,
|
|
1969
|
+
FormsModule,
|
|
1970
|
+
MatButtonModule,
|
|
1971
|
+
MatCardModule,
|
|
1972
|
+
MatFormFieldModule,
|
|
1973
|
+
MatIconModule,
|
|
1974
|
+
MatInputModule,
|
|
1975
|
+
], template: `
|
|
1976
|
+
<div class="json-config-editor">
|
|
1977
|
+
<mat-card class="educational-card">
|
|
1978
|
+
<mat-card-header>
|
|
1979
|
+
<mat-icon mat-card-avatar class="card-icon">data_object</mat-icon>
|
|
1980
|
+
<mat-card-title>Edição Avançada JSON</mat-card-title>
|
|
1981
|
+
</mat-card-header>
|
|
1982
|
+
<mat-card-content>
|
|
1983
|
+
<p>O contrato oficial de autoria da lista é <strong>ListAuthoringDocument</strong>. Esta aba edita esse documento diretamente.</p>
|
|
1984
|
+
</mat-card-content>
|
|
1985
|
+
</mat-card>
|
|
1986
|
+
|
|
1987
|
+
<div class="json-editor-section">
|
|
1988
|
+
<div class="json-editor-toolbar">
|
|
1989
|
+
<button mat-button (click)="formatJson()" [disabled]="!isValidJson">
|
|
1990
|
+
<mat-icon>format_align_left</mat-icon>
|
|
1991
|
+
Formatar JSON
|
|
1992
|
+
</button>
|
|
1993
|
+
<button mat-button color="primary" (click)="applyJsonChanges()" [disabled]="!isValidJson">
|
|
1994
|
+
<mat-icon>check</mat-icon>
|
|
1995
|
+
Aplicar JSON
|
|
1996
|
+
</button>
|
|
1997
|
+
@if (hasPendingExternalUpdate) {
|
|
1998
|
+
<button mat-button type="button" (click)="reloadFromDocument()">
|
|
1999
|
+
<mat-icon>refresh</mat-icon>
|
|
2000
|
+
Recarregar documento
|
|
2001
|
+
</button>
|
|
2002
|
+
}
|
|
2003
|
+
</div>
|
|
2004
|
+
@if (hasPendingExternalUpdate) {
|
|
2005
|
+
<p class="pending-update-note">
|
|
2006
|
+
O documento externo mudou enquanto havia edições locais não aplicadas. Revise e recarregue quando quiser substituir o texto atual.
|
|
2007
|
+
</p>
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
<mat-form-field appearance="outline" class="json-textarea-field">
|
|
2011
|
+
<mat-label>Documento JSON</mat-label>
|
|
2012
|
+
<textarea
|
|
2013
|
+
matInput
|
|
2014
|
+
[(ngModel)]="jsonText"
|
|
2015
|
+
(ngModelChange)="onJsonTextChange($event)"
|
|
2016
|
+
rows="20"
|
|
2017
|
+
spellcheck="false"
|
|
2018
|
+
class="json-textarea"
|
|
2019
|
+
placeholder="Edite o documento canônico da lista aqui...">
|
|
2020
|
+
</textarea>
|
|
2021
|
+
@if (isValidJson) {
|
|
2022
|
+
<mat-hint class="valid-hint">JSON válido</mat-hint>
|
|
2023
|
+
}
|
|
2024
|
+
@if (isValidJson && unknownTopKeys.length) {
|
|
2025
|
+
<mat-hint align="end" class="valid-hint warning-hint">
|
|
2026
|
+
Aviso: chaves desconhecidas no topo — {{ unknownTopKeys.join(', ') }}
|
|
2027
|
+
</mat-hint>
|
|
2028
|
+
}
|
|
2029
|
+
@if (!isValidJson && jsonText) {
|
|
2030
|
+
<mat-error>JSON inválido: {{ jsonError }}</mat-error>
|
|
2031
|
+
}
|
|
2032
|
+
</mat-form-field>
|
|
2033
|
+
</div>
|
|
2034
|
+
</div>
|
|
2035
|
+
`, styles: [".json-config-editor{display:flex;flex-direction:column;gap:16px}.educational-card{background-color:var(--md-sys-color-surface-container-low);border-left:4px solid var(--md-sys-color-primary)}.card-icon{background-color:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container);font-size:20px;width:40px;height:40px;display:flex;align-items:center;justify-content:center}.json-editor-section{display:flex;flex-direction:column;gap:16px}.json-editor-toolbar{display:flex;gap:12px;padding:12px;background-color:var(--md-sys-color-surface-container-low);border-radius:8px;border:1px solid var(--md-sys-color-outline-variant)}.json-textarea-field{width:100%}.pending-update-note{margin:0;color:var(--md-sys-color-secondary);font-size:.875rem}.json-textarea{font-family:Monaco,Menlo,Ubuntu Mono,Consolas,monospace!important;font-size:13px!important;line-height:1.4!important;min-height:320px!important;white-space:pre!important;overflow-wrap:normal!important;overflow-x:auto!important;resize:none!important}.valid-hint{color:var(--md-sys-color-primary)!important}.warning-hint{color:var(--md-sys-color-secondary)!important}\n"] }]
|
|
2036
|
+
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { document: [{
|
|
2037
|
+
type: Input
|
|
2038
|
+
}], documentChange: [{
|
|
2039
|
+
type: Output
|
|
2040
|
+
}], validationChange: [{
|
|
2041
|
+
type: Output
|
|
2042
|
+
}], editorEvent: [{
|
|
2043
|
+
type: Output
|
|
2044
|
+
}] } });
|
|
2045
|
+
|
|
1165
2046
|
const SKIN_PRESETS = [
|
|
1166
2047
|
'pill-soft',
|
|
1167
2048
|
'gradient-tile',
|
|
@@ -1174,6 +2055,7 @@ const SKIN_PRESETS = [
|
|
|
1174
2055
|
class PraxisListConfigEditor {
|
|
1175
2056
|
config;
|
|
1176
2057
|
listId;
|
|
2058
|
+
document = createListAuthoringDocument({ config: {} });
|
|
1177
2059
|
working = {
|
|
1178
2060
|
dataSource: {},
|
|
1179
2061
|
layout: { variant: 'list', lines: 2, density: 'default', dividers: 'between' },
|
|
@@ -1231,29 +2113,15 @@ class PraxisListConfigEditor {
|
|
|
1231
2113
|
setAutoFreeze(false);
|
|
1232
2114
|
// optional inject
|
|
1233
2115
|
this.crud = inject(GenericCrudService, { optional: true });
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
this.working = this.normalize(structuredClone(cfg));
|
|
1237
|
-
// Initialize mapping UI from existing templating
|
|
1238
|
-
this.hydrateMappingFromTemplating(this.working.templating);
|
|
1239
|
-
this.hydrateUiEditorFromConfig();
|
|
1240
|
-
}
|
|
2116
|
+
const incoming = injected?.document ?? injected?.config ?? this.config;
|
|
2117
|
+
this.applyIncomingDocument(incoming);
|
|
1241
2118
|
this.globalActionCatalog = this.resolveGlobalActionCatalog();
|
|
1242
2119
|
this.appliedMappingSnapshot = this.getMappingSnapshot();
|
|
1243
|
-
this.initialJson =
|
|
2120
|
+
this.initialJson = this.snapshotCurrentDocument();
|
|
1244
2121
|
this.lastJson = this.initialJson;
|
|
1245
|
-
this.queryJson = this.working?.dataSource?.query ? JSON.stringify(this.working.dataSource.query, null, 2) : '';
|
|
1246
|
-
this.skeletonCountInput = this.working?.templating?.skeleton?.count ?? this.skeletonCountInput;
|
|
1247
2122
|
// Start schema watcher and emit initial value
|
|
1248
2123
|
this.setupSchemaWatcher();
|
|
1249
2124
|
this.resourcePathChanges.next(this.working?.dataSource?.resourcePath || '');
|
|
1250
|
-
// Hydrate sort controls from existing config
|
|
1251
|
-
const sort = this.working.dataSource?.sort?.[0];
|
|
1252
|
-
if (sort && typeof sort === 'string') {
|
|
1253
|
-
const [field, dir] = sort.split(',');
|
|
1254
|
-
this.sortField = field;
|
|
1255
|
-
this.sortDir = (dir === 'desc' ? 'desc' : 'asc');
|
|
1256
|
-
}
|
|
1257
2125
|
}
|
|
1258
2126
|
ip = inject(IconPickerService);
|
|
1259
2127
|
async pickLeadingIcon() {
|
|
@@ -1282,58 +2150,95 @@ class PraxisListConfigEditor {
|
|
|
1282
2150
|
this.onMappingChanged();
|
|
1283
2151
|
}
|
|
1284
2152
|
getSettingsValue() {
|
|
1285
|
-
|
|
1286
|
-
this.
|
|
1287
|
-
this.normalizeActionPayloads();
|
|
1288
|
-
return { config: this.working, id: this.listId };
|
|
2153
|
+
const document = this.buildCurrentDocument();
|
|
2154
|
+
return { document, config: document.config, id: this.listId };
|
|
1289
2155
|
}
|
|
1290
2156
|
onSave() {
|
|
1291
|
-
this.
|
|
1292
|
-
this.
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
2157
|
+
const document = this.buildCurrentDocument();
|
|
2158
|
+
return { document, config: document.config, id: this.listId };
|
|
2159
|
+
}
|
|
2160
|
+
onJsonConfigChange(newValue) {
|
|
2161
|
+
this.applyIncomingDocument(newValue);
|
|
2162
|
+
this.markDirty();
|
|
2163
|
+
this.verify();
|
|
2164
|
+
}
|
|
2165
|
+
onJsonValidationChange(result) {
|
|
2166
|
+
this.isValid$.next(result.isValid);
|
|
2167
|
+
}
|
|
2168
|
+
onJsonEditorEvent(event) {
|
|
2169
|
+
if (event.type === 'validation')
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
buildJsonAuthoringDocument() {
|
|
2173
|
+
return this.buildCurrentDocument();
|
|
2174
|
+
}
|
|
2175
|
+
applyIncomingDocument(raw) {
|
|
2176
|
+
const document = projectListAuthoringDocument(parseLegacyOrListDocument(raw), {
|
|
2177
|
+
schemaFieldNames: this.fields,
|
|
2178
|
+
});
|
|
2179
|
+
this.document = document;
|
|
2180
|
+
this.working = document.config;
|
|
2181
|
+
this.resetVisualMappingState();
|
|
2182
|
+
this.hydrateMappingFromTemplating(this.working.templating);
|
|
2183
|
+
this.hydrateUiEditorFromConfig();
|
|
2184
|
+
this.queryJson = this.working?.dataSource?.query
|
|
2185
|
+
? JSON.stringify(this.working.dataSource.query, null, 2)
|
|
2186
|
+
: '';
|
|
2187
|
+
this.queryError = '';
|
|
2188
|
+
this.skeletonCountInput =
|
|
2189
|
+
this.working?.templating?.skeleton?.count ?? this.skeletonCountInput;
|
|
2190
|
+
this.syncSortStateFromWorking();
|
|
2191
|
+
}
|
|
2192
|
+
buildCurrentDocument() {
|
|
2193
|
+
const config = this.mappingDirty
|
|
2194
|
+
? this.composeWorkingFromVisualState()
|
|
2195
|
+
: this.working;
|
|
2196
|
+
const document = createListAuthoringDocument({
|
|
2197
|
+
config,
|
|
2198
|
+
});
|
|
2199
|
+
this.document = document;
|
|
2200
|
+
return document;
|
|
2201
|
+
}
|
|
2202
|
+
snapshotCurrentDocument() {
|
|
2203
|
+
return JSON.stringify(serializeListAuthoringDocument(this.buildCurrentDocument()));
|
|
2204
|
+
}
|
|
2205
|
+
syncSortStateFromWorking() {
|
|
2206
|
+
this.sortField = undefined;
|
|
2207
|
+
this.sortDir = 'asc';
|
|
2208
|
+
const sort = this.working.dataSource?.sort?.[0];
|
|
2209
|
+
if (sort && typeof sort === 'string') {
|
|
2210
|
+
const [field, dir] = sort.split(',');
|
|
2211
|
+
this.sortField = field;
|
|
2212
|
+
this.sortDir = dir === 'desc' ? 'desc' : 'asc';
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
resetVisualMappingState() {
|
|
2216
|
+
this.mappingPrimary = { type: 'text' };
|
|
2217
|
+
this.mappingSecondary = { type: 'text' };
|
|
2218
|
+
this.mappingMeta = { type: 'text' };
|
|
2219
|
+
this.mappingTrailing = {};
|
|
2220
|
+
this.mappingLeading = { type: 'icon' };
|
|
2221
|
+
this.mappingSectionHeader = { type: 'text', expr: '${item.key}' };
|
|
2222
|
+
this.mappingEmptyState = {
|
|
2223
|
+
type: 'text',
|
|
2224
|
+
expr: 'Nenhum item disponível',
|
|
1330
2225
|
};
|
|
1331
|
-
|
|
2226
|
+
this.mappingMetaPrefixIcon = undefined;
|
|
2227
|
+
this.mappingMetaFields = [];
|
|
2228
|
+
this.mappingMetaSeparator = ' • ';
|
|
2229
|
+
this.mappingMetaWrapSecondInParens = false;
|
|
2230
|
+
this.features = [];
|
|
2231
|
+
this.featuresVisible = true;
|
|
2232
|
+
this.featuresMode = 'icons+labels';
|
|
2233
|
+
this.statusPosition = undefined;
|
|
2234
|
+
this.iconColorMapEntries = [];
|
|
2235
|
+
this.chipColorMapEntries = [];
|
|
2236
|
+
this.chipLabelMapEntries = [];
|
|
1332
2237
|
}
|
|
1333
2238
|
// Debounced schema loader for resourcePath
|
|
1334
2239
|
setupSchemaWatcher() {
|
|
1335
2240
|
this.resourcePathChanges
|
|
1336
|
-
.pipe(debounceTime(300), distinctUntilChanged(), tap(() => this.isBusy$.next(true)), switchMap((rp) => {
|
|
2241
|
+
.pipe(debounceTime$1(300), distinctUntilChanged(), tap(() => this.isBusy$.next(true)), switchMap((rp) => {
|
|
1337
2242
|
const path = (rp || '').trim();
|
|
1338
2243
|
if (!path || !this.crud) {
|
|
1339
2244
|
this.fields = [];
|
|
@@ -1733,14 +2638,11 @@ class PraxisListConfigEditor {
|
|
|
1733
2638
|
reset() {
|
|
1734
2639
|
try {
|
|
1735
2640
|
this.isBusy$.next(true);
|
|
1736
|
-
const parsed = JSON.parse(this.initialJson);
|
|
1737
|
-
this.
|
|
2641
|
+
const parsed = parseLegacyOrListDocument(JSON.parse(this.initialJson));
|
|
2642
|
+
this.applyIncomingDocument(parsed);
|
|
1738
2643
|
this.lastJson = this.initialJson;
|
|
1739
2644
|
this.mappingDirty = false;
|
|
1740
2645
|
this.appliedMappingSnapshot = this.getMappingSnapshot();
|
|
1741
|
-
this.queryJson = this.working?.dataSource?.query ? JSON.stringify(this.working.dataSource.query, null, 2) : '';
|
|
1742
|
-
this.queryError = '';
|
|
1743
|
-
this.skeletonCountInput = this.working?.templating?.skeleton?.count ?? this.skeletonCountInput;
|
|
1744
2646
|
this.isDirty$.next(false);
|
|
1745
2647
|
this.isValid$.next(true);
|
|
1746
2648
|
}
|
|
@@ -1820,10 +2722,10 @@ class PraxisListConfigEditor {
|
|
|
1820
2722
|
}
|
|
1821
2723
|
}
|
|
1822
2724
|
onPageSizeChange(value) {
|
|
1823
|
-
const
|
|
1824
|
-
const
|
|
2725
|
+
const raw = value === '' || value === null ? undefined : Number(value);
|
|
2726
|
+
const nextValue = raw === undefined || !Number.isFinite(raw) ? raw : Math.trunc(raw);
|
|
1825
2727
|
this.working = produce(this.working, (draft) => {
|
|
1826
|
-
draft.layout.pageSize =
|
|
2728
|
+
draft.layout.pageSize = nextValue;
|
|
1827
2729
|
});
|
|
1828
2730
|
this.markDirty();
|
|
1829
2731
|
this.verify();
|
|
@@ -1917,19 +2819,14 @@ class PraxisListConfigEditor {
|
|
|
1917
2819
|
this.verify();
|
|
1918
2820
|
}
|
|
1919
2821
|
applyTemplate() {
|
|
2822
|
+
this.working = this.composeWorkingFromVisualState();
|
|
2823
|
+
this.mappingDirty = false;
|
|
2824
|
+
this.appliedMappingSnapshot = this.getMappingSnapshot();
|
|
2825
|
+
this.markDirty();
|
|
2826
|
+
this.verify();
|
|
2827
|
+
}
|
|
2828
|
+
composeWorkingFromVisualState() {
|
|
1920
2829
|
const t = { ...this.working.templating };
|
|
1921
|
-
const normalizeRating = (slot) => {
|
|
1922
|
-
if (!slot)
|
|
1923
|
-
return;
|
|
1924
|
-
if (slot.ratingSize != null) {
|
|
1925
|
-
const s = Number(slot.ratingSize);
|
|
1926
|
-
slot.ratingSize = Number.isFinite(s) ? Math.min(32, Math.max(10, Math.round(s))) : undefined;
|
|
1927
|
-
}
|
|
1928
|
-
if (slot.ratingMax != null) {
|
|
1929
|
-
const m = Number(slot.ratingMax);
|
|
1930
|
-
slot.ratingMax = Number.isFinite(m) ? Math.min(10, Math.max(1, Math.round(m))) : undefined;
|
|
1931
|
-
}
|
|
1932
|
-
};
|
|
1933
2830
|
const build = (slot) => {
|
|
1934
2831
|
if (!slot?.field)
|
|
1935
2832
|
return undefined;
|
|
@@ -2021,11 +2918,6 @@ class PraxisListConfigEditor {
|
|
|
2021
2918
|
}
|
|
2022
2919
|
return { type: slot.type, expr, class: slot.class, style: slot.style };
|
|
2023
2920
|
};
|
|
2024
|
-
normalizeRating(this.mappingMeta);
|
|
2025
|
-
normalizeRating(this.mappingTrailing);
|
|
2026
|
-
normalizeRating(this.mappingLeading);
|
|
2027
|
-
normalizeRating(this.mappingSectionHeader);
|
|
2028
|
-
normalizeRating(this.mappingEmptyState);
|
|
2029
2921
|
const p = build(this.mappingPrimary);
|
|
2030
2922
|
const s = build(this.mappingSecondary);
|
|
2031
2923
|
const m = build(this.mappingMeta);
|
|
@@ -2148,17 +3040,15 @@ class PraxisListConfigEditor {
|
|
|
2148
3040
|
t.featuresVisible = this.featuresVisible;
|
|
2149
3041
|
t.featuresMode = this.featuresMode;
|
|
2150
3042
|
t.skeleton = this.skeletonCountInput > 0 ? { count: this.skeletonCountInput } : undefined;
|
|
2151
|
-
|
|
3043
|
+
return produce(this.working, (draft) => {
|
|
2152
3044
|
draft.templating = t;
|
|
2153
3045
|
});
|
|
2154
|
-
this.markDirty();
|
|
2155
|
-
this.verify();
|
|
2156
|
-
this.mappingDirty = false;
|
|
2157
|
-
this.appliedMappingSnapshot = this.getMappingSnapshot();
|
|
2158
3046
|
}
|
|
2159
3047
|
ensureMappingApplied() {
|
|
2160
3048
|
if (this.mappingDirty) {
|
|
2161
|
-
this.
|
|
3049
|
+
this.working = this.composeWorkingFromVisualState();
|
|
3050
|
+
this.mappingDirty = false;
|
|
3051
|
+
this.appliedMappingSnapshot = this.getMappingSnapshot();
|
|
2162
3052
|
}
|
|
2163
3053
|
}
|
|
2164
3054
|
onMappingChanged() {
|
|
@@ -2542,11 +3432,12 @@ class PraxisListConfigEditor {
|
|
|
2542
3432
|
}
|
|
2543
3433
|
markDirty() {
|
|
2544
3434
|
setTimeout(() => {
|
|
3435
|
+
this.document = this.buildCurrentDocument();
|
|
2545
3436
|
if (this.mappingDirty) {
|
|
2546
3437
|
this.isDirty$.next(true);
|
|
2547
3438
|
return;
|
|
2548
3439
|
}
|
|
2549
|
-
const current = JSON.stringify(this.
|
|
3440
|
+
const current = JSON.stringify(serializeListAuthoringDocument(this.document));
|
|
2550
3441
|
this.lastJson = current;
|
|
2551
3442
|
this.isDirty$.next(current !== this.initialJson);
|
|
2552
3443
|
});
|
|
@@ -2577,106 +3468,19 @@ class PraxisListConfigEditor {
|
|
|
2577
3468
|
updateMappingDirty() {
|
|
2578
3469
|
this.mappingDirty = this.getMappingSnapshot() !== this.appliedMappingSnapshot;
|
|
2579
3470
|
}
|
|
2580
|
-
isJsonLike(value) {
|
|
2581
|
-
const trimmed = value.trim();
|
|
2582
|
-
return (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'));
|
|
2583
|
-
}
|
|
2584
3471
|
resolveGlobalActionCatalog() {
|
|
2585
3472
|
const injected = inject(GLOBAL_ACTION_CATALOG, { optional: true });
|
|
2586
3473
|
if (!injected)
|
|
2587
3474
|
return PRAXIS_GLOBAL_ACTION_CATALOG.slice();
|
|
2588
3475
|
return getGlobalActionCatalog(injected);
|
|
2589
3476
|
}
|
|
2590
|
-
normalizeActionPayloads() {
|
|
2591
|
-
const actions = this.working?.actions || [];
|
|
2592
|
-
if (!Array.isArray(actions))
|
|
2593
|
-
return;
|
|
2594
|
-
for (const a of actions) {
|
|
2595
|
-
if (!a)
|
|
2596
|
-
continue;
|
|
2597
|
-
const v = a.globalPayload;
|
|
2598
|
-
if (typeof v !== 'string')
|
|
2599
|
-
continue;
|
|
2600
|
-
const trimmed = v.trim();
|
|
2601
|
-
if (!trimmed) {
|
|
2602
|
-
a.globalPayload = undefined;
|
|
2603
|
-
continue;
|
|
2604
|
-
}
|
|
2605
|
-
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
2606
|
-
try {
|
|
2607
|
-
a.globalPayload = JSON.parse(trimmed);
|
|
2608
|
-
}
|
|
2609
|
-
catch {
|
|
2610
|
-
// keep as string
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
3477
|
verify() {
|
|
2616
3478
|
setTimeout(() => {
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
const
|
|
2621
|
-
|
|
2622
|
-
const sortField = (this.sortField || '').trim();
|
|
2623
|
-
let valid = true;
|
|
2624
|
-
// Allow configs without resourcePath (local data or template-only editing)
|
|
2625
|
-
if (pageSizeNum != null && (!Number.isFinite(pageSizeNum) || pageSizeNum < 1))
|
|
2626
|
-
valid = false;
|
|
2627
|
-
if (![1, 2, 3].includes(Number(lines)))
|
|
2628
|
-
valid = false;
|
|
2629
|
-
if (sortField && this.fields.length && !this.fields.includes(sortField))
|
|
2630
|
-
valid = false;
|
|
2631
|
-
// selection.compareBy should exist when fields are loaded
|
|
2632
|
-
const compareBy = (this.working?.selection?.compareBy || '').trim();
|
|
2633
|
-
if (compareBy && this.fields.length && !this.fields.includes(compareBy))
|
|
2634
|
-
valid = false;
|
|
2635
|
-
// Duplicatas em opções de ordenação (UI) tornam inválido
|
|
2636
|
-
if (valid && this.working?.ui?.showSort) {
|
|
2637
|
-
const rows = this.uiSortRows || [];
|
|
2638
|
-
const seen = new Map();
|
|
2639
|
-
for (const r of rows) {
|
|
2640
|
-
const k = `${(r?.field || '').trim()},${(r?.dir || 'desc').trim()}`;
|
|
2641
|
-
if (!r?.field)
|
|
2642
|
-
continue;
|
|
2643
|
-
seen.set(k, (seen.get(k) || 0) + 1);
|
|
2644
|
-
}
|
|
2645
|
-
for (const [, count] of seen) {
|
|
2646
|
-
if (count > 1) {
|
|
2647
|
-
valid = false;
|
|
2648
|
-
break;
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
if (this.queryError)
|
|
2653
|
-
valid = false;
|
|
2654
|
-
if ((this.mappingLeading?.type === 'image' && !this.validateImageUrl(this.mappingLeading.imageUrl)) ||
|
|
2655
|
-
(this.mappingTrailing?.type === 'image' && !this.validateImageUrl(this.mappingTrailing.imageUrl)) ||
|
|
2656
|
-
(this.mappingSectionHeader?.type === 'image' && !this.validateImageUrl(this.mappingSectionHeader.imageUrl)) ||
|
|
2657
|
-
(this.mappingEmptyState?.type === 'image' && !this.validateImageUrl(this.mappingEmptyState.imageUrl))) {
|
|
2658
|
-
valid = false;
|
|
2659
|
-
}
|
|
2660
|
-
if ((this.mappingMeta?.type === 'rating' && (this.isRatingSizeInvalid(this.mappingMeta.ratingSize) || this.isRatingMaxInvalid(this.mappingMeta.ratingMax))) ||
|
|
2661
|
-
(this.mappingLeading?.type === 'rating' && (this.isRatingSizeInvalid(this.mappingLeading.ratingSize) || this.isRatingMaxInvalid(this.mappingLeading.ratingMax))) ||
|
|
2662
|
-
(this.mappingTrailing?.type === 'rating' && (this.isRatingSizeInvalid(this.mappingTrailing.ratingSize) || this.isRatingMaxInvalid(this.mappingTrailing.ratingMax))) ||
|
|
2663
|
-
(this.mappingSectionHeader?.type === 'rating' && (this.isRatingSizeInvalid(this.mappingSectionHeader.ratingSize) || this.isRatingMaxInvalid(this.mappingSectionHeader.ratingMax))) ||
|
|
2664
|
-
(this.mappingEmptyState?.type === 'rating' && (this.isRatingSizeInvalid(this.mappingEmptyState.ratingSize) || this.isRatingMaxInvalid(this.mappingEmptyState.ratingMax)))) {
|
|
2665
|
-
valid = false;
|
|
2666
|
-
}
|
|
2667
|
-
// Validação de JSON nas Actions
|
|
2668
|
-
if (this.working?.actions?.some(a => this.isGlobalPayloadInvalid(a.globalPayload))) {
|
|
2669
|
-
valid = false;
|
|
2670
|
-
}
|
|
2671
|
-
if (valid && Array.isArray(this.working?.actions)) {
|
|
2672
|
-
for (const a of this.working.actions) {
|
|
2673
|
-
if (this.isGlobalPayloadInvalid(a?.globalPayload)) {
|
|
2674
|
-
valid = false;
|
|
2675
|
-
break;
|
|
2676
|
-
}
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
this.isValid$.next(valid);
|
|
3479
|
+
const diagnostics = validateListAuthoringDocument(this.buildCurrentDocument(), {
|
|
3480
|
+
schemaFieldNames: this.fields,
|
|
3481
|
+
});
|
|
3482
|
+
const hasErrors = diagnostics.some((item) => item.level === 'error');
|
|
3483
|
+
this.isValid$.next(!hasErrors && !this.queryError);
|
|
2680
3484
|
});
|
|
2681
3485
|
}
|
|
2682
3486
|
// UI editor hydration already called in ctor
|
|
@@ -2684,10 +3488,10 @@ class PraxisListConfigEditor {
|
|
|
2684
3488
|
inferFromFields() {
|
|
2685
3489
|
if (!this.fields?.length)
|
|
2686
3490
|
return;
|
|
2687
|
-
const
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
this.
|
|
3491
|
+
const inferred = inferListAuthoringDocument(this.buildCurrentDocument(), {
|
|
3492
|
+
schemaFieldNames: this.fields,
|
|
3493
|
+
});
|
|
3494
|
+
this.applyIncomingDocument(inferred);
|
|
2691
3495
|
this.markDirty();
|
|
2692
3496
|
this.verify();
|
|
2693
3497
|
}
|
|
@@ -2826,7 +3630,7 @@ class PraxisListConfigEditor {
|
|
|
2826
3630
|
}
|
|
2827
3631
|
}
|
|
2828
3632
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisListConfigEditor, deps: [{ token: SETTINGS_PANEL_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
2829
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisListConfigEditor, isStandalone: true, selector: "praxis-list-config-editor", inputs: { config: "config", listId: "listId" }, ngImport: i0, template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab label=\"Dados\">\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">Observa\u00E7\u00E3o: ajustes aplicados pelo assistente substituem o objeto de configura\u00E7\u00E3o inteiro.\n </div>\n <button mat-icon-button type=\"button\" class=\"help-icon-button\"\n matTooltip=\"O applyConfigFromAdapter n\u00E3o faz merge profundo. Garanta que o adapter envie a config completa.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Recurso (API)</mat-label>\n <input matInput [(ngModel)]=\"working.dataSource.resourcePath\" (ngModelChange)=\"onResourcePathChange($event)\"\n placeholder=\"ex.: users\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Endpoint do recurso (resourcePath).\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Query (JSON)</mat-label>\n <textarea matInput rows=\"3\" [(ngModel)]=\"queryJson\" (ngModelChange)=\"onQueryChanged($event)\"\n placeholder='ex.: {\"status\":\"active\",\"department\":\"sales\"}'></textarea>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Opcional. Use JSON v\u00E1lido para filtros iniciais.\" *ngIf=\"!queryError\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"queryError\">{{ queryError }}</mat-error>\n </mat-form-field>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Ordenar por</mat-label>\n <mat-select [(ngModel)]=\"sortField\" (ngModelChange)=\"updateSortConfig()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"Campo base do recurso.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Dire\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"sortDir\" (ngModelChange)=\"updateSortConfig()\">\n <mat-option value=\"asc\">Ascendente</mat-option>\n <mat-option value=\"desc\">Descendente</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"A\u00E7\u00F5es\">\n <div class=\"editor-content g gap-12\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">Configure bot\u00F5es de a\u00E7\u00E3o por item (\u00EDcone, r\u00F3tulo, cor, visibilidade)</div>\n <button mat-flat-button color=\"primary\" (click)=\"addAction()\">Adicionar a\u00E7\u00E3o</button>\n </div>\n <div class=\"g g-1-auto gap-8 ai-center\">\n <mat-form-field appearance=\"outline\">\n <mat-label>A\u00E7\u00E3o global (Praxis)</mat-label>\n <mat-select [(ngModel)]=\"selectedGlobalActionId\" (ngModelChange)=\"onGlobalActionSelected($event)\">\n <mat-option [value]=\"undefined\">-- Selecionar --</mat-option>\n <mat-option *ngFor=\"let ga of globalActionCatalog\" [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || 'bolt' }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n </mat-select>\n <mat-hint *ngIf=\"!globalActionCatalog.length\" class=\"text-caption muted\">Nenhuma a\u00E7\u00E3o global registrada.</mat-hint>\n </mat-form-field>\n <div class=\"muted text-caption\">Selecione para adicionar com `command` global.</div>\n </div>\n <div *ngFor=\"let a of (working.actions || []); let i = index\" class=\"g g-auto-200 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>ID</mat-label>\n <input matInput [(ngModel)]=\"a.id\" (ngModelChange)=\"onActionsChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo de a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">\u00CDcone</mat-option>\n <mat-option value=\"button\">Bot\u00E3o</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input matInput [(ngModel)]=\"a.icon\" (ngModelChange)=\"onActionsChanged()\" placeholder=\"ex.: edit, delete\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Command (global)</mat-label>\n <input matInput [(ngModel)]=\"a.command\" (ngModelChange)=\"onActionsChanged()\" placeholder=\"global:toast.success\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input matInput [(ngModel)]=\"a.label\" (ngModelChange)=\"onActionsChanged()\" />\n </mat-form-field>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select [(ngModel)]=\"a.buttonVariant\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"stroked\">Contorno</mat-option>\n <mat-option value=\"raised\">Elevado</mat-option>\n <mat-option value=\"flat\">Preenchido</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>Cor da a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.color\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span class=\"color-dot\" [style.background]=\"colorDotBackground(c.value)\"></span>{{ c.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"g gap-8\" *ngIf=\"isCustomColor(a.color); else actionCustomBtn\">\n <pdx-color-picker label=\"Cor personalizada\" [format]=\"'hex'\" [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button mat-stroked-button type=\"button\" (click)=\"enableCustomActionColor(a)\">Usar cor personalizada</button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>Payload da a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.emitPayload\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option [value]=\"undefined\">Padr\u00E3o</mat-option>\n <mat-option value=\"item\">item</mat-option>\n <mat-option value=\"id\">id</mat-option>\n <mat-option value=\"value\">value</mat-option>\n </mat-select>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"emitPayload\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>Exibir quando (ex.: ${item.status} == 'done')</mat-label>\n <input matInput [(ngModel)]=\"a.showIf\" (ngModelChange)=\"onActionsChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Sintaxe suportada: "${item.campo} == 'valor'". Express\u00F5es avan\u00E7adas n\u00E3o s\u00E3o avaliadas.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button *ngIf=\"(a.kind || 'icon') === 'icon'\" mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"><mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\" [style.cssText]=\"iconStyle(a.color)\"></mat-icon></button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button *ngIf=\"a.buttonVariant === 'stroked'\" mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'stroked')\">{{ a.label\n || a.id || 'A\u00E7\u00E3o' }}</button>\n <button *ngIf=\"a.buttonVariant === 'raised'\" mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'raised')\">{{ a.label ||\n a.id || 'A\u00E7\u00E3o' }}</button>\n <button *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\" mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'flat')\">{{ a.label || a.id || 'A\u00E7\u00E3o' }}</button>\n </ng-container>\n <span class=\"muted\">Pr\u00E9-visualiza\u00E7\u00E3o</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">Remover</button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle [(ngModel)]=\"a.showLoading\" (ngModelChange)=\"onActionsChanged()\">Mostrar loading</mat-slide-toggle>\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Confirma\u00E7\u00E3o</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">Tipo</span>\n <mat-button-toggle-group [value]=\"a.confirmation?.type || ''\" (change)=\"applyConfirmationPreset(a, $event.value)\">\n <mat-button-toggle value=\"\">Padr\u00E3o</mat-button-toggle>\n <mat-button-toggle value=\"danger\">Danger</mat-button-toggle>\n <mat-button-toggle value=\"warning\">Warning</mat-button-toggle>\n <mat-button-toggle value=\"info\">Info</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>T\u00EDtulo</mat-label>\n <input matInput [ngModel]=\"a.confirmation?.title\" (ngModelChange)=\"setConfirmationField(a, 'title', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem</mat-label>\n <input matInput [ngModel]=\"a.confirmation?.message\" (ngModelChange)=\"setConfirmationField(a, 'message', $event)\" />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">Pr\u00E9via</div>\n <div class=\"text-caption\">\n <strong>{{ a.confirmation?.title || 'Confirmar a\u00E7\u00E3o' }}</strong>\n </div>\n <div class=\"text-caption muted\">{{ a.confirmation?.message || 'Tem certeza que deseja continuar?' }}</div>\n <div class=\"text-caption\">\n <span class=\"confirm-type\" [ngClass]=\"(a.confirmation?.type || 'default')\">Tipo: {{ a.confirmation?.type || 'padr\u00E3o' }}</span>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\">\n Defina um t\u00EDtulo ou mensagem para a confirma\u00E7\u00E3o.\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>Payload (JSON/Template)</mat-label>\n <textarea matInput rows=\"4\" [(ngModel)]=\"a.globalPayload\" (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'></textarea>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\">JSON inv\u00E1lido</mat-error>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button mat-stroked-button type=\"button\" (click)=\"applyGlobalPayloadExample(a)\">Inserir exemplo</button>\n <span class=\"muted text-caption\">{{ globalPayloadExampleHint(a) }}</span>\n </div>\n <mat-slide-toggle [(ngModel)]=\"a.emitLocal\" (ngModelChange)=\"onActionsChanged()\">Emitir evento local tamb\u00E9m</mat-slide-toggle>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Layout\">\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-stroked-button (click)=\"applyLayoutPreset('tiles-modern')\">Preset Tiles Moderno</button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select [(ngModel)]=\"working.layout.variant\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"list\">Lista</mat-option>\n <mat-option value=\"cards\">Cards</mat-option>\n <mat-option value=\"tiles\">Tiles</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Modelo</mat-label>\n <mat-select [(ngModel)]=\"working.layout.model\" (ngModelChange)=\"onLayoutChanged()\">\n <ng-container *ngIf=\"working.layout.variant === 'list'; else cardModels\">\n <mat-option value=\"standard\">Padr\u00E3o</mat-option>\n <mat-option value=\"media\">M\u00EDdia \u00E0 esquerda</mat-option>\n <mat-option value=\"hotel\">Hotel (m\u00EDdia grande)</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\">\n <mat-option value=\"standard\">Tile padr\u00E3o</mat-option>\n <mat-option value=\"media\">Tile com m\u00EDdia</mat-option>\n <mat-option value=\"hotel\">Tile hotel</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">Padr\u00E3o</mat-option>\n <mat-option value=\"media\">Card com m\u00EDdia</mat-option>\n <mat-option value=\"hotel\">Hotel</mat-option>\n </ng-template>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Linhas</mat-label>\n <mat-select [(ngModel)]=\"working.layout.lines\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option [value]=\"1\">1</mat-option>\n <mat-option [value]=\"2\">2</mat-option>\n <mat-option [value]=\"3\">3</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Itens por p\u00E1gina</mat-label>\n <input matInput type=\"number\" min=\"1\" [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Densidade</mat-label>\n <mat-select [(ngModel)]=\"working.layout.density\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"default\">Padr\u00E3o</mat-option>\n <mat-option value=\"comfortable\">Confort\u00E1vel</mat-option>\n <mat-option value=\"compact\">Compacta</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"working.layout.variant !== 'tiles'\">\n <mat-label>Divisores</mat-label>\n <mat-select [(ngModel)]=\"working.layout.dividers\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"none\">Sem</mat-option>\n <mat-option value=\"between\">Entre grupos</mat-option>\n <mat-option value=\"all\">Todos</mat-option>\n </mat-select>\n </mat-form-field>\n <ng-container *ngIf=\"fields.length > 0; else groupByText\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Agrupar por</mat-label>\n <mat-select [(ngModel)]=\"working.layout.groupBy\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option [value]=\"\">Nenhum</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <ng-template #groupByText>\n <mat-form-field appearance=\"outline\">\n <mat-label>Agrupar por</mat-label>\n <input matInput [(ngModel)]=\"working.layout.groupBy\" (ngModelChange)=\"onLayoutChanged()\"\n placeholder=\"ex.: departamento\" />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle [(ngModel)]=\"working.layout.stickySectionHeader\" (ngModelChange)=\"onLayoutChanged()\">\n Header de se\u00E7\u00E3o fixo\n </mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.layout.virtualScroll\" (ngModelChange)=\"onLayoutChanged()\">\n Scroll virtual\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Ferramentas da lista</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle [(ngModel)]=\"working.ui.showSearch\" (ngModelChange)=\"onUiChanged()\">Mostrar\n busca</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.ui.showSort\" (ngModelChange)=\"onUiChanged()\">Mostrar\n ordenar</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.ui.showRange\" (ngModelChange)=\"onUiChanged()\">Mostrar faixa X\u2013Y de\n Total</mat-slide-toggle>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\" *ngIf=\"working.ui?.showSearch\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo para buscar</mat-label>\n <mat-select [(ngModel)]=\"working.ui.searchField\" (ngModelChange)=\"onUiChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Placeholder da busca</mat-label>\n <input matInput [(ngModel)]=\"working.ui.searchPlaceholder\" (ngModelChange)=\"onUiChanged()\"\n placeholder=\"ex.: Buscar por t\u00EDtulo\" />\n </mat-form-field>\n </div>\n <div class=\"mt-12\" *ngIf=\"working.ui?.showSort\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">Op\u00E7\u00F5es de ordena\u00E7\u00E3o (r\u00F3tulo \u2192 campo+dire\u00E7\u00E3o)</div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">Adicionar op\u00E7\u00E3o</button>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\" *ngFor=\"let r of uiSortRows; let i = index\">\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input matInput [(ngModel)]=\"r.label\" (ngModelChange)=\"onUiSortRowsChanged()\"\n placeholder=\"ex.: Mais recentes\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"r.field\" (ngModelChange)=\"onUiSortRowsChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Dire\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"r.dir\" (ngModelChange)=\"onUiSortRowsChanged()\">\n <mat-option value=\"desc\">Descendente</mat-option>\n <mat-option value=\"asc\">Ascendente</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">Op\u00E7\u00E3o duplicada (campo+dire\u00E7\u00E3o)</div>\n <div class=\"flex-end\"><button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">Remover</button></div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Conte\u00FAdo\">\n <div class=\"editor-content\">\n <div class=\"editor-main\">\n <mat-accordion multi>\n <!-- Primary -->\n <mat-expansion-panel [expanded]=\"true\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingPrimary.type) }}</mat-icon>\n <span>Primary (T\u00EDtulo)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingPrimary.field || 'N\u00E3o mapeado' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='name'; onMappingChanged()\">Nome</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='title'; onMappingChanged()\">T\u00EDtulo</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='name'; mappingSecondary.type='text'; mappingSecondary.field='role'; onMappingChanged()\">Nome + Papel</button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingPrimary.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingPrimary.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of primaryTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingPrimary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingPrimary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingPrimary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingPrimary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>Formata\u00E7\u00E3o e Estilo</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\" *ngIf=\"mappingPrimary.type==='text' || mappingPrimary.type==='html'\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"mappingPrimary.type==='text' || mappingPrimary.type==='html'\">\n <mat-label>Estilo Inline</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo Inline</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Secondary -->\n <mat-expansion-panel [expanded]=\"!!mappingSecondary.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSecondary.type) }}</mat-icon>\n <span>Secondary (Resumo)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingSecondary.field || 'N\u00E3o mapeado' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='text'; mappingSecondary.field='subtitle'; onMappingChanged()\">Subt\u00EDtulo</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='date'; mappingSecondary.field='hireDate'; mappingSecondary.dateStyle='short'; onMappingChanged()\">Data curta</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='currency'; mappingSecondary.field='salary'; mappingSecondary.currencyCode='BRL'; mappingSecondary.locale='pt-BR'; onMappingChanged()\">Sal\u00E1rio</button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingSecondary.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingSecondary.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of secondaryTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingSecondary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingSecondary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingSecondary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingSecondary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>Formata\u00E7\u00E3o e Estilo</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe CSS</mat-label><input matInput\n [(ngModel)]=\"mappingSecondary.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Estilo Inline</mat-label><input matInput\n [(ngModel)]=\"mappingSecondary.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingMeta.type || 'text') }}</mat-icon>\n <span>Meta (Detalhe/Lateral)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{ mappingMetaFields.length ? 'Campo composto (' + mappingMetaFields.length + ')' :\n (mappingMeta.field || 'N\u00E3o mapeado') }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <!-- Composition Mode Toggle -->\n <div class=\"g g-1-1 gap-12 p-12 bg-subtle rounded\">\n <div class=\"text-caption muted\">Modo de composi\u00E7\u00E3o</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campos para compor (Multi-select)</mat-label>\n <mat-select [(ngModel)]=\"mappingMetaFields\" multiple (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"g g-1-1 ai-center gap-12\" *ngIf=\"mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Separador</mat-label>\n <input matInput [(ngModel)]=\"mappingMetaSeparator\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-slide-toggle [(ngModel)]=\"mappingMetaWrapSecondInParens\" (ngModelChange)=\"onMappingChanged()\">\n (Seg) entre par\u00EAnteses\n </mat-slide-toggle>\n </div>\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"!mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo \u00DAnico</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of metaTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Type configuration (pluggable editors) -->\n @switch (mappingMeta.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingMeta\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingMeta\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') { <praxis-meta-editor-image [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image> }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Op\u00E7\u00F5es\n avan\u00E7adas</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Posi\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.placement\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option value=\"side\">Lateral (Direita)</mat-option>\n <mat-option value=\"line\">Na linha (Abaixo)</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingMeta.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <input matInput [(ngModel)]=\"mappingMeta.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n <!-- Trailing -->\n <mat-expansion-panel [expanded]=\"!!mappingTrailing.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingTrailing.type || 'text') }}</mat-icon>\n <span>Trailing (Direita)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingTrailing.field || 'N\u00E3o mapeado'\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingTrailing.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingTrailing.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of trailingTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='chip'; mappingTrailing.chipColor='primary'; mappingTrailing.chipVariant='filled'; mappingTrailing.field='status'; onMappingChanged()\">Status Chip</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='icon'; mappingTrailing.field='status'; mappingTrailing.iconColor='primary'; onMappingChanged()\">Status \u00CDcone</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='currency'; mappingTrailing.field='price'; mappingTrailing.currencyCode='BRL'; mappingTrailing.locale='pt-BR'; onMappingChanged()\">Pre\u00E7o</button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingTrailing\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingTrailing\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL / Expr</mat-label>\n <input matInput [(ngModel)]=\"mappingTrailing.imageUrl\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <praxis-meta-editor-image [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n <div class=\"text-caption muted\" *ngIf=\"!mappingTrailing.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingTrailing.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingTrailing.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Leading -->\n <mat-expansion-panel\n [expanded]=\"!!mappingLeading.field || (mappingLeading.type === 'icon' && !!mappingLeading.icon) || (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingLeading.type) }}</mat-icon>\n <span>Leading (Esquerda)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{ mappingLeading.type === 'icon' ? (mappingLeading.icon || '\u00CDcone est\u00E1tico') :\n (mappingLeading.field || (mappingLeading.imageUrl ? 'Imagem est\u00E1tica' : 'N\u00E3o mapeado'))\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingLeading.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of leadingTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n <mat-form-field appearance=\"outline\"\n *ngIf=\"mappingLeading.type !== 'icon' && mappingLeading.type !== 'image'\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingLeading.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='icon'; mappingLeading.icon='person'; mappingLeading.iconColor='primary'; onMappingChanged()\">Avatar \u00CDcone</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='image'; mappingLeading.imageUrl='https://placehold.co/64x64'; mappingLeading.imageAlt='Avatar'; mappingLeading.badgeText='${item.status}'; onMappingChanged()\">Avatar Imagem + Badge</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='chip'; mappingLeading.field='tag'; mappingLeading.chipColor='accent'; mappingLeading.chipVariant='filled'; onMappingChanged()\">Chip Tag</button>\n </div>\n\n <!-- Icon Specific -->\n <div class=\"g g-1-auto gap-12 ai-center\" *ngIf=\"mappingLeading.type === 'icon'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.icon\" (ngModelChange)=\"onMappingChanged()\" />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\"><mat-icon>search</mat-icon></button>\n </mat-form-field>\n <div class=\"text-caption muted\">Use pipe <code>|iconMap</code> no extra pipe para\n din\u00E2mico</div>\n </div>\n <div *ngIf=\"mappingLeading.type === 'icon'\">\n <praxis-meta-editor-icon\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"mappingLeading.type === 'image'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL da Imagem</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.imageUrl\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Alt Text</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.imageAlt\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Badge Texto</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.badgeText\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingLeading\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingLeading\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingLeading.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingLeading.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Features -->\n <mat-expansion-panel [expanded]=\"featuresVisible && features.length > 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>view_list</mat-icon>\n <span>Recursos (Features)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ features.length }} item(s)</mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-12 ai-center\">\n <mat-slide-toggle [(ngModel)]=\"featuresVisible\" (ngModelChange)=\"onFeaturesChanged()\">Ativar\n recursos</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"featuresSyncWithMeta\" (ngModelChange)=\"onMappingChanged()\">Sincronizar\n com Meta</mat-slide-toggle>\n <span class=\"flex-1\"></span>\n <mat-button-toggle-group [(ngModel)]=\"featuresMode\" (change)=\"onFeaturesChanged()\" appearance=\"legacy\">\n <mat-button-toggle value=\"icons+labels\"><mat-icon>view_list</mat-icon></mat-button-toggle>\n <mat-button-toggle value=\"icons-only\"><mat-icon>more_horiz</mat-icon></mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n\n <div *ngFor=\"let f of features; let i = index\" class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\">\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\"><mat-icon>{{ f.icon || 'search'\n }}</mat-icon></button>\n <mat-form-field appearance=\"outline\" class=\"dense-form-field no-sub\">\n <input matInput [(ngModel)]=\"f.expr\" (ngModelChange)=\"onFeaturesChanged()\" placeholder=\"Expr/Texto\" />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\"><mat-icon>delete</mat-icon></button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\"><mat-icon>add</mat-icon>\n Adicionar recurso</button>\n </div>\n </mat-expansion-panel>\n <!-- Section Header -->\n <mat-expansion-panel [expanded]=\"!!mappingSectionHeader.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSectionHeader.type) }}</mat-icon>\n <span>Cabe\u00E7alho de Se\u00E7\u00E3o</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingSectionHeader.expr || 'N\u00E3o configurado'\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingSectionHeader.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of sectionHeaderTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Express\u00E3o (item.key)</mat-label>\n <input matInput [(ngModel)]=\"mappingSectionHeader.expr\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\" />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSectionHeader.type='text'; mappingSectionHeader.expr='${item.key}'; onMappingChanged()\">Texto padr\u00E3o</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSectionHeader.type='chip'; mappingSectionHeader.chipColor='primary'; mappingSectionHeader.chipVariant='filled'; mappingSectionHeader.expr='${item.key}'; onMappingChanged()\">Chip padr\u00E3o</button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingSectionHeader\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingSectionHeader\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL Imagem</mat-label>\n <input matInput [(ngModel)]=\"mappingSectionHeader.imageUrl\" (ngModelChange)=\"onMappingChanged()\" />\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingSectionHeader.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!mappingSectionHeader.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n <praxis-meta-editor-image [model]=\"mappingSectionHeader\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingSectionHeader.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingSectionHeader.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Empty State -->\n <mat-expansion-panel [expanded]=\"!!mappingEmptyState.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>inbox</mat-icon>\n <span>Estado Vazio</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingEmptyState.expr || 'Padr\u00E3o' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingEmptyState.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of emptyStateTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem / Expr</mat-label>\n <input matInput [(ngModel)]=\"mappingEmptyState.expr\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingEmptyState.type='text'; mappingEmptyState.expr='Nenhum item dispon\u00EDvel'; onMappingChanged()\">Mensagem padr\u00E3o</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingEmptyState.type='image'; mappingEmptyState.imageUrl='/list-empty-state.svg'; mappingEmptyState.imageAlt='Sem resultados'; onMappingChanged()\">Imagem padr\u00E3o</button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingEmptyState\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingEmptyState\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>URL Imagem</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.imageUrl\" (ngModelChange)=\"onMappingChanged()\" />\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!mappingEmptyState.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n <praxis-meta-editor-image [model]=\"mappingEmptyState\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n </mat-accordion>\n\n <button mat-flat-button color=\"primary\" (click)=\"applyTemplate()\">Aplicar mapeamento</button>\n <button mat-button (click)=\"inferFromFields()\" [disabled]=\"!fields.length\">Inferir do schema</button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Skeleton (quantidade)</mat-label>\n <input matInput type=\"number\" min=\"0\" [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\" />\n </mat-form-field>\n </div>\n\n <div class=\"g gap-12 mt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"section-title mat-subtitle-1\">Pr\u00E9via de tema</span>\n <mat-button-toggle-group [(ngModel)]=\"skinPreviewTheme\" (change)=\"onSkinChanged()\" appearance=\"legacy\">\n <mat-button-toggle [value]=\"'light'\">Claro</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">Escuro</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">Grade</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview [config]=\"working\" [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n\n </mat-tab>\n <mat-tab label=\"i18n/A11y\">\n <div class=\"editor-content grid gap-3\" *ngIf=\"working?.a11y && working?.events\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Locale padr\u00E3o</mat-label>\n <input matInput [(ngModel)]=\"working.i18n.locale\" (ngModelChange)=\"markDirty()\" placeholder=\"ex.: pt-BR\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Moeda padr\u00E3o</mat-label>\n <input matInput [(ngModel)]=\"working.i18n.currency\" (ngModelChange)=\"markDirty()\" placeholder=\"ex.: BRL\" />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Acessibilidade</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-label</mat-label>\n <input matInput [(ngModel)]=\"working!.a11y!.ariaLabel\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-labelledby</mat-label>\n <input matInput [(ngModel)]=\"working!.a11y!.ariaLabelledBy\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle [(ngModel)]=\"working!.a11y!.highContrast\" (ngModelChange)=\"markDirty()\">Alto\n contraste</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working!.a11y!.reduceMotion\" (ngModelChange)=\"markDirty()\">Reduzir\n movimento</mat-slide-toggle>\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Eventos</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>itemClick</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.itemClick\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.actionClick\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.selectionChange\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.loaded\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Sele\u00E7\u00E3o\">\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Modo</mat-label>\n <mat-select [(ngModel)]=\"working.selection.mode\" (ngModelChange)=\"onSelectionChanged()\">\n <mat-option value=\"none\">Sem sele\u00E7\u00E3o</mat-option>\n <mat-option value=\"single\">\u00DAnica</mat-option>\n <mat-option value=\"multiple\">M\u00FAltipla</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Nome no formul\u00E1rio</mat-label>\n <input matInput [(ngModel)]=\"working.selection.formControlName\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"formControlName\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Caminho no formul\u00E1rio</mat-label>\n <input matInput [(ngModel)]=\"working.selection.formControlPath\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"formControlPath\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Comparar por (campo)</mat-label>\n <input matInput [(ngModel)]=\"working.selection.compareBy\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"Chave unica do item.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Retorno</mat-label>\n <mat-select [(ngModel)]=\"working.selection.return\" (ngModelChange)=\"onSelectionChanged()\">\n <mat-option value=\"value\">value</mat-option>\n <mat-option value=\"item\">item</mat-option>\n <mat-option value=\"id\">id</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab label=\"Apar\u00EAncia\">\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-button (click)=\"applySkinPreset('pill-soft')\">Pill Soft</button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">Gradient Tile</button>\n <button mat-button (click)=\"applySkinPreset('glass')\">Glass</button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">Elevated</button>\n <button mat-button (click)=\"applySkinPreset('outline')\">Outline</button>\n <button mat-button (click)=\"applySkinPreset('flat')\">Flat</button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">Neumorphism</button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <mat-select [(ngModel)]=\"working.skin.type\" (ngModelChange)=\"onSkinTypeChanged($event)\">\n <mat-option value=\"pill-soft\">Pill Soft</mat-option>\n <mat-option value=\"gradient-tile\">Gradient Tile</mat-option>\n <mat-option value=\"glass\">Glass</mat-option>\n <mat-option value=\"elevated\">Elevated</mat-option>\n <mat-option value=\"outline\">Outline</mat-option>\n <mat-option value=\"flat\">Flat</mat-option>\n <mat-option value=\"neumorphism\">Neumorphism</mat-option>\n <mat-option value=\"custom\">Custom</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Raio</mat-label>\n <input matInput [(ngModel)]=\"working.skin.radius\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 1.25rem\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Sombra</mat-label>\n <input matInput [(ngModel)]=\"working.skin.shadow\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: var(--md-sys-elevation-level2)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Borda</mat-label>\n <input matInput [(ngModel)]=\"working.skin.border\" (ngModelChange)=\"onSkinChanged()\" />\n </mat-form-field>\n <mat-form-field *ngIf=\"working.skin.type==='glass'\" appearance=\"outline\">\n <mat-label>Desfoque</mat-label>\n <input matInput [(ngModel)]=\"working.skin.backdropBlur\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 8px\" />\n </mat-form-field>\n <div *ngIf=\"working.skin.type==='gradient-tile'\" class=\"g gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA de</mat-label>\n <input matInput [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA at\u00E9</mat-label>\n <input matInput [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00C2ngulo</mat-label>\n <input matInput type=\"number\" [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\" />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS extra (skin.class)</mat-label>\n <input matInput [(ngModel)]=\"working.skin.class\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: my-list-skin\" />\n </mat-form-field>\n\n <div *ngIf=\"working.skin.type==='custom'\" class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Estilo inline (skin.inlineStyle)</mat-label>\n <textarea matInput rows=\"4\" [(ngModel)]=\"working.skin.inlineStyle\" (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n Exemplo de CSS por classe (adicione no seu styles global):\n <pre class=\"code-block\">.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre>\n </div>\n </div>\n\n\n </div>\n </mat-tab>\n</mat-tab-group>\n", styles: [".confirm-type{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;line-height:16px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.confirm-type.danger{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.confirm-type.warning{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.confirm-type.info{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}:host{display:block;color:var(--md-sys-color-on-surface)}.list-editor-tabs{--editor-surface: var(--md-sys-color-surface-container-lowest);--editor-border: 1px solid var(--md-sys-color-outline-variant);--editor-radius: var(--md-sys-shape-corner-large, 16px);--editor-muted: var(--md-sys-color-on-surface-variant);--editor-accent: var(--md-sys-color-primary)}.editor-content{padding:16px;background:var(--editor-surface);border:var(--editor-border);border-radius:var(--editor-radius);display:grid;gap:12px}.editor-content .mat-mdc-form-field{width:100%;max-width:none;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.editor-content .mat-mdc-form-field.w-full{max-width:none}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.editor-split{grid-template-columns:minmax(0,1fr);align-items:start}.editor-main,.editor-aside{display:grid;gap:12px}.skin-preview-wrap{border-radius:calc(var(--editor-radius) - 4px);border:var(--editor-border);background:var(--md-sys-color-surface-container);padding:12px}.g{display:grid}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.g-auto-200{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.g-1-auto{grid-template-columns:1fr auto}.row-flow{grid-auto-flow:column}.gap-6{gap:6px}.gap-8{gap:8px}.gap-12{gap:12px}.ai-center{align-items:center}.ai-end{align-items:end}.mt-12{margin-top:12px}.mb-8{margin-bottom:8px}.mb-6{margin-bottom:6px}.my-8{margin:8px 0}.subtitle{margin:8px 0 4px;color:var(--editor-muted);font-weight:500}.section-title{color:var(--editor-muted);font-weight:600}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--md-sys-color-error);font-size:.85rem}.muted{color:var(--editor-muted)}.text-caption{color:var(--editor-muted);font-size:.8rem}:host ::ng-deep .mat-mdc-select-panel .option-icon{font-size:18px;margin-right:6px;vertical-align:middle}:host ::ng-deep .mat-mdc-select-panel .color-dot{width:10px;height:10px;border-radius:999px;display:inline-block;margin-right:6px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-outline)}:host ::ng-deep .mat-mdc-select-panel .color-primary{background:var(--md-sys-color-primary)}:host ::ng-deep .mat-mdc-select-panel .color-accent{background:var(--md-sys-color-tertiary)}:host ::ng-deep .mat-mdc-select-panel .color-warn{background:var(--md-sys-color-error)}:host ::ng-deep .mat-mdc-select-panel .color-default{background:var(--md-sys-color-outline)}@media(max-width:1024px){.editor-split{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i3$2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i3$2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i2$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i10.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i10.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i10.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i10.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i10.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i11.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i11.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i12.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "ngmodule", type: MatMenuModule }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisListSkinPreviewComponent, selector: "praxis-list-skin-preview", inputs: ["config", "items", "theme"] }, { kind: "component", type: PraxisMetaEditorTextComponent, selector: "praxis-meta-editor-text", inputs: ["model", "setPipe"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorChipComponent, selector: "praxis-meta-editor-chip", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorRatingComponent, selector: "praxis-meta-editor-rating", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorCurrencyComponent, selector: "praxis-meta-editor-currency", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorDateComponent, selector: "praxis-meta-editor-date", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorIconComponent, selector: "praxis-meta-editor-icon", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorImageComponent, selector: "praxis-meta-editor-image", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PdxColorPickerComponent, selector: "pdx-color-picker", inputs: ["actionsLayout", "activeView", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "clearButton", "disabledMode", "readonlyMode", "visible", "presentationMode", "fillMode", "format", "gradientSettings", "icon", "iconClass", "svgIcon", "paletteSettings", "popupSettings", "preview", "rounded", "size", "tabindex", "views", "maxRecent", "showRecent"], outputs: ["valueChange", "open", "close", "cancel", "activeViewChange", "activeColorClick", "focusEvent", "blurEvent"] }] });
|
|
3633
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisListConfigEditor, isStandalone: true, selector: "praxis-list-config-editor", inputs: { config: "config", listId: "listId" }, ngImport: i0, template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab label=\"Dados\">\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">Observa\u00E7\u00E3o: ajustes aplicados pelo assistente substituem o objeto de configura\u00E7\u00E3o inteiro.\n </div>\n <button mat-icon-button type=\"button\" class=\"help-icon-button\"\n matTooltip=\"O applyConfigFromAdapter n\u00E3o faz merge profundo. Garanta que o adapter envie a config completa.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Recurso (API)</mat-label>\n <input matInput [(ngModel)]=\"working.dataSource.resourcePath\" (ngModelChange)=\"onResourcePathChange($event)\"\n placeholder=\"ex.: users\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Endpoint do recurso (resourcePath).\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Query (JSON)</mat-label>\n <textarea matInput rows=\"3\" [(ngModel)]=\"queryJson\" (ngModelChange)=\"onQueryChanged($event)\"\n placeholder='ex.: {\"status\":\"active\",\"department\":\"sales\"}'></textarea>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Opcional. Use JSON v\u00E1lido para filtros iniciais.\" *ngIf=\"!queryError\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"queryError\">{{ queryError }}</mat-error>\n </mat-form-field>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Ordenar por</mat-label>\n <mat-select [(ngModel)]=\"sortField\" (ngModelChange)=\"updateSortConfig()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"Campo base do recurso.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Dire\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"sortDir\" (ngModelChange)=\"updateSortConfig()\">\n <mat-option value=\"asc\">Ascendente</mat-option>\n <mat-option value=\"desc\">Descendente</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"JSON\">\n <div class=\"editor-content\">\n <praxis-list-json-config-editor\n [document]=\"document\"\n (documentChange)=\"onJsonConfigChange($event)\"\n (validationChange)=\"onJsonValidationChange($event)\"\n (editorEvent)=\"onJsonEditorEvent($event)\">\n </praxis-list-json-config-editor>\n </div>\n </mat-tab>\n <mat-tab label=\"A\u00E7\u00F5es\">\n <div class=\"editor-content g gap-12\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">Configure bot\u00F5es de a\u00E7\u00E3o por item (\u00EDcone, r\u00F3tulo, cor, visibilidade)</div>\n <button mat-flat-button color=\"primary\" (click)=\"addAction()\">Adicionar a\u00E7\u00E3o</button>\n </div>\n <div class=\"g g-1-auto gap-8 ai-center\">\n <mat-form-field appearance=\"outline\">\n <mat-label>A\u00E7\u00E3o global (Praxis)</mat-label>\n <mat-select [(ngModel)]=\"selectedGlobalActionId\" (ngModelChange)=\"onGlobalActionSelected($event)\">\n <mat-option [value]=\"undefined\">-- Selecionar --</mat-option>\n <mat-option *ngFor=\"let ga of globalActionCatalog\" [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || 'bolt' }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n </mat-select>\n <mat-hint *ngIf=\"!globalActionCatalog.length\" class=\"text-caption muted\">Nenhuma a\u00E7\u00E3o global registrada.</mat-hint>\n </mat-form-field>\n <div class=\"muted text-caption\">Selecione para adicionar com `command` global.</div>\n </div>\n <div *ngFor=\"let a of (working.actions || []); let i = index\" class=\"g g-auto-200 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>ID</mat-label>\n <input matInput [(ngModel)]=\"a.id\" (ngModelChange)=\"onActionsChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo de a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">\u00CDcone</mat-option>\n <mat-option value=\"button\">Bot\u00E3o</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input matInput [(ngModel)]=\"a.icon\" (ngModelChange)=\"onActionsChanged()\" placeholder=\"ex.: edit, delete\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Command (global)</mat-label>\n <input matInput [(ngModel)]=\"a.command\" (ngModelChange)=\"onActionsChanged()\" placeholder=\"global:toast.success\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input matInput [(ngModel)]=\"a.label\" (ngModelChange)=\"onActionsChanged()\" />\n </mat-form-field>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select [(ngModel)]=\"a.buttonVariant\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"stroked\">Contorno</mat-option>\n <mat-option value=\"raised\">Elevado</mat-option>\n <mat-option value=\"flat\">Preenchido</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>Cor da a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.color\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span class=\"color-dot\" [style.background]=\"colorDotBackground(c.value)\"></span>{{ c.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"g gap-8\" *ngIf=\"isCustomColor(a.color); else actionCustomBtn\">\n <pdx-color-picker label=\"Cor personalizada\" [format]=\"'hex'\" [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button mat-stroked-button type=\"button\" (click)=\"enableCustomActionColor(a)\">Usar cor personalizada</button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>Payload da a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.emitPayload\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option [value]=\"undefined\">Padr\u00E3o</mat-option>\n <mat-option value=\"item\">item</mat-option>\n <mat-option value=\"id\">id</mat-option>\n <mat-option value=\"value\">value</mat-option>\n </mat-select>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"emitPayload\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>Exibir quando (ex.: ${item.status} == 'done')</mat-label>\n <input matInput [(ngModel)]=\"a.showIf\" (ngModelChange)=\"onActionsChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Sintaxe suportada: "${item.campo} == 'valor'". Express\u00F5es avan\u00E7adas n\u00E3o s\u00E3o avaliadas.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button *ngIf=\"(a.kind || 'icon') === 'icon'\" mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"><mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\" [style.cssText]=\"iconStyle(a.color)\"></mat-icon></button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button *ngIf=\"a.buttonVariant === 'stroked'\" mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'stroked')\">{{ a.label\n || a.id || 'A\u00E7\u00E3o' }}</button>\n <button *ngIf=\"a.buttonVariant === 'raised'\" mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'raised')\">{{ a.label ||\n a.id || 'A\u00E7\u00E3o' }}</button>\n <button *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\" mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'flat')\">{{ a.label || a.id || 'A\u00E7\u00E3o' }}</button>\n </ng-container>\n <span class=\"muted\">Pr\u00E9-visualiza\u00E7\u00E3o</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">Remover</button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle [(ngModel)]=\"a.showLoading\" (ngModelChange)=\"onActionsChanged()\">Mostrar loading</mat-slide-toggle>\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Confirma\u00E7\u00E3o</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">Tipo</span>\n <mat-button-toggle-group [value]=\"a.confirmation?.type || ''\" (change)=\"applyConfirmationPreset(a, $event.value)\">\n <mat-button-toggle value=\"\">Padr\u00E3o</mat-button-toggle>\n <mat-button-toggle value=\"danger\">Danger</mat-button-toggle>\n <mat-button-toggle value=\"warning\">Warning</mat-button-toggle>\n <mat-button-toggle value=\"info\">Info</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>T\u00EDtulo</mat-label>\n <input matInput [ngModel]=\"a.confirmation?.title\" (ngModelChange)=\"setConfirmationField(a, 'title', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem</mat-label>\n <input matInput [ngModel]=\"a.confirmation?.message\" (ngModelChange)=\"setConfirmationField(a, 'message', $event)\" />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">Pr\u00E9via</div>\n <div class=\"text-caption\">\n <strong>{{ a.confirmation?.title || 'Confirmar a\u00E7\u00E3o' }}</strong>\n </div>\n <div class=\"text-caption muted\">{{ a.confirmation?.message || 'Tem certeza que deseja continuar?' }}</div>\n <div class=\"text-caption\">\n <span class=\"confirm-type\" [ngClass]=\"(a.confirmation?.type || 'default')\">Tipo: {{ a.confirmation?.type || 'padr\u00E3o' }}</span>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\">\n Defina um t\u00EDtulo ou mensagem para a confirma\u00E7\u00E3o.\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>Payload (JSON/Template)</mat-label>\n <textarea matInput rows=\"4\" [(ngModel)]=\"a.globalPayload\" (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'></textarea>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\">JSON inv\u00E1lido</mat-error>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button mat-stroked-button type=\"button\" (click)=\"applyGlobalPayloadExample(a)\">Inserir exemplo</button>\n <span class=\"muted text-caption\">{{ globalPayloadExampleHint(a) }}</span>\n </div>\n <mat-slide-toggle [(ngModel)]=\"a.emitLocal\" (ngModelChange)=\"onActionsChanged()\">Emitir evento local tamb\u00E9m</mat-slide-toggle>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Layout\">\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-stroked-button (click)=\"applyLayoutPreset('tiles-modern')\">Preset Tiles Moderno</button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select [(ngModel)]=\"working.layout.variant\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"list\">Lista</mat-option>\n <mat-option value=\"cards\">Cards</mat-option>\n <mat-option value=\"tiles\">Tiles</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Modelo</mat-label>\n <mat-select [(ngModel)]=\"working.layout.model\" (ngModelChange)=\"onLayoutChanged()\">\n <ng-container *ngIf=\"working.layout.variant === 'list'; else cardModels\">\n <mat-option value=\"standard\">Padr\u00E3o</mat-option>\n <mat-option value=\"media\">M\u00EDdia \u00E0 esquerda</mat-option>\n <mat-option value=\"hotel\">Hotel (m\u00EDdia grande)</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\">\n <mat-option value=\"standard\">Tile padr\u00E3o</mat-option>\n <mat-option value=\"media\">Tile com m\u00EDdia</mat-option>\n <mat-option value=\"hotel\">Tile hotel</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">Padr\u00E3o</mat-option>\n <mat-option value=\"media\">Card com m\u00EDdia</mat-option>\n <mat-option value=\"hotel\">Hotel</mat-option>\n </ng-template>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Linhas</mat-label>\n <mat-select [(ngModel)]=\"working.layout.lines\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option [value]=\"1\">1</mat-option>\n <mat-option [value]=\"2\">2</mat-option>\n <mat-option [value]=\"3\">3</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Itens por p\u00E1gina</mat-label>\n <input matInput type=\"number\" min=\"1\" [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Densidade</mat-label>\n <mat-select [(ngModel)]=\"working.layout.density\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"default\">Padr\u00E3o</mat-option>\n <mat-option value=\"comfortable\">Confort\u00E1vel</mat-option>\n <mat-option value=\"compact\">Compacta</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"working.layout.variant !== 'tiles'\">\n <mat-label>Divisores</mat-label>\n <mat-select [(ngModel)]=\"working.layout.dividers\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"none\">Sem</mat-option>\n <mat-option value=\"between\">Entre grupos</mat-option>\n <mat-option value=\"all\">Todos</mat-option>\n </mat-select>\n </mat-form-field>\n <ng-container *ngIf=\"fields.length > 0; else groupByText\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Agrupar por</mat-label>\n <mat-select [(ngModel)]=\"working.layout.groupBy\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option [value]=\"\">Nenhum</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <ng-template #groupByText>\n <mat-form-field appearance=\"outline\">\n <mat-label>Agrupar por</mat-label>\n <input matInput [(ngModel)]=\"working.layout.groupBy\" (ngModelChange)=\"onLayoutChanged()\"\n placeholder=\"ex.: departamento\" />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle [(ngModel)]=\"working.layout.stickySectionHeader\" (ngModelChange)=\"onLayoutChanged()\">\n Header de se\u00E7\u00E3o fixo\n </mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.layout.virtualScroll\" (ngModelChange)=\"onLayoutChanged()\">\n Scroll virtual\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Ferramentas da lista</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle [(ngModel)]=\"working.ui.showSearch\" (ngModelChange)=\"onUiChanged()\">Mostrar\n busca</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.ui.showSort\" (ngModelChange)=\"onUiChanged()\">Mostrar\n ordenar</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.ui.showRange\" (ngModelChange)=\"onUiChanged()\">Mostrar faixa X\u2013Y de\n Total</mat-slide-toggle>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\" *ngIf=\"working.ui?.showSearch\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo para buscar</mat-label>\n <mat-select [(ngModel)]=\"working.ui.searchField\" (ngModelChange)=\"onUiChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Placeholder da busca</mat-label>\n <input matInput [(ngModel)]=\"working.ui.searchPlaceholder\" (ngModelChange)=\"onUiChanged()\"\n placeholder=\"ex.: Buscar por t\u00EDtulo\" />\n </mat-form-field>\n </div>\n <div class=\"mt-12\" *ngIf=\"working.ui?.showSort\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">Op\u00E7\u00F5es de ordena\u00E7\u00E3o (r\u00F3tulo \u2192 campo+dire\u00E7\u00E3o)</div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">Adicionar op\u00E7\u00E3o</button>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\" *ngFor=\"let r of uiSortRows; let i = index\">\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input matInput [(ngModel)]=\"r.label\" (ngModelChange)=\"onUiSortRowsChanged()\"\n placeholder=\"ex.: Mais recentes\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"r.field\" (ngModelChange)=\"onUiSortRowsChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Dire\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"r.dir\" (ngModelChange)=\"onUiSortRowsChanged()\">\n <mat-option value=\"desc\">Descendente</mat-option>\n <mat-option value=\"asc\">Ascendente</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">Op\u00E7\u00E3o duplicada (campo+dire\u00E7\u00E3o)</div>\n <div class=\"flex-end\"><button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">Remover</button></div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Conte\u00FAdo\">\n <div class=\"editor-content\">\n <div class=\"editor-main\">\n <mat-accordion multi>\n <!-- Primary -->\n <mat-expansion-panel [expanded]=\"true\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingPrimary.type) }}</mat-icon>\n <span>Primary (T\u00EDtulo)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingPrimary.field || 'N\u00E3o mapeado' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='name'; onMappingChanged()\">Nome</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='title'; onMappingChanged()\">T\u00EDtulo</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='name'; mappingSecondary.type='text'; mappingSecondary.field='role'; onMappingChanged()\">Nome + Papel</button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingPrimary.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingPrimary.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of primaryTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingPrimary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingPrimary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingPrimary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingPrimary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>Formata\u00E7\u00E3o e Estilo</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\" *ngIf=\"mappingPrimary.type==='text' || mappingPrimary.type==='html'\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"mappingPrimary.type==='text' || mappingPrimary.type==='html'\">\n <mat-label>Estilo Inline</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo Inline</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Secondary -->\n <mat-expansion-panel [expanded]=\"!!mappingSecondary.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSecondary.type) }}</mat-icon>\n <span>Secondary (Resumo)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingSecondary.field || 'N\u00E3o mapeado' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='text'; mappingSecondary.field='subtitle'; onMappingChanged()\">Subt\u00EDtulo</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='date'; mappingSecondary.field='hireDate'; mappingSecondary.dateStyle='short'; onMappingChanged()\">Data curta</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='currency'; mappingSecondary.field='salary'; mappingSecondary.currencyCode='BRL'; mappingSecondary.locale='pt-BR'; onMappingChanged()\">Sal\u00E1rio</button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingSecondary.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingSecondary.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of secondaryTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingSecondary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingSecondary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingSecondary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingSecondary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>Formata\u00E7\u00E3o e Estilo</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe CSS</mat-label><input matInput\n [(ngModel)]=\"mappingSecondary.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Estilo Inline</mat-label><input matInput\n [(ngModel)]=\"mappingSecondary.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingMeta.type || 'text') }}</mat-icon>\n <span>Meta (Detalhe/Lateral)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{ mappingMetaFields.length ? 'Campo composto (' + mappingMetaFields.length + ')' :\n (mappingMeta.field || 'N\u00E3o mapeado') }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <!-- Composition Mode Toggle -->\n <div class=\"g g-1-1 gap-12 p-12 bg-subtle rounded\">\n <div class=\"text-caption muted\">Modo de composi\u00E7\u00E3o</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campos para compor (Multi-select)</mat-label>\n <mat-select [(ngModel)]=\"mappingMetaFields\" multiple (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"g g-1-1 ai-center gap-12\" *ngIf=\"mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Separador</mat-label>\n <input matInput [(ngModel)]=\"mappingMetaSeparator\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-slide-toggle [(ngModel)]=\"mappingMetaWrapSecondInParens\" (ngModelChange)=\"onMappingChanged()\">\n (Seg) entre par\u00EAnteses\n </mat-slide-toggle>\n </div>\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"!mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo \u00DAnico</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of metaTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Type configuration (pluggable editors) -->\n @switch (mappingMeta.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingMeta\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingMeta\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') { <praxis-meta-editor-image [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image> }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Op\u00E7\u00F5es\n avan\u00E7adas</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Posi\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.placement\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option value=\"side\">Lateral (Direita)</mat-option>\n <mat-option value=\"line\">Na linha (Abaixo)</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingMeta.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <input matInput [(ngModel)]=\"mappingMeta.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n <!-- Trailing -->\n <mat-expansion-panel [expanded]=\"!!mappingTrailing.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingTrailing.type || 'text') }}</mat-icon>\n <span>Trailing (Direita)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingTrailing.field || 'N\u00E3o mapeado'\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingTrailing.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingTrailing.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of trailingTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='chip'; mappingTrailing.chipColor='primary'; mappingTrailing.chipVariant='filled'; mappingTrailing.field='status'; onMappingChanged()\">Status Chip</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='icon'; mappingTrailing.field='status'; mappingTrailing.iconColor='primary'; onMappingChanged()\">Status \u00CDcone</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='currency'; mappingTrailing.field='price'; mappingTrailing.currencyCode='BRL'; mappingTrailing.locale='pt-BR'; onMappingChanged()\">Pre\u00E7o</button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingTrailing\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingTrailing\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL / Expr</mat-label>\n <input matInput [(ngModel)]=\"mappingTrailing.imageUrl\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <praxis-meta-editor-image [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n <div class=\"text-caption muted\" *ngIf=\"!mappingTrailing.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingTrailing.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingTrailing.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Leading -->\n <mat-expansion-panel\n [expanded]=\"!!mappingLeading.field || (mappingLeading.type === 'icon' && !!mappingLeading.icon) || (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingLeading.type) }}</mat-icon>\n <span>Leading (Esquerda)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{ mappingLeading.type === 'icon' ? (mappingLeading.icon || '\u00CDcone est\u00E1tico') :\n (mappingLeading.field || (mappingLeading.imageUrl ? 'Imagem est\u00E1tica' : 'N\u00E3o mapeado'))\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingLeading.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of leadingTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n <mat-form-field appearance=\"outline\"\n *ngIf=\"mappingLeading.type !== 'icon' && mappingLeading.type !== 'image'\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingLeading.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='icon'; mappingLeading.icon='person'; mappingLeading.iconColor='primary'; onMappingChanged()\">Avatar \u00CDcone</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='image'; mappingLeading.imageUrl='https://placehold.co/64x64'; mappingLeading.imageAlt='Avatar'; mappingLeading.badgeText='${item.status}'; onMappingChanged()\">Avatar Imagem + Badge</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='chip'; mappingLeading.field='tag'; mappingLeading.chipColor='accent'; mappingLeading.chipVariant='filled'; onMappingChanged()\">Chip Tag</button>\n </div>\n\n <!-- Icon Specific -->\n <div class=\"g g-1-auto gap-12 ai-center\" *ngIf=\"mappingLeading.type === 'icon'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.icon\" (ngModelChange)=\"onMappingChanged()\" />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\"><mat-icon>search</mat-icon></button>\n </mat-form-field>\n <div class=\"text-caption muted\">Use pipe <code>|iconMap</code> no extra pipe para\n din\u00E2mico</div>\n </div>\n <div *ngIf=\"mappingLeading.type === 'icon'\">\n <praxis-meta-editor-icon\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"mappingLeading.type === 'image'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL da Imagem</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.imageUrl\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Alt Text</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.imageAlt\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Badge Texto</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.badgeText\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingLeading\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingLeading\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingLeading.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingLeading.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Features -->\n <mat-expansion-panel [expanded]=\"featuresVisible && features.length > 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>view_list</mat-icon>\n <span>Recursos (Features)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ features.length }} item(s)</mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-12 ai-center\">\n <mat-slide-toggle [(ngModel)]=\"featuresVisible\" (ngModelChange)=\"onFeaturesChanged()\">Ativar\n recursos</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"featuresSyncWithMeta\" (ngModelChange)=\"onMappingChanged()\">Sincronizar\n com Meta</mat-slide-toggle>\n <span class=\"flex-1\"></span>\n <mat-button-toggle-group [(ngModel)]=\"featuresMode\" (change)=\"onFeaturesChanged()\" appearance=\"legacy\">\n <mat-button-toggle value=\"icons+labels\"><mat-icon>view_list</mat-icon></mat-button-toggle>\n <mat-button-toggle value=\"icons-only\"><mat-icon>more_horiz</mat-icon></mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n\n <div *ngFor=\"let f of features; let i = index\" class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\">\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\"><mat-icon>{{ f.icon || 'search'\n }}</mat-icon></button>\n <mat-form-field appearance=\"outline\" class=\"dense-form-field no-sub\">\n <input matInput [(ngModel)]=\"f.expr\" (ngModelChange)=\"onFeaturesChanged()\" placeholder=\"Expr/Texto\" />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\"><mat-icon>delete</mat-icon></button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\"><mat-icon>add</mat-icon>\n Adicionar recurso</button>\n </div>\n </mat-expansion-panel>\n <!-- Section Header -->\n <mat-expansion-panel [expanded]=\"!!mappingSectionHeader.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSectionHeader.type) }}</mat-icon>\n <span>Cabe\u00E7alho de Se\u00E7\u00E3o</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingSectionHeader.expr || 'N\u00E3o configurado'\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingSectionHeader.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of sectionHeaderTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Express\u00E3o (item.key)</mat-label>\n <input matInput [(ngModel)]=\"mappingSectionHeader.expr\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\" />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSectionHeader.type='text'; mappingSectionHeader.expr='${item.key}'; onMappingChanged()\">Texto padr\u00E3o</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSectionHeader.type='chip'; mappingSectionHeader.chipColor='primary'; mappingSectionHeader.chipVariant='filled'; mappingSectionHeader.expr='${item.key}'; onMappingChanged()\">Chip padr\u00E3o</button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingSectionHeader\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingSectionHeader\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL Imagem</mat-label>\n <input matInput [(ngModel)]=\"mappingSectionHeader.imageUrl\" (ngModelChange)=\"onMappingChanged()\" />\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingSectionHeader.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!mappingSectionHeader.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n <praxis-meta-editor-image [model]=\"mappingSectionHeader\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingSectionHeader.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingSectionHeader.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Empty State -->\n <mat-expansion-panel [expanded]=\"!!mappingEmptyState.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>inbox</mat-icon>\n <span>Estado Vazio</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingEmptyState.expr || 'Padr\u00E3o' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingEmptyState.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of emptyStateTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem / Expr</mat-label>\n <input matInput [(ngModel)]=\"mappingEmptyState.expr\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingEmptyState.type='text'; mappingEmptyState.expr='Nenhum item dispon\u00EDvel'; onMappingChanged()\">Mensagem padr\u00E3o</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingEmptyState.type='image'; mappingEmptyState.imageUrl='/list-empty-state.svg'; mappingEmptyState.imageAlt='Sem resultados'; onMappingChanged()\">Imagem padr\u00E3o</button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingEmptyState\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingEmptyState\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>URL Imagem</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.imageUrl\" (ngModelChange)=\"onMappingChanged()\" />\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!mappingEmptyState.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n <praxis-meta-editor-image [model]=\"mappingEmptyState\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n </mat-accordion>\n\n <button mat-flat-button color=\"primary\" (click)=\"applyTemplate()\">Aplicar mapeamento</button>\n <button mat-button (click)=\"inferFromFields()\" [disabled]=\"!fields.length\">Inferir do schema</button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Skeleton (quantidade)</mat-label>\n <input matInput type=\"number\" min=\"0\" [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\" />\n </mat-form-field>\n </div>\n\n <div class=\"g gap-12 mt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"section-title mat-subtitle-1\">Pr\u00E9via de tema</span>\n <mat-button-toggle-group [(ngModel)]=\"skinPreviewTheme\" (change)=\"onSkinChanged()\" appearance=\"legacy\">\n <mat-button-toggle [value]=\"'light'\">Claro</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">Escuro</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">Grade</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview [config]=\"working\" [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n\n </mat-tab>\n <mat-tab label=\"i18n/A11y\">\n <div class=\"editor-content grid gap-3\" *ngIf=\"working?.a11y && working?.events\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Locale padr\u00E3o</mat-label>\n <input matInput [(ngModel)]=\"working.i18n.locale\" (ngModelChange)=\"markDirty()\" placeholder=\"ex.: pt-BR\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Moeda padr\u00E3o</mat-label>\n <input matInput [(ngModel)]=\"working.i18n.currency\" (ngModelChange)=\"markDirty()\" placeholder=\"ex.: BRL\" />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Acessibilidade</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-label</mat-label>\n <input matInput [(ngModel)]=\"working!.a11y!.ariaLabel\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-labelledby</mat-label>\n <input matInput [(ngModel)]=\"working!.a11y!.ariaLabelledBy\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle [(ngModel)]=\"working!.a11y!.highContrast\" (ngModelChange)=\"markDirty()\">Alto\n contraste</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working!.a11y!.reduceMotion\" (ngModelChange)=\"markDirty()\">Reduzir\n movimento</mat-slide-toggle>\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Eventos</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>itemClick</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.itemClick\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.actionClick\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.selectionChange\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.loaded\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Sele\u00E7\u00E3o\">\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Modo</mat-label>\n <mat-select [(ngModel)]=\"working.selection.mode\" (ngModelChange)=\"onSelectionChanged()\">\n <mat-option value=\"none\">Sem sele\u00E7\u00E3o</mat-option>\n <mat-option value=\"single\">\u00DAnica</mat-option>\n <mat-option value=\"multiple\">M\u00FAltipla</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Nome no formul\u00E1rio</mat-label>\n <input matInput [(ngModel)]=\"working.selection.formControlName\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"formControlName\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Caminho no formul\u00E1rio</mat-label>\n <input matInput [(ngModel)]=\"working.selection.formControlPath\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"formControlPath\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Comparar por (campo)</mat-label>\n <input matInput [(ngModel)]=\"working.selection.compareBy\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"Chave unica do item.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Retorno</mat-label>\n <mat-select [(ngModel)]=\"working.selection.return\" (ngModelChange)=\"onSelectionChanged()\">\n <mat-option value=\"value\">value</mat-option>\n <mat-option value=\"item\">item</mat-option>\n <mat-option value=\"id\">id</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab label=\"Apar\u00EAncia\">\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-button (click)=\"applySkinPreset('pill-soft')\">Pill Soft</button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">Gradient Tile</button>\n <button mat-button (click)=\"applySkinPreset('glass')\">Glass</button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">Elevated</button>\n <button mat-button (click)=\"applySkinPreset('outline')\">Outline</button>\n <button mat-button (click)=\"applySkinPreset('flat')\">Flat</button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">Neumorphism</button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <mat-select [(ngModel)]=\"working.skin.type\" (ngModelChange)=\"onSkinTypeChanged($event)\">\n <mat-option value=\"pill-soft\">Pill Soft</mat-option>\n <mat-option value=\"gradient-tile\">Gradient Tile</mat-option>\n <mat-option value=\"glass\">Glass</mat-option>\n <mat-option value=\"elevated\">Elevated</mat-option>\n <mat-option value=\"outline\">Outline</mat-option>\n <mat-option value=\"flat\">Flat</mat-option>\n <mat-option value=\"neumorphism\">Neumorphism</mat-option>\n <mat-option value=\"custom\">Custom</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Raio</mat-label>\n <input matInput [(ngModel)]=\"working.skin.radius\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 1.25rem\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Sombra</mat-label>\n <input matInput [(ngModel)]=\"working.skin.shadow\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: var(--md-sys-elevation-level2)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Borda</mat-label>\n <input matInput [(ngModel)]=\"working.skin.border\" (ngModelChange)=\"onSkinChanged()\" />\n </mat-form-field>\n <mat-form-field *ngIf=\"working.skin.type==='glass'\" appearance=\"outline\">\n <mat-label>Desfoque</mat-label>\n <input matInput [(ngModel)]=\"working.skin.backdropBlur\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 8px\" />\n </mat-form-field>\n <div *ngIf=\"working.skin.type==='gradient-tile'\" class=\"g gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA de</mat-label>\n <input matInput [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA at\u00E9</mat-label>\n <input matInput [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00C2ngulo</mat-label>\n <input matInput type=\"number\" [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\" />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS extra (skin.class)</mat-label>\n <input matInput [(ngModel)]=\"working.skin.class\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: my-list-skin\" />\n </mat-form-field>\n\n <div *ngIf=\"working.skin.type==='custom'\" class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Estilo inline (skin.inlineStyle)</mat-label>\n <textarea matInput rows=\"4\" [(ngModel)]=\"working.skin.inlineStyle\" (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n Exemplo de CSS por classe (adicione no seu styles global):\n <pre class=\"code-block\">.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre>\n </div>\n </div>\n\n\n </div>\n </mat-tab>\n</mat-tab-group>\n", styles: [".confirm-type{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;line-height:16px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.confirm-type.danger{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.confirm-type.warning{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.confirm-type.info{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}:host{display:block;color:var(--md-sys-color-on-surface)}.list-editor-tabs{--editor-surface: var(--md-sys-color-surface-container-lowest);--editor-border: 1px solid var(--md-sys-color-outline-variant);--editor-radius: var(--md-sys-shape-corner-large, 16px);--editor-muted: var(--md-sys-color-on-surface-variant);--editor-accent: var(--md-sys-color-primary)}.editor-content{padding:16px;background:var(--editor-surface);border:var(--editor-border);border-radius:var(--editor-radius);display:grid;gap:12px}.editor-content .mat-mdc-form-field{width:100%;max-width:none;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.editor-content .mat-mdc-form-field.w-full{max-width:none}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.editor-split{grid-template-columns:minmax(0,1fr);align-items:start}.editor-main,.editor-aside{display:grid;gap:12px}.skin-preview-wrap{border-radius:calc(var(--editor-radius) - 4px);border:var(--editor-border);background:var(--md-sys-color-surface-container);padding:12px}.g{display:grid}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.g-auto-200{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.g-1-auto{grid-template-columns:1fr auto}.row-flow{grid-auto-flow:column}.gap-6{gap:6px}.gap-8{gap:8px}.gap-12{gap:12px}.ai-center{align-items:center}.ai-end{align-items:end}.mt-12{margin-top:12px}.mb-8{margin-bottom:8px}.mb-6{margin-bottom:6px}.my-8{margin:8px 0}.subtitle{margin:8px 0 4px;color:var(--editor-muted);font-weight:500}.section-title{color:var(--editor-muted);font-weight:600}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--md-sys-color-error);font-size:.85rem}.muted{color:var(--editor-muted)}.text-caption{color:var(--editor-muted);font-size:.8rem}:host ::ng-deep .mat-mdc-select-panel .option-icon{font-size:18px;margin-right:6px;vertical-align:middle}:host ::ng-deep .mat-mdc-select-panel .color-dot{width:10px;height:10px;border-radius:999px;display:inline-block;margin-right:6px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-outline)}:host ::ng-deep .mat-mdc-select-panel .color-primary{background:var(--md-sys-color-primary)}:host ::ng-deep .mat-mdc-select-panel .color-accent{background:var(--md-sys-color-tertiary)}:host ::ng-deep .mat-mdc-select-panel .color-warn{background:var(--md-sys-color-error)}:host ::ng-deep .mat-mdc-select-panel .color-default{background:var(--md-sys-color-outline)}@media(max-width:1024px){.editor-split{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i3$3.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i3$3.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i2$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i10.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i10.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i10.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i10.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i10.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i11.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i11.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i12.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "ngmodule", type: MatMenuModule }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisListSkinPreviewComponent, selector: "praxis-list-skin-preview", inputs: ["config", "items", "theme"] }, { kind: "component", type: PraxisMetaEditorTextComponent, selector: "praxis-meta-editor-text", inputs: ["model", "setPipe"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorChipComponent, selector: "praxis-meta-editor-chip", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorRatingComponent, selector: "praxis-meta-editor-rating", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorCurrencyComponent, selector: "praxis-meta-editor-currency", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorDateComponent, selector: "praxis-meta-editor-date", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorIconComponent, selector: "praxis-meta-editor-icon", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorImageComponent, selector: "praxis-meta-editor-image", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisListJsonConfigEditorComponent, selector: "praxis-list-json-config-editor", inputs: ["document"], outputs: ["documentChange", "validationChange", "editorEvent"] }, { kind: "component", type: PdxColorPickerComponent, selector: "pdx-color-picker", inputs: ["actionsLayout", "activeView", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "clearButton", "disabledMode", "readonlyMode", "visible", "presentationMode", "fillMode", "format", "gradientSettings", "icon", "iconClass", "svgIcon", "paletteSettings", "popupSettings", "preview", "rounded", "size", "tabindex", "views", "maxRecent", "showRecent"], outputs: ["valueChange", "open", "close", "cancel", "activeViewChange", "activeColorClick", "focusEvent", "blurEvent"] }] });
|
|
2830
3634
|
}
|
|
2831
3635
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisListConfigEditor, decorators: [{
|
|
2832
3636
|
type: Component,
|
|
@@ -2855,8 +3659,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2855
3659
|
PraxisMetaEditorDateComponent,
|
|
2856
3660
|
PraxisMetaEditorIconComponent,
|
|
2857
3661
|
PraxisMetaEditorImageComponent,
|
|
3662
|
+
PraxisListJsonConfigEditorComponent,
|
|
2858
3663
|
PdxColorPickerComponent,
|
|
2859
|
-
], template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab label=\"Dados\">\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">Observa\u00E7\u00E3o: ajustes aplicados pelo assistente substituem o objeto de configura\u00E7\u00E3o inteiro.\n </div>\n <button mat-icon-button type=\"button\" class=\"help-icon-button\"\n matTooltip=\"O applyConfigFromAdapter n\u00E3o faz merge profundo. Garanta que o adapter envie a config completa.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Recurso (API)</mat-label>\n <input matInput [(ngModel)]=\"working.dataSource.resourcePath\" (ngModelChange)=\"onResourcePathChange($event)\"\n placeholder=\"ex.: users\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Endpoint do recurso (resourcePath).\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Query (JSON)</mat-label>\n <textarea matInput rows=\"3\" [(ngModel)]=\"queryJson\" (ngModelChange)=\"onQueryChanged($event)\"\n placeholder='ex.: {\"status\":\"active\",\"department\":\"sales\"}'></textarea>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Opcional. Use JSON v\u00E1lido para filtros iniciais.\" *ngIf=\"!queryError\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"queryError\">{{ queryError }}</mat-error>\n </mat-form-field>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Ordenar por</mat-label>\n <mat-select [(ngModel)]=\"sortField\" (ngModelChange)=\"updateSortConfig()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"Campo base do recurso.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Dire\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"sortDir\" (ngModelChange)=\"updateSortConfig()\">\n <mat-option value=\"asc\">Ascendente</mat-option>\n <mat-option value=\"desc\">Descendente</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"A\u00E7\u00F5es\">\n <div class=\"editor-content g gap-12\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">Configure bot\u00F5es de a\u00E7\u00E3o por item (\u00EDcone, r\u00F3tulo, cor, visibilidade)</div>\n <button mat-flat-button color=\"primary\" (click)=\"addAction()\">Adicionar a\u00E7\u00E3o</button>\n </div>\n <div class=\"g g-1-auto gap-8 ai-center\">\n <mat-form-field appearance=\"outline\">\n <mat-label>A\u00E7\u00E3o global (Praxis)</mat-label>\n <mat-select [(ngModel)]=\"selectedGlobalActionId\" (ngModelChange)=\"onGlobalActionSelected($event)\">\n <mat-option [value]=\"undefined\">-- Selecionar --</mat-option>\n <mat-option *ngFor=\"let ga of globalActionCatalog\" [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || 'bolt' }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n </mat-select>\n <mat-hint *ngIf=\"!globalActionCatalog.length\" class=\"text-caption muted\">Nenhuma a\u00E7\u00E3o global registrada.</mat-hint>\n </mat-form-field>\n <div class=\"muted text-caption\">Selecione para adicionar com `command` global.</div>\n </div>\n <div *ngFor=\"let a of (working.actions || []); let i = index\" class=\"g g-auto-200 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>ID</mat-label>\n <input matInput [(ngModel)]=\"a.id\" (ngModelChange)=\"onActionsChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo de a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">\u00CDcone</mat-option>\n <mat-option value=\"button\">Bot\u00E3o</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input matInput [(ngModel)]=\"a.icon\" (ngModelChange)=\"onActionsChanged()\" placeholder=\"ex.: edit, delete\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Command (global)</mat-label>\n <input matInput [(ngModel)]=\"a.command\" (ngModelChange)=\"onActionsChanged()\" placeholder=\"global:toast.success\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input matInput [(ngModel)]=\"a.label\" (ngModelChange)=\"onActionsChanged()\" />\n </mat-form-field>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select [(ngModel)]=\"a.buttonVariant\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"stroked\">Contorno</mat-option>\n <mat-option value=\"raised\">Elevado</mat-option>\n <mat-option value=\"flat\">Preenchido</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>Cor da a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.color\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span class=\"color-dot\" [style.background]=\"colorDotBackground(c.value)\"></span>{{ c.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"g gap-8\" *ngIf=\"isCustomColor(a.color); else actionCustomBtn\">\n <pdx-color-picker label=\"Cor personalizada\" [format]=\"'hex'\" [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button mat-stroked-button type=\"button\" (click)=\"enableCustomActionColor(a)\">Usar cor personalizada</button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>Payload da a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.emitPayload\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option [value]=\"undefined\">Padr\u00E3o</mat-option>\n <mat-option value=\"item\">item</mat-option>\n <mat-option value=\"id\">id</mat-option>\n <mat-option value=\"value\">value</mat-option>\n </mat-select>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"emitPayload\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>Exibir quando (ex.: ${item.status} == 'done')</mat-label>\n <input matInput [(ngModel)]=\"a.showIf\" (ngModelChange)=\"onActionsChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Sintaxe suportada: "${item.campo} == 'valor'". Express\u00F5es avan\u00E7adas n\u00E3o s\u00E3o avaliadas.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button *ngIf=\"(a.kind || 'icon') === 'icon'\" mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"><mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\" [style.cssText]=\"iconStyle(a.color)\"></mat-icon></button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button *ngIf=\"a.buttonVariant === 'stroked'\" mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'stroked')\">{{ a.label\n || a.id || 'A\u00E7\u00E3o' }}</button>\n <button *ngIf=\"a.buttonVariant === 'raised'\" mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'raised')\">{{ a.label ||\n a.id || 'A\u00E7\u00E3o' }}</button>\n <button *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\" mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'flat')\">{{ a.label || a.id || 'A\u00E7\u00E3o' }}</button>\n </ng-container>\n <span class=\"muted\">Pr\u00E9-visualiza\u00E7\u00E3o</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">Remover</button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle [(ngModel)]=\"a.showLoading\" (ngModelChange)=\"onActionsChanged()\">Mostrar loading</mat-slide-toggle>\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Confirma\u00E7\u00E3o</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">Tipo</span>\n <mat-button-toggle-group [value]=\"a.confirmation?.type || ''\" (change)=\"applyConfirmationPreset(a, $event.value)\">\n <mat-button-toggle value=\"\">Padr\u00E3o</mat-button-toggle>\n <mat-button-toggle value=\"danger\">Danger</mat-button-toggle>\n <mat-button-toggle value=\"warning\">Warning</mat-button-toggle>\n <mat-button-toggle value=\"info\">Info</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>T\u00EDtulo</mat-label>\n <input matInput [ngModel]=\"a.confirmation?.title\" (ngModelChange)=\"setConfirmationField(a, 'title', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem</mat-label>\n <input matInput [ngModel]=\"a.confirmation?.message\" (ngModelChange)=\"setConfirmationField(a, 'message', $event)\" />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">Pr\u00E9via</div>\n <div class=\"text-caption\">\n <strong>{{ a.confirmation?.title || 'Confirmar a\u00E7\u00E3o' }}</strong>\n </div>\n <div class=\"text-caption muted\">{{ a.confirmation?.message || 'Tem certeza que deseja continuar?' }}</div>\n <div class=\"text-caption\">\n <span class=\"confirm-type\" [ngClass]=\"(a.confirmation?.type || 'default')\">Tipo: {{ a.confirmation?.type || 'padr\u00E3o' }}</span>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\">\n Defina um t\u00EDtulo ou mensagem para a confirma\u00E7\u00E3o.\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>Payload (JSON/Template)</mat-label>\n <textarea matInput rows=\"4\" [(ngModel)]=\"a.globalPayload\" (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'></textarea>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\">JSON inv\u00E1lido</mat-error>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button mat-stroked-button type=\"button\" (click)=\"applyGlobalPayloadExample(a)\">Inserir exemplo</button>\n <span class=\"muted text-caption\">{{ globalPayloadExampleHint(a) }}</span>\n </div>\n <mat-slide-toggle [(ngModel)]=\"a.emitLocal\" (ngModelChange)=\"onActionsChanged()\">Emitir evento local tamb\u00E9m</mat-slide-toggle>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Layout\">\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-stroked-button (click)=\"applyLayoutPreset('tiles-modern')\">Preset Tiles Moderno</button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select [(ngModel)]=\"working.layout.variant\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"list\">Lista</mat-option>\n <mat-option value=\"cards\">Cards</mat-option>\n <mat-option value=\"tiles\">Tiles</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Modelo</mat-label>\n <mat-select [(ngModel)]=\"working.layout.model\" (ngModelChange)=\"onLayoutChanged()\">\n <ng-container *ngIf=\"working.layout.variant === 'list'; else cardModels\">\n <mat-option value=\"standard\">Padr\u00E3o</mat-option>\n <mat-option value=\"media\">M\u00EDdia \u00E0 esquerda</mat-option>\n <mat-option value=\"hotel\">Hotel (m\u00EDdia grande)</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\">\n <mat-option value=\"standard\">Tile padr\u00E3o</mat-option>\n <mat-option value=\"media\">Tile com m\u00EDdia</mat-option>\n <mat-option value=\"hotel\">Tile hotel</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">Padr\u00E3o</mat-option>\n <mat-option value=\"media\">Card com m\u00EDdia</mat-option>\n <mat-option value=\"hotel\">Hotel</mat-option>\n </ng-template>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Linhas</mat-label>\n <mat-select [(ngModel)]=\"working.layout.lines\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option [value]=\"1\">1</mat-option>\n <mat-option [value]=\"2\">2</mat-option>\n <mat-option [value]=\"3\">3</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Itens por p\u00E1gina</mat-label>\n <input matInput type=\"number\" min=\"1\" [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Densidade</mat-label>\n <mat-select [(ngModel)]=\"working.layout.density\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"default\">Padr\u00E3o</mat-option>\n <mat-option value=\"comfortable\">Confort\u00E1vel</mat-option>\n <mat-option value=\"compact\">Compacta</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"working.layout.variant !== 'tiles'\">\n <mat-label>Divisores</mat-label>\n <mat-select [(ngModel)]=\"working.layout.dividers\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"none\">Sem</mat-option>\n <mat-option value=\"between\">Entre grupos</mat-option>\n <mat-option value=\"all\">Todos</mat-option>\n </mat-select>\n </mat-form-field>\n <ng-container *ngIf=\"fields.length > 0; else groupByText\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Agrupar por</mat-label>\n <mat-select [(ngModel)]=\"working.layout.groupBy\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option [value]=\"\">Nenhum</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <ng-template #groupByText>\n <mat-form-field appearance=\"outline\">\n <mat-label>Agrupar por</mat-label>\n <input matInput [(ngModel)]=\"working.layout.groupBy\" (ngModelChange)=\"onLayoutChanged()\"\n placeholder=\"ex.: departamento\" />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle [(ngModel)]=\"working.layout.stickySectionHeader\" (ngModelChange)=\"onLayoutChanged()\">\n Header de se\u00E7\u00E3o fixo\n </mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.layout.virtualScroll\" (ngModelChange)=\"onLayoutChanged()\">\n Scroll virtual\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Ferramentas da lista</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle [(ngModel)]=\"working.ui.showSearch\" (ngModelChange)=\"onUiChanged()\">Mostrar\n busca</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.ui.showSort\" (ngModelChange)=\"onUiChanged()\">Mostrar\n ordenar</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.ui.showRange\" (ngModelChange)=\"onUiChanged()\">Mostrar faixa X\u2013Y de\n Total</mat-slide-toggle>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\" *ngIf=\"working.ui?.showSearch\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo para buscar</mat-label>\n <mat-select [(ngModel)]=\"working.ui.searchField\" (ngModelChange)=\"onUiChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Placeholder da busca</mat-label>\n <input matInput [(ngModel)]=\"working.ui.searchPlaceholder\" (ngModelChange)=\"onUiChanged()\"\n placeholder=\"ex.: Buscar por t\u00EDtulo\" />\n </mat-form-field>\n </div>\n <div class=\"mt-12\" *ngIf=\"working.ui?.showSort\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">Op\u00E7\u00F5es de ordena\u00E7\u00E3o (r\u00F3tulo \u2192 campo+dire\u00E7\u00E3o)</div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">Adicionar op\u00E7\u00E3o</button>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\" *ngFor=\"let r of uiSortRows; let i = index\">\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input matInput [(ngModel)]=\"r.label\" (ngModelChange)=\"onUiSortRowsChanged()\"\n placeholder=\"ex.: Mais recentes\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"r.field\" (ngModelChange)=\"onUiSortRowsChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Dire\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"r.dir\" (ngModelChange)=\"onUiSortRowsChanged()\">\n <mat-option value=\"desc\">Descendente</mat-option>\n <mat-option value=\"asc\">Ascendente</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">Op\u00E7\u00E3o duplicada (campo+dire\u00E7\u00E3o)</div>\n <div class=\"flex-end\"><button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">Remover</button></div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Conte\u00FAdo\">\n <div class=\"editor-content\">\n <div class=\"editor-main\">\n <mat-accordion multi>\n <!-- Primary -->\n <mat-expansion-panel [expanded]=\"true\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingPrimary.type) }}</mat-icon>\n <span>Primary (T\u00EDtulo)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingPrimary.field || 'N\u00E3o mapeado' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='name'; onMappingChanged()\">Nome</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='title'; onMappingChanged()\">T\u00EDtulo</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='name'; mappingSecondary.type='text'; mappingSecondary.field='role'; onMappingChanged()\">Nome + Papel</button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingPrimary.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingPrimary.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of primaryTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingPrimary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingPrimary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingPrimary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingPrimary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>Formata\u00E7\u00E3o e Estilo</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\" *ngIf=\"mappingPrimary.type==='text' || mappingPrimary.type==='html'\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"mappingPrimary.type==='text' || mappingPrimary.type==='html'\">\n <mat-label>Estilo Inline</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo Inline</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Secondary -->\n <mat-expansion-panel [expanded]=\"!!mappingSecondary.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSecondary.type) }}</mat-icon>\n <span>Secondary (Resumo)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingSecondary.field || 'N\u00E3o mapeado' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='text'; mappingSecondary.field='subtitle'; onMappingChanged()\">Subt\u00EDtulo</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='date'; mappingSecondary.field='hireDate'; mappingSecondary.dateStyle='short'; onMappingChanged()\">Data curta</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='currency'; mappingSecondary.field='salary'; mappingSecondary.currencyCode='BRL'; mappingSecondary.locale='pt-BR'; onMappingChanged()\">Sal\u00E1rio</button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingSecondary.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingSecondary.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of secondaryTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingSecondary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingSecondary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingSecondary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingSecondary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>Formata\u00E7\u00E3o e Estilo</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe CSS</mat-label><input matInput\n [(ngModel)]=\"mappingSecondary.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Estilo Inline</mat-label><input matInput\n [(ngModel)]=\"mappingSecondary.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingMeta.type || 'text') }}</mat-icon>\n <span>Meta (Detalhe/Lateral)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{ mappingMetaFields.length ? 'Campo composto (' + mappingMetaFields.length + ')' :\n (mappingMeta.field || 'N\u00E3o mapeado') }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <!-- Composition Mode Toggle -->\n <div class=\"g g-1-1 gap-12 p-12 bg-subtle rounded\">\n <div class=\"text-caption muted\">Modo de composi\u00E7\u00E3o</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campos para compor (Multi-select)</mat-label>\n <mat-select [(ngModel)]=\"mappingMetaFields\" multiple (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"g g-1-1 ai-center gap-12\" *ngIf=\"mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Separador</mat-label>\n <input matInput [(ngModel)]=\"mappingMetaSeparator\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-slide-toggle [(ngModel)]=\"mappingMetaWrapSecondInParens\" (ngModelChange)=\"onMappingChanged()\">\n (Seg) entre par\u00EAnteses\n </mat-slide-toggle>\n </div>\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"!mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo \u00DAnico</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of metaTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Type configuration (pluggable editors) -->\n @switch (mappingMeta.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingMeta\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingMeta\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') { <praxis-meta-editor-image [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image> }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Op\u00E7\u00F5es\n avan\u00E7adas</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Posi\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.placement\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option value=\"side\">Lateral (Direita)</mat-option>\n <mat-option value=\"line\">Na linha (Abaixo)</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingMeta.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <input matInput [(ngModel)]=\"mappingMeta.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n <!-- Trailing -->\n <mat-expansion-panel [expanded]=\"!!mappingTrailing.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingTrailing.type || 'text') }}</mat-icon>\n <span>Trailing (Direita)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingTrailing.field || 'N\u00E3o mapeado'\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingTrailing.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingTrailing.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of trailingTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='chip'; mappingTrailing.chipColor='primary'; mappingTrailing.chipVariant='filled'; mappingTrailing.field='status'; onMappingChanged()\">Status Chip</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='icon'; mappingTrailing.field='status'; mappingTrailing.iconColor='primary'; onMappingChanged()\">Status \u00CDcone</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='currency'; mappingTrailing.field='price'; mappingTrailing.currencyCode='BRL'; mappingTrailing.locale='pt-BR'; onMappingChanged()\">Pre\u00E7o</button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingTrailing\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingTrailing\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL / Expr</mat-label>\n <input matInput [(ngModel)]=\"mappingTrailing.imageUrl\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <praxis-meta-editor-image [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n <div class=\"text-caption muted\" *ngIf=\"!mappingTrailing.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingTrailing.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingTrailing.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Leading -->\n <mat-expansion-panel\n [expanded]=\"!!mappingLeading.field || (mappingLeading.type === 'icon' && !!mappingLeading.icon) || (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingLeading.type) }}</mat-icon>\n <span>Leading (Esquerda)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{ mappingLeading.type === 'icon' ? (mappingLeading.icon || '\u00CDcone est\u00E1tico') :\n (mappingLeading.field || (mappingLeading.imageUrl ? 'Imagem est\u00E1tica' : 'N\u00E3o mapeado'))\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingLeading.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of leadingTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n <mat-form-field appearance=\"outline\"\n *ngIf=\"mappingLeading.type !== 'icon' && mappingLeading.type !== 'image'\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingLeading.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='icon'; mappingLeading.icon='person'; mappingLeading.iconColor='primary'; onMappingChanged()\">Avatar \u00CDcone</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='image'; mappingLeading.imageUrl='https://placehold.co/64x64'; mappingLeading.imageAlt='Avatar'; mappingLeading.badgeText='${item.status}'; onMappingChanged()\">Avatar Imagem + Badge</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='chip'; mappingLeading.field='tag'; mappingLeading.chipColor='accent'; mappingLeading.chipVariant='filled'; onMappingChanged()\">Chip Tag</button>\n </div>\n\n <!-- Icon Specific -->\n <div class=\"g g-1-auto gap-12 ai-center\" *ngIf=\"mappingLeading.type === 'icon'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.icon\" (ngModelChange)=\"onMappingChanged()\" />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\"><mat-icon>search</mat-icon></button>\n </mat-form-field>\n <div class=\"text-caption muted\">Use pipe <code>|iconMap</code> no extra pipe para\n din\u00E2mico</div>\n </div>\n <div *ngIf=\"mappingLeading.type === 'icon'\">\n <praxis-meta-editor-icon\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"mappingLeading.type === 'image'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL da Imagem</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.imageUrl\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Alt Text</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.imageAlt\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Badge Texto</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.badgeText\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingLeading\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingLeading\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingLeading.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingLeading.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Features -->\n <mat-expansion-panel [expanded]=\"featuresVisible && features.length > 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>view_list</mat-icon>\n <span>Recursos (Features)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ features.length }} item(s)</mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-12 ai-center\">\n <mat-slide-toggle [(ngModel)]=\"featuresVisible\" (ngModelChange)=\"onFeaturesChanged()\">Ativar\n recursos</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"featuresSyncWithMeta\" (ngModelChange)=\"onMappingChanged()\">Sincronizar\n com Meta</mat-slide-toggle>\n <span class=\"flex-1\"></span>\n <mat-button-toggle-group [(ngModel)]=\"featuresMode\" (change)=\"onFeaturesChanged()\" appearance=\"legacy\">\n <mat-button-toggle value=\"icons+labels\"><mat-icon>view_list</mat-icon></mat-button-toggle>\n <mat-button-toggle value=\"icons-only\"><mat-icon>more_horiz</mat-icon></mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n\n <div *ngFor=\"let f of features; let i = index\" class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\">\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\"><mat-icon>{{ f.icon || 'search'\n }}</mat-icon></button>\n <mat-form-field appearance=\"outline\" class=\"dense-form-field no-sub\">\n <input matInput [(ngModel)]=\"f.expr\" (ngModelChange)=\"onFeaturesChanged()\" placeholder=\"Expr/Texto\" />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\"><mat-icon>delete</mat-icon></button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\"><mat-icon>add</mat-icon>\n Adicionar recurso</button>\n </div>\n </mat-expansion-panel>\n <!-- Section Header -->\n <mat-expansion-panel [expanded]=\"!!mappingSectionHeader.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSectionHeader.type) }}</mat-icon>\n <span>Cabe\u00E7alho de Se\u00E7\u00E3o</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingSectionHeader.expr || 'N\u00E3o configurado'\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingSectionHeader.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of sectionHeaderTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Express\u00E3o (item.key)</mat-label>\n <input matInput [(ngModel)]=\"mappingSectionHeader.expr\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\" />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSectionHeader.type='text'; mappingSectionHeader.expr='${item.key}'; onMappingChanged()\">Texto padr\u00E3o</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSectionHeader.type='chip'; mappingSectionHeader.chipColor='primary'; mappingSectionHeader.chipVariant='filled'; mappingSectionHeader.expr='${item.key}'; onMappingChanged()\">Chip padr\u00E3o</button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingSectionHeader\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingSectionHeader\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL Imagem</mat-label>\n <input matInput [(ngModel)]=\"mappingSectionHeader.imageUrl\" (ngModelChange)=\"onMappingChanged()\" />\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingSectionHeader.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!mappingSectionHeader.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n <praxis-meta-editor-image [model]=\"mappingSectionHeader\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingSectionHeader.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingSectionHeader.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Empty State -->\n <mat-expansion-panel [expanded]=\"!!mappingEmptyState.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>inbox</mat-icon>\n <span>Estado Vazio</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingEmptyState.expr || 'Padr\u00E3o' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingEmptyState.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of emptyStateTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem / Expr</mat-label>\n <input matInput [(ngModel)]=\"mappingEmptyState.expr\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingEmptyState.type='text'; mappingEmptyState.expr='Nenhum item dispon\u00EDvel'; onMappingChanged()\">Mensagem padr\u00E3o</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingEmptyState.type='image'; mappingEmptyState.imageUrl='/list-empty-state.svg'; mappingEmptyState.imageAlt='Sem resultados'; onMappingChanged()\">Imagem padr\u00E3o</button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingEmptyState\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingEmptyState\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>URL Imagem</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.imageUrl\" (ngModelChange)=\"onMappingChanged()\" />\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!mappingEmptyState.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n <praxis-meta-editor-image [model]=\"mappingEmptyState\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n </mat-accordion>\n\n <button mat-flat-button color=\"primary\" (click)=\"applyTemplate()\">Aplicar mapeamento</button>\n <button mat-button (click)=\"inferFromFields()\" [disabled]=\"!fields.length\">Inferir do schema</button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Skeleton (quantidade)</mat-label>\n <input matInput type=\"number\" min=\"0\" [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\" />\n </mat-form-field>\n </div>\n\n <div class=\"g gap-12 mt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"section-title mat-subtitle-1\">Pr\u00E9via de tema</span>\n <mat-button-toggle-group [(ngModel)]=\"skinPreviewTheme\" (change)=\"onSkinChanged()\" appearance=\"legacy\">\n <mat-button-toggle [value]=\"'light'\">Claro</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">Escuro</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">Grade</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview [config]=\"working\" [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n\n </mat-tab>\n <mat-tab label=\"i18n/A11y\">\n <div class=\"editor-content grid gap-3\" *ngIf=\"working?.a11y && working?.events\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Locale padr\u00E3o</mat-label>\n <input matInput [(ngModel)]=\"working.i18n.locale\" (ngModelChange)=\"markDirty()\" placeholder=\"ex.: pt-BR\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Moeda padr\u00E3o</mat-label>\n <input matInput [(ngModel)]=\"working.i18n.currency\" (ngModelChange)=\"markDirty()\" placeholder=\"ex.: BRL\" />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Acessibilidade</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-label</mat-label>\n <input matInput [(ngModel)]=\"working!.a11y!.ariaLabel\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-labelledby</mat-label>\n <input matInput [(ngModel)]=\"working!.a11y!.ariaLabelledBy\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle [(ngModel)]=\"working!.a11y!.highContrast\" (ngModelChange)=\"markDirty()\">Alto\n contraste</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working!.a11y!.reduceMotion\" (ngModelChange)=\"markDirty()\">Reduzir\n movimento</mat-slide-toggle>\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Eventos</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>itemClick</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.itemClick\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.actionClick\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.selectionChange\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.loaded\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Sele\u00E7\u00E3o\">\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Modo</mat-label>\n <mat-select [(ngModel)]=\"working.selection.mode\" (ngModelChange)=\"onSelectionChanged()\">\n <mat-option value=\"none\">Sem sele\u00E7\u00E3o</mat-option>\n <mat-option value=\"single\">\u00DAnica</mat-option>\n <mat-option value=\"multiple\">M\u00FAltipla</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Nome no formul\u00E1rio</mat-label>\n <input matInput [(ngModel)]=\"working.selection.formControlName\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"formControlName\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Caminho no formul\u00E1rio</mat-label>\n <input matInput [(ngModel)]=\"working.selection.formControlPath\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"formControlPath\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Comparar por (campo)</mat-label>\n <input matInput [(ngModel)]=\"working.selection.compareBy\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"Chave unica do item.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Retorno</mat-label>\n <mat-select [(ngModel)]=\"working.selection.return\" (ngModelChange)=\"onSelectionChanged()\">\n <mat-option value=\"value\">value</mat-option>\n <mat-option value=\"item\">item</mat-option>\n <mat-option value=\"id\">id</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab label=\"Apar\u00EAncia\">\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-button (click)=\"applySkinPreset('pill-soft')\">Pill Soft</button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">Gradient Tile</button>\n <button mat-button (click)=\"applySkinPreset('glass')\">Glass</button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">Elevated</button>\n <button mat-button (click)=\"applySkinPreset('outline')\">Outline</button>\n <button mat-button (click)=\"applySkinPreset('flat')\">Flat</button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">Neumorphism</button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <mat-select [(ngModel)]=\"working.skin.type\" (ngModelChange)=\"onSkinTypeChanged($event)\">\n <mat-option value=\"pill-soft\">Pill Soft</mat-option>\n <mat-option value=\"gradient-tile\">Gradient Tile</mat-option>\n <mat-option value=\"glass\">Glass</mat-option>\n <mat-option value=\"elevated\">Elevated</mat-option>\n <mat-option value=\"outline\">Outline</mat-option>\n <mat-option value=\"flat\">Flat</mat-option>\n <mat-option value=\"neumorphism\">Neumorphism</mat-option>\n <mat-option value=\"custom\">Custom</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Raio</mat-label>\n <input matInput [(ngModel)]=\"working.skin.radius\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 1.25rem\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Sombra</mat-label>\n <input matInput [(ngModel)]=\"working.skin.shadow\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: var(--md-sys-elevation-level2)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Borda</mat-label>\n <input matInput [(ngModel)]=\"working.skin.border\" (ngModelChange)=\"onSkinChanged()\" />\n </mat-form-field>\n <mat-form-field *ngIf=\"working.skin.type==='glass'\" appearance=\"outline\">\n <mat-label>Desfoque</mat-label>\n <input matInput [(ngModel)]=\"working.skin.backdropBlur\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 8px\" />\n </mat-form-field>\n <div *ngIf=\"working.skin.type==='gradient-tile'\" class=\"g gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA de</mat-label>\n <input matInput [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA at\u00E9</mat-label>\n <input matInput [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00C2ngulo</mat-label>\n <input matInput type=\"number\" [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\" />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS extra (skin.class)</mat-label>\n <input matInput [(ngModel)]=\"working.skin.class\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: my-list-skin\" />\n </mat-form-field>\n\n <div *ngIf=\"working.skin.type==='custom'\" class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Estilo inline (skin.inlineStyle)</mat-label>\n <textarea matInput rows=\"4\" [(ngModel)]=\"working.skin.inlineStyle\" (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n Exemplo de CSS por classe (adicione no seu styles global):\n <pre class=\"code-block\">.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre>\n </div>\n </div>\n\n\n </div>\n </mat-tab>\n</mat-tab-group>\n", styles: [".confirm-type{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;line-height:16px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.confirm-type.danger{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.confirm-type.warning{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.confirm-type.info{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}:host{display:block;color:var(--md-sys-color-on-surface)}.list-editor-tabs{--editor-surface: var(--md-sys-color-surface-container-lowest);--editor-border: 1px solid var(--md-sys-color-outline-variant);--editor-radius: var(--md-sys-shape-corner-large, 16px);--editor-muted: var(--md-sys-color-on-surface-variant);--editor-accent: var(--md-sys-color-primary)}.editor-content{padding:16px;background:var(--editor-surface);border:var(--editor-border);border-radius:var(--editor-radius);display:grid;gap:12px}.editor-content .mat-mdc-form-field{width:100%;max-width:none;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.editor-content .mat-mdc-form-field.w-full{max-width:none}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.editor-split{grid-template-columns:minmax(0,1fr);align-items:start}.editor-main,.editor-aside{display:grid;gap:12px}.skin-preview-wrap{border-radius:calc(var(--editor-radius) - 4px);border:var(--editor-border);background:var(--md-sys-color-surface-container);padding:12px}.g{display:grid}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.g-auto-200{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.g-1-auto{grid-template-columns:1fr auto}.row-flow{grid-auto-flow:column}.gap-6{gap:6px}.gap-8{gap:8px}.gap-12{gap:12px}.ai-center{align-items:center}.ai-end{align-items:end}.mt-12{margin-top:12px}.mb-8{margin-bottom:8px}.mb-6{margin-bottom:6px}.my-8{margin:8px 0}.subtitle{margin:8px 0 4px;color:var(--editor-muted);font-weight:500}.section-title{color:var(--editor-muted);font-weight:600}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--md-sys-color-error);font-size:.85rem}.muted{color:var(--editor-muted)}.text-caption{color:var(--editor-muted);font-size:.8rem}:host ::ng-deep .mat-mdc-select-panel .option-icon{font-size:18px;margin-right:6px;vertical-align:middle}:host ::ng-deep .mat-mdc-select-panel .color-dot{width:10px;height:10px;border-radius:999px;display:inline-block;margin-right:6px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-outline)}:host ::ng-deep .mat-mdc-select-panel .color-primary{background:var(--md-sys-color-primary)}:host ::ng-deep .mat-mdc-select-panel .color-accent{background:var(--md-sys-color-tertiary)}:host ::ng-deep .mat-mdc-select-panel .color-warn{background:var(--md-sys-color-error)}:host ::ng-deep .mat-mdc-select-panel .color-default{background:var(--md-sys-color-outline)}@media(max-width:1024px){.editor-split{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
3664
|
+
], template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab label=\"Dados\">\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">Observa\u00E7\u00E3o: ajustes aplicados pelo assistente substituem o objeto de configura\u00E7\u00E3o inteiro.\n </div>\n <button mat-icon-button type=\"button\" class=\"help-icon-button\"\n matTooltip=\"O applyConfigFromAdapter n\u00E3o faz merge profundo. Garanta que o adapter envie a config completa.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Recurso (API)</mat-label>\n <input matInput [(ngModel)]=\"working.dataSource.resourcePath\" (ngModelChange)=\"onResourcePathChange($event)\"\n placeholder=\"ex.: users\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Endpoint do recurso (resourcePath).\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Query (JSON)</mat-label>\n <textarea matInput rows=\"3\" [(ngModel)]=\"queryJson\" (ngModelChange)=\"onQueryChanged($event)\"\n placeholder='ex.: {\"status\":\"active\",\"department\":\"sales\"}'></textarea>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Opcional. Use JSON v\u00E1lido para filtros iniciais.\" *ngIf=\"!queryError\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"queryError\">{{ queryError }}</mat-error>\n </mat-form-field>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Ordenar por</mat-label>\n <mat-select [(ngModel)]=\"sortField\" (ngModelChange)=\"updateSortConfig()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"Campo base do recurso.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Dire\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"sortDir\" (ngModelChange)=\"updateSortConfig()\">\n <mat-option value=\"asc\">Ascendente</mat-option>\n <mat-option value=\"desc\">Descendente</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"JSON\">\n <div class=\"editor-content\">\n <praxis-list-json-config-editor\n [document]=\"document\"\n (documentChange)=\"onJsonConfigChange($event)\"\n (validationChange)=\"onJsonValidationChange($event)\"\n (editorEvent)=\"onJsonEditorEvent($event)\">\n </praxis-list-json-config-editor>\n </div>\n </mat-tab>\n <mat-tab label=\"A\u00E7\u00F5es\">\n <div class=\"editor-content g gap-12\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">Configure bot\u00F5es de a\u00E7\u00E3o por item (\u00EDcone, r\u00F3tulo, cor, visibilidade)</div>\n <button mat-flat-button color=\"primary\" (click)=\"addAction()\">Adicionar a\u00E7\u00E3o</button>\n </div>\n <div class=\"g g-1-auto gap-8 ai-center\">\n <mat-form-field appearance=\"outline\">\n <mat-label>A\u00E7\u00E3o global (Praxis)</mat-label>\n <mat-select [(ngModel)]=\"selectedGlobalActionId\" (ngModelChange)=\"onGlobalActionSelected($event)\">\n <mat-option [value]=\"undefined\">-- Selecionar --</mat-option>\n <mat-option *ngFor=\"let ga of globalActionCatalog\" [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || 'bolt' }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n </mat-select>\n <mat-hint *ngIf=\"!globalActionCatalog.length\" class=\"text-caption muted\">Nenhuma a\u00E7\u00E3o global registrada.</mat-hint>\n </mat-form-field>\n <div class=\"muted text-caption\">Selecione para adicionar com `command` global.</div>\n </div>\n <div *ngFor=\"let a of (working.actions || []); let i = index\" class=\"g g-auto-200 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>ID</mat-label>\n <input matInput [(ngModel)]=\"a.id\" (ngModelChange)=\"onActionsChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo de a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">\u00CDcone</mat-option>\n <mat-option value=\"button\">Bot\u00E3o</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input matInput [(ngModel)]=\"a.icon\" (ngModelChange)=\"onActionsChanged()\" placeholder=\"ex.: edit, delete\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Command (global)</mat-label>\n <input matInput [(ngModel)]=\"a.command\" (ngModelChange)=\"onActionsChanged()\" placeholder=\"global:toast.success\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input matInput [(ngModel)]=\"a.label\" (ngModelChange)=\"onActionsChanged()\" />\n </mat-form-field>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select [(ngModel)]=\"a.buttonVariant\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"stroked\">Contorno</mat-option>\n <mat-option value=\"raised\">Elevado</mat-option>\n <mat-option value=\"flat\">Preenchido</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>Cor da a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.color\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span class=\"color-dot\" [style.background]=\"colorDotBackground(c.value)\"></span>{{ c.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"g gap-8\" *ngIf=\"isCustomColor(a.color); else actionCustomBtn\">\n <pdx-color-picker label=\"Cor personalizada\" [format]=\"'hex'\" [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button mat-stroked-button type=\"button\" (click)=\"enableCustomActionColor(a)\">Usar cor personalizada</button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>Payload da a\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"a.emitPayload\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option [value]=\"undefined\">Padr\u00E3o</mat-option>\n <mat-option value=\"item\">item</mat-option>\n <mat-option value=\"id\">id</mat-option>\n <mat-option value=\"value\">value</mat-option>\n </mat-select>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"emitPayload\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>Exibir quando (ex.: ${item.status} == 'done')</mat-label>\n <input matInput [(ngModel)]=\"a.showIf\" (ngModelChange)=\"onActionsChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Sintaxe suportada: "${item.campo} == 'valor'". Express\u00F5es avan\u00E7adas n\u00E3o s\u00E3o avaliadas.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button *ngIf=\"(a.kind || 'icon') === 'icon'\" mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"><mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\" [style.cssText]=\"iconStyle(a.color)\"></mat-icon></button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button *ngIf=\"a.buttonVariant === 'stroked'\" mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'stroked')\">{{ a.label\n || a.id || 'A\u00E7\u00E3o' }}</button>\n <button *ngIf=\"a.buttonVariant === 'raised'\" mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'raised')\">{{ a.label ||\n a.id || 'A\u00E7\u00E3o' }}</button>\n <button *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\" mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\" [style.cssText]=\"buttonStyle(a.color, 'flat')\">{{ a.label || a.id || 'A\u00E7\u00E3o' }}</button>\n </ng-container>\n <span class=\"muted\">Pr\u00E9-visualiza\u00E7\u00E3o</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">Remover</button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle [(ngModel)]=\"a.showLoading\" (ngModelChange)=\"onActionsChanged()\">Mostrar loading</mat-slide-toggle>\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Confirma\u00E7\u00E3o</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">Tipo</span>\n <mat-button-toggle-group [value]=\"a.confirmation?.type || ''\" (change)=\"applyConfirmationPreset(a, $event.value)\">\n <mat-button-toggle value=\"\">Padr\u00E3o</mat-button-toggle>\n <mat-button-toggle value=\"danger\">Danger</mat-button-toggle>\n <mat-button-toggle value=\"warning\">Warning</mat-button-toggle>\n <mat-button-toggle value=\"info\">Info</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>T\u00EDtulo</mat-label>\n <input matInput [ngModel]=\"a.confirmation?.title\" (ngModelChange)=\"setConfirmationField(a, 'title', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem</mat-label>\n <input matInput [ngModel]=\"a.confirmation?.message\" (ngModelChange)=\"setConfirmationField(a, 'message', $event)\" />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">Pr\u00E9via</div>\n <div class=\"text-caption\">\n <strong>{{ a.confirmation?.title || 'Confirmar a\u00E7\u00E3o' }}</strong>\n </div>\n <div class=\"text-caption muted\">{{ a.confirmation?.message || 'Tem certeza que deseja continuar?' }}</div>\n <div class=\"text-caption\">\n <span class=\"confirm-type\" [ngClass]=\"(a.confirmation?.type || 'default')\">Tipo: {{ a.confirmation?.type || 'padr\u00E3o' }}</span>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\">\n Defina um t\u00EDtulo ou mensagem para a confirma\u00E7\u00E3o.\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>Payload (JSON/Template)</mat-label>\n <textarea matInput rows=\"4\" [(ngModel)]=\"a.globalPayload\" (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'></textarea>\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\">JSON inv\u00E1lido</mat-error>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button mat-stroked-button type=\"button\" (click)=\"applyGlobalPayloadExample(a)\">Inserir exemplo</button>\n <span class=\"muted text-caption\">{{ globalPayloadExampleHint(a) }}</span>\n </div>\n <mat-slide-toggle [(ngModel)]=\"a.emitLocal\" (ngModelChange)=\"onActionsChanged()\">Emitir evento local tamb\u00E9m</mat-slide-toggle>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Layout\">\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-stroked-button (click)=\"applyLayoutPreset('tiles-modern')\">Preset Tiles Moderno</button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select [(ngModel)]=\"working.layout.variant\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"list\">Lista</mat-option>\n <mat-option value=\"cards\">Cards</mat-option>\n <mat-option value=\"tiles\">Tiles</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Modelo</mat-label>\n <mat-select [(ngModel)]=\"working.layout.model\" (ngModelChange)=\"onLayoutChanged()\">\n <ng-container *ngIf=\"working.layout.variant === 'list'; else cardModels\">\n <mat-option value=\"standard\">Padr\u00E3o</mat-option>\n <mat-option value=\"media\">M\u00EDdia \u00E0 esquerda</mat-option>\n <mat-option value=\"hotel\">Hotel (m\u00EDdia grande)</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\">\n <mat-option value=\"standard\">Tile padr\u00E3o</mat-option>\n <mat-option value=\"media\">Tile com m\u00EDdia</mat-option>\n <mat-option value=\"hotel\">Tile hotel</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">Padr\u00E3o</mat-option>\n <mat-option value=\"media\">Card com m\u00EDdia</mat-option>\n <mat-option value=\"hotel\">Hotel</mat-option>\n </ng-template>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Linhas</mat-label>\n <mat-select [(ngModel)]=\"working.layout.lines\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option [value]=\"1\">1</mat-option>\n <mat-option [value]=\"2\">2</mat-option>\n <mat-option [value]=\"3\">3</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Itens por p\u00E1gina</mat-label>\n <input matInput type=\"number\" min=\"1\" [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Densidade</mat-label>\n <mat-select [(ngModel)]=\"working.layout.density\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"default\">Padr\u00E3o</mat-option>\n <mat-option value=\"comfortable\">Confort\u00E1vel</mat-option>\n <mat-option value=\"compact\">Compacta</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"working.layout.variant !== 'tiles'\">\n <mat-label>Divisores</mat-label>\n <mat-select [(ngModel)]=\"working.layout.dividers\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option value=\"none\">Sem</mat-option>\n <mat-option value=\"between\">Entre grupos</mat-option>\n <mat-option value=\"all\">Todos</mat-option>\n </mat-select>\n </mat-form-field>\n <ng-container *ngIf=\"fields.length > 0; else groupByText\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Agrupar por</mat-label>\n <mat-select [(ngModel)]=\"working.layout.groupBy\" (ngModelChange)=\"onLayoutChanged()\">\n <mat-option [value]=\"\">Nenhum</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <ng-template #groupByText>\n <mat-form-field appearance=\"outline\">\n <mat-label>Agrupar por</mat-label>\n <input matInput [(ngModel)]=\"working.layout.groupBy\" (ngModelChange)=\"onLayoutChanged()\"\n placeholder=\"ex.: departamento\" />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle [(ngModel)]=\"working.layout.stickySectionHeader\" (ngModelChange)=\"onLayoutChanged()\">\n Header de se\u00E7\u00E3o fixo\n </mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.layout.virtualScroll\" (ngModelChange)=\"onLayoutChanged()\">\n Scroll virtual\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Ferramentas da lista</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle [(ngModel)]=\"working.ui.showSearch\" (ngModelChange)=\"onUiChanged()\">Mostrar\n busca</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.ui.showSort\" (ngModelChange)=\"onUiChanged()\">Mostrar\n ordenar</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working.ui.showRange\" (ngModelChange)=\"onUiChanged()\">Mostrar faixa X\u2013Y de\n Total</mat-slide-toggle>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\" *ngIf=\"working.ui?.showSearch\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo para buscar</mat-label>\n <mat-select [(ngModel)]=\"working.ui.searchField\" (ngModelChange)=\"onUiChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Placeholder da busca</mat-label>\n <input matInput [(ngModel)]=\"working.ui.searchPlaceholder\" (ngModelChange)=\"onUiChanged()\"\n placeholder=\"ex.: Buscar por t\u00EDtulo\" />\n </mat-form-field>\n </div>\n <div class=\"mt-12\" *ngIf=\"working.ui?.showSort\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">Op\u00E7\u00F5es de ordena\u00E7\u00E3o (r\u00F3tulo \u2192 campo+dire\u00E7\u00E3o)</div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">Adicionar op\u00E7\u00E3o</button>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\" *ngFor=\"let r of uiSortRows; let i = index\">\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input matInput [(ngModel)]=\"r.label\" (ngModelChange)=\"onUiSortRowsChanged()\"\n placeholder=\"ex.: Mais recentes\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"r.field\" (ngModelChange)=\"onUiSortRowsChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Dire\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"r.dir\" (ngModelChange)=\"onUiSortRowsChanged()\">\n <mat-option value=\"desc\">Descendente</mat-option>\n <mat-option value=\"asc\">Ascendente</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">Op\u00E7\u00E3o duplicada (campo+dire\u00E7\u00E3o)</div>\n <div class=\"flex-end\"><button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">Remover</button></div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Conte\u00FAdo\">\n <div class=\"editor-content\">\n <div class=\"editor-main\">\n <mat-accordion multi>\n <!-- Primary -->\n <mat-expansion-panel [expanded]=\"true\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingPrimary.type) }}</mat-icon>\n <span>Primary (T\u00EDtulo)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingPrimary.field || 'N\u00E3o mapeado' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='name'; onMappingChanged()\">Nome</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='title'; onMappingChanged()\">T\u00EDtulo</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingPrimary.type='text'; mappingPrimary.field='name'; mappingSecondary.type='text'; mappingSecondary.field='role'; onMappingChanged()\">Nome + Papel</button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingPrimary.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingPrimary.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of primaryTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingPrimary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingPrimary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingPrimary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingPrimary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>Formata\u00E7\u00E3o e Estilo</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\" *ngIf=\"mappingPrimary.type==='text' || mappingPrimary.type==='html'\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" *ngIf=\"mappingPrimary.type==='text' || mappingPrimary.type==='html'\">\n <mat-label>Estilo Inline</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo Inline</mat-label>\n <input matInput [(ngModel)]=\"mappingPrimary.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Secondary -->\n <mat-expansion-panel [expanded]=\"!!mappingSecondary.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSecondary.type) }}</mat-icon>\n <span>Secondary (Resumo)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingSecondary.field || 'N\u00E3o mapeado' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='text'; mappingSecondary.field='subtitle'; onMappingChanged()\">Subt\u00EDtulo</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='date'; mappingSecondary.field='hireDate'; mappingSecondary.dateStyle='short'; onMappingChanged()\">Data curta</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSecondary.type='currency'; mappingSecondary.field='salary'; mappingSecondary.currencyCode='BRL'; mappingSecondary.locale='pt-BR'; onMappingChanged()\">Sal\u00E1rio</button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingSecondary.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingSecondary.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of secondaryTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingSecondary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingSecondary\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingSecondary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingSecondary\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>Formata\u00E7\u00E3o e Estilo</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe CSS</mat-label><input matInput\n [(ngModel)]=\"mappingSecondary.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Estilo Inline</mat-label><input matInput\n [(ngModel)]=\"mappingSecondary.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingMeta.type || 'text') }}</mat-icon>\n <span>Meta (Detalhe/Lateral)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{ mappingMetaFields.length ? 'Campo composto (' + mappingMetaFields.length + ')' :\n (mappingMeta.field || 'N\u00E3o mapeado') }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <!-- Composition Mode Toggle -->\n <div class=\"g g-1-1 gap-12 p-12 bg-subtle rounded\">\n <div class=\"text-caption muted\">Modo de composi\u00E7\u00E3o</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campos para compor (Multi-select)</mat-label>\n <mat-select [(ngModel)]=\"mappingMetaFields\" multiple (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"g g-1-1 ai-center gap-12\" *ngIf=\"mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Separador</mat-label>\n <input matInput [(ngModel)]=\"mappingMetaSeparator\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-slide-toggle [(ngModel)]=\"mappingMetaWrapSecondInParens\" (ngModelChange)=\"onMappingChanged()\">\n (Seg) entre par\u00EAnteses\n </mat-slide-toggle>\n </div>\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"!mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo \u00DAnico</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of metaTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Type configuration (pluggable editors) -->\n @switch (mappingMeta.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingMeta\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingMeta\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') { <praxis-meta-editor-image [model]=\"mappingMeta\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image> }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Op\u00E7\u00F5es\n avan\u00E7adas</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Posi\u00E7\u00E3o</mat-label>\n <mat-select [(ngModel)]=\"mappingMeta.placement\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option value=\"side\">Lateral (Direita)</mat-option>\n <mat-option value=\"line\">Na linha (Abaixo)</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS</mat-label>\n <input matInput [(ngModel)]=\"mappingMeta.class\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <input matInput [(ngModel)]=\"mappingMeta.style\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n <!-- Trailing -->\n <mat-expansion-panel [expanded]=\"!!mappingTrailing.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingTrailing.type || 'text') }}</mat-icon>\n <span>Trailing (Direita)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingTrailing.field || 'N\u00E3o mapeado'\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingTrailing.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingTrailing.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of trailingTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='chip'; mappingTrailing.chipColor='primary'; mappingTrailing.chipVariant='filled'; mappingTrailing.field='status'; onMappingChanged()\">Status Chip</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='icon'; mappingTrailing.field='status'; mappingTrailing.iconColor='primary'; onMappingChanged()\">Status \u00CDcone</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingTrailing.type='currency'; mappingTrailing.field='price'; mappingTrailing.currencyCode='BRL'; mappingTrailing.locale='pt-BR'; onMappingChanged()\">Pre\u00E7o</button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingTrailing\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingTrailing\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('currency') { <praxis-meta-editor-currency [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-currency> }\n @case ('date') { <praxis-meta-editor-date [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-date> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL / Expr</mat-label>\n <input matInput [(ngModel)]=\"mappingTrailing.imageUrl\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <praxis-meta-editor-image [model]=\"mappingTrailing\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n <div class=\"text-caption muted\" *ngIf=\"!mappingTrailing.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingTrailing.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingTrailing.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Leading -->\n <mat-expansion-panel\n [expanded]=\"!!mappingLeading.field || (mappingLeading.type === 'icon' && !!mappingLeading.icon) || (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingLeading.type) }}</mat-icon>\n <span>Leading (Esquerda)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{ mappingLeading.type === 'icon' ? (mappingLeading.icon || '\u00CDcone est\u00E1tico') :\n (mappingLeading.field || (mappingLeading.imageUrl ? 'Imagem est\u00E1tica' : 'N\u00E3o mapeado'))\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingLeading.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of leadingTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n <mat-form-field appearance=\"outline\"\n *ngIf=\"mappingLeading.type !== 'icon' && mappingLeading.type !== 'image'\">\n <mat-label>Campo</mat-label>\n <mat-select [(ngModel)]=\"mappingLeading.field\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{ f }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='icon'; mappingLeading.icon='person'; mappingLeading.iconColor='primary'; onMappingChanged()\">Avatar \u00CDcone</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='image'; mappingLeading.imageUrl='https://placehold.co/64x64'; mappingLeading.imageAlt='Avatar'; mappingLeading.badgeText='${item.status}'; onMappingChanged()\">Avatar Imagem + Badge</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingLeading.type='chip'; mappingLeading.field='tag'; mappingLeading.chipColor='accent'; mappingLeading.chipVariant='filled'; onMappingChanged()\">Chip Tag</button>\n </div>\n\n <!-- Icon Specific -->\n <div class=\"g g-1-auto gap-12 ai-center\" *ngIf=\"mappingLeading.type === 'icon'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.icon\" (ngModelChange)=\"onMappingChanged()\" />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\"><mat-icon>search</mat-icon></button>\n </mat-form-field>\n <div class=\"text-caption muted\">Use pipe <code>|iconMap</code> no extra pipe para\n din\u00E2mico</div>\n </div>\n <div *ngIf=\"mappingLeading.type === 'icon'\">\n <praxis-meta-editor-icon\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"mappingLeading.type === 'image'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL da Imagem</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.imageUrl\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Alt Text</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.imageAlt\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Badge Texto</mat-label>\n <input matInput [(ngModel)]=\"mappingLeading.badgeText\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingLeading\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingLeading\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingLeading.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingLeading.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Features -->\n <mat-expansion-panel [expanded]=\"featuresVisible && features.length > 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>view_list</mat-icon>\n <span>Recursos (Features)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ features.length }} item(s)</mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-12 ai-center\">\n <mat-slide-toggle [(ngModel)]=\"featuresVisible\" (ngModelChange)=\"onFeaturesChanged()\">Ativar\n recursos</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"featuresSyncWithMeta\" (ngModelChange)=\"onMappingChanged()\">Sincronizar\n com Meta</mat-slide-toggle>\n <span class=\"flex-1\"></span>\n <mat-button-toggle-group [(ngModel)]=\"featuresMode\" (change)=\"onFeaturesChanged()\" appearance=\"legacy\">\n <mat-button-toggle value=\"icons+labels\"><mat-icon>view_list</mat-icon></mat-button-toggle>\n <mat-button-toggle value=\"icons-only\"><mat-icon>more_horiz</mat-icon></mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n\n <div *ngFor=\"let f of features; let i = index\" class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\">\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\"><mat-icon>{{ f.icon || 'search'\n }}</mat-icon></button>\n <mat-form-field appearance=\"outline\" class=\"dense-form-field no-sub\">\n <input matInput [(ngModel)]=\"f.expr\" (ngModelChange)=\"onFeaturesChanged()\" placeholder=\"Expr/Texto\" />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\"><mat-icon>delete</mat-icon></button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\"><mat-icon>add</mat-icon>\n Adicionar recurso</button>\n </div>\n </mat-expansion-panel>\n <!-- Section Header -->\n <mat-expansion-panel [expanded]=\"!!mappingSectionHeader.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSectionHeader.type) }}</mat-icon>\n <span>Cabe\u00E7alho de Se\u00E7\u00E3o</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingSectionHeader.expr || 'N\u00E3o configurado'\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingSectionHeader.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of sectionHeaderTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Express\u00E3o (item.key)</mat-label>\n <input matInput [(ngModel)]=\"mappingSectionHeader.expr\" (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\" />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSectionHeader.type='text'; mappingSectionHeader.expr='${item.key}'; onMappingChanged()\">Texto padr\u00E3o</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingSectionHeader.type='chip'; mappingSectionHeader.chipColor='primary'; mappingSectionHeader.chipVariant='filled'; mappingSectionHeader.expr='${item.key}'; onMappingChanged()\">Chip padr\u00E3o</button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingSectionHeader\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingSectionHeader\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>URL Imagem</mat-label>\n <input matInput [(ngModel)]=\"mappingSectionHeader.imageUrl\" (ngModelChange)=\"onMappingChanged()\" />\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingSectionHeader.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!mappingSectionHeader.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n <praxis-meta-editor-image [model]=\"mappingSectionHeader\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingSectionHeader.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingSectionHeader.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Empty State -->\n <mat-expansion-panel [expanded]=\"!!mappingEmptyState.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>inbox</mat-icon>\n <span>Estado Vazio</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{ mappingEmptyState.expr || 'Padr\u00E3o' }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select [(ngModel)]=\"mappingEmptyState.type\" (ngModelChange)=\"onMappingChanged()\">\n <mat-option *ngFor=\"let mt of emptyStateTypeConfigs\" [value]=\"mt.type\">\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ mt.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem / Expr</mat-label>\n <input matInput [(ngModel)]=\"mappingEmptyState.expr\" (ngModelChange)=\"onMappingChanged()\" />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button mat-stroked-button type=\"button\" (click)=\"mappingEmptyState.type='text'; mappingEmptyState.expr='Nenhum item dispon\u00EDvel'; onMappingChanged()\">Mensagem padr\u00E3o</button>\n <button mat-stroked-button type=\"button\" (click)=\"mappingEmptyState.type='image'; mappingEmptyState.imageUrl='/list-empty-state.svg'; mappingEmptyState.imageAlt='Sem resultados'; onMappingChanged()\">Imagem padr\u00E3o</button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case ('text') { <praxis-meta-editor-text [model]=\"mappingEmptyState\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('html') { <praxis-meta-editor-text [model]=\"mappingEmptyState\" [setPipe]=\"setPipe.bind(this)\" (change)=\"onMappingChanged()\"></praxis-meta-editor-text> }\n @case ('chip') {\n <praxis-meta-editor-chip\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-chip>\n }\n @case ('rating') {\n <praxis-meta-editor-rating\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-rating>\n }\n @case ('icon') {\n <praxis-meta-editor-icon\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"></praxis-meta-editor-icon>\n }\n @case ('image') {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"><mat-label>URL Imagem</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.imageUrl\" (ngModelChange)=\"onMappingChanged()\" />\n <mat-error *ngIf=\"isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\">URL/expr obrigat\u00F3ria</mat-error>\n </mat-form-field>\n </div>\n <div class=\"text-caption muted\" *ngIf=\"!mappingEmptyState.imageUrl\">Defina a URL/expr para renderizar a imagem.</div>\n <praxis-meta-editor-image [model]=\"mappingEmptyState\" (change)=\"onMappingChanged()\"></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header><mat-panel-title>Estilo</mat-panel-title></mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"><mat-label>Classe</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.class\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n <mat-form-field appearance=\"outline\"><mat-label>Style</mat-label><input matInput\n [(ngModel)]=\"mappingEmptyState.style\" (ngModelChange)=\"onMappingChanged()\" /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n </mat-accordion>\n\n <button mat-flat-button color=\"primary\" (click)=\"applyTemplate()\">Aplicar mapeamento</button>\n <button mat-button (click)=\"inferFromFields()\" [disabled]=\"!fields.length\">Inferir do schema</button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Skeleton (quantidade)</mat-label>\n <input matInput type=\"number\" min=\"0\" [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\" />\n </mat-form-field>\n </div>\n\n <div class=\"g gap-12 mt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"section-title mat-subtitle-1\">Pr\u00E9via de tema</span>\n <mat-button-toggle-group [(ngModel)]=\"skinPreviewTheme\" (change)=\"onSkinChanged()\" appearance=\"legacy\">\n <mat-button-toggle [value]=\"'light'\">Claro</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">Escuro</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">Grade</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview [config]=\"working\" [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n\n </mat-tab>\n <mat-tab label=\"i18n/A11y\">\n <div class=\"editor-content grid gap-3\" *ngIf=\"working?.a11y && working?.events\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Locale padr\u00E3o</mat-label>\n <input matInput [(ngModel)]=\"working.i18n.locale\" (ngModelChange)=\"markDirty()\" placeholder=\"ex.: pt-BR\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Moeda padr\u00E3o</mat-label>\n <input matInput [(ngModel)]=\"working.i18n.currency\" (ngModelChange)=\"markDirty()\" placeholder=\"ex.: BRL\" />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Acessibilidade</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-label</mat-label>\n <input matInput [(ngModel)]=\"working!.a11y!.ariaLabel\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-labelledby</mat-label>\n <input matInput [(ngModel)]=\"working!.a11y!.ariaLabelledBy\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle [(ngModel)]=\"working!.a11y!.highContrast\" (ngModelChange)=\"markDirty()\">Alto\n contraste</mat-slide-toggle>\n <mat-slide-toggle [(ngModel)]=\"working!.a11y!.reduceMotion\" (ngModelChange)=\"markDirty()\">Reduzir\n movimento</mat-slide-toggle>\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">Eventos</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>itemClick</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.itemClick\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.actionClick\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.selectionChange\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input matInput [(ngModel)]=\"working!.events!.loaded\" (ngModelChange)=\"markDirty()\" />\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Sele\u00E7\u00E3o\">\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Modo</mat-label>\n <mat-select [(ngModel)]=\"working.selection.mode\" (ngModelChange)=\"onSelectionChanged()\">\n <mat-option value=\"none\">Sem sele\u00E7\u00E3o</mat-option>\n <mat-option value=\"single\">\u00DAnica</mat-option>\n <mat-option value=\"multiple\">M\u00FAltipla</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Nome no formul\u00E1rio</mat-label>\n <input matInput [(ngModel)]=\"working.selection.formControlName\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"formControlName\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Caminho no formul\u00E1rio</mat-label>\n <input matInput [(ngModel)]=\"working.selection.formControlPath\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"formControlPath\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Comparar por (campo)</mat-label>\n <input matInput [(ngModel)]=\"working.selection.compareBy\" (ngModelChange)=\"onSelectionChanged()\" />\n <button mat-icon-button matSuffix type=\"button\" class=\"help-icon-button\" matTooltip=\"Chave unica do item.\">\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Retorno</mat-label>\n <mat-select [(ngModel)]=\"working.selection.return\" (ngModelChange)=\"onSelectionChanged()\">\n <mat-option value=\"value\">value</mat-option>\n <mat-option value=\"item\">item</mat-option>\n <mat-option value=\"id\">id</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab label=\"Apar\u00EAncia\">\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-button (click)=\"applySkinPreset('pill-soft')\">Pill Soft</button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">Gradient Tile</button>\n <button mat-button (click)=\"applySkinPreset('glass')\">Glass</button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">Elevated</button>\n <button mat-button (click)=\"applySkinPreset('outline')\">Outline</button>\n <button mat-button (click)=\"applySkinPreset('flat')\">Flat</button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">Neumorphism</button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <mat-select [(ngModel)]=\"working.skin.type\" (ngModelChange)=\"onSkinTypeChanged($event)\">\n <mat-option value=\"pill-soft\">Pill Soft</mat-option>\n <mat-option value=\"gradient-tile\">Gradient Tile</mat-option>\n <mat-option value=\"glass\">Glass</mat-option>\n <mat-option value=\"elevated\">Elevated</mat-option>\n <mat-option value=\"outline\">Outline</mat-option>\n <mat-option value=\"flat\">Flat</mat-option>\n <mat-option value=\"neumorphism\">Neumorphism</mat-option>\n <mat-option value=\"custom\">Custom</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Raio</mat-label>\n <input matInput [(ngModel)]=\"working.skin.radius\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 1.25rem\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Sombra</mat-label>\n <input matInput [(ngModel)]=\"working.skin.shadow\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: var(--md-sys-elevation-level2)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Borda</mat-label>\n <input matInput [(ngModel)]=\"working.skin.border\" (ngModelChange)=\"onSkinChanged()\" />\n </mat-form-field>\n <mat-form-field *ngIf=\"working.skin.type==='glass'\" appearance=\"outline\">\n <mat-label>Desfoque</mat-label>\n <input matInput [(ngModel)]=\"working.skin.backdropBlur\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 8px\" />\n </mat-form-field>\n <div *ngIf=\"working.skin.type==='gradient-tile'\" class=\"g gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA de</mat-label>\n <input matInput [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA at\u00E9</mat-label>\n <input matInput [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00C2ngulo</mat-label>\n <input matInput type=\"number\" [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\" />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>Classe CSS extra (skin.class)</mat-label>\n <input matInput [(ngModel)]=\"working.skin.class\" (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: my-list-skin\" />\n </mat-form-field>\n\n <div *ngIf=\"working.skin.type==='custom'\" class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Estilo inline (skin.inlineStyle)</mat-label>\n <textarea matInput rows=\"4\" [(ngModel)]=\"working.skin.inlineStyle\" (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n Exemplo de CSS por classe (adicione no seu styles global):\n <pre class=\"code-block\">.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre>\n </div>\n </div>\n\n\n </div>\n </mat-tab>\n</mat-tab-group>\n", styles: [".confirm-type{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;line-height:16px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.confirm-type.danger{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.confirm-type.warning{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.confirm-type.info{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}:host{display:block;color:var(--md-sys-color-on-surface)}.list-editor-tabs{--editor-surface: var(--md-sys-color-surface-container-lowest);--editor-border: 1px solid var(--md-sys-color-outline-variant);--editor-radius: var(--md-sys-shape-corner-large, 16px);--editor-muted: var(--md-sys-color-on-surface-variant);--editor-accent: var(--md-sys-color-primary)}.editor-content{padding:16px;background:var(--editor-surface);border:var(--editor-border);border-radius:var(--editor-radius);display:grid;gap:12px}.editor-content .mat-mdc-form-field{width:100%;max-width:none;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.editor-content .mat-mdc-form-field.w-full{max-width:none}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.editor-split{grid-template-columns:minmax(0,1fr);align-items:start}.editor-main,.editor-aside{display:grid;gap:12px}.skin-preview-wrap{border-radius:calc(var(--editor-radius) - 4px);border:var(--editor-border);background:var(--md-sys-color-surface-container);padding:12px}.g{display:grid}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.g-auto-200{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.g-1-auto{grid-template-columns:1fr auto}.row-flow{grid-auto-flow:column}.gap-6{gap:6px}.gap-8{gap:8px}.gap-12{gap:12px}.ai-center{align-items:center}.ai-end{align-items:end}.mt-12{margin-top:12px}.mb-8{margin-bottom:8px}.mb-6{margin-bottom:6px}.my-8{margin:8px 0}.subtitle{margin:8px 0 4px;color:var(--editor-muted);font-weight:500}.section-title{color:var(--editor-muted);font-weight:600}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--md-sys-color-error);font-size:.85rem}.muted{color:var(--editor-muted)}.text-caption{color:var(--editor-muted);font-size:.8rem}:host ::ng-deep .mat-mdc-select-panel .option-icon{font-size:18px;margin-right:6px;vertical-align:middle}:host ::ng-deep .mat-mdc-select-panel .color-dot{width:10px;height:10px;border-radius:999px;display:inline-block;margin-right:6px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-outline)}:host ::ng-deep .mat-mdc-select-panel .color-primary{background:var(--md-sys-color-primary)}:host ::ng-deep .mat-mdc-select-panel .color-accent{background:var(--md-sys-color-tertiary)}:host ::ng-deep .mat-mdc-select-panel .color-warn{background:var(--md-sys-color-error)}:host ::ng-deep .mat-mdc-select-panel .color-default{background:var(--md-sys-color-outline)}@media(max-width:1024px){.editor-split{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
2860
3665
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
2861
3666
|
type: Optional
|
|
2862
3667
|
}, {
|
|
@@ -3242,6 +4047,9 @@ class PraxisList {
|
|
|
3242
4047
|
storage = inject(ASYNC_CONFIG_STORAGE);
|
|
3243
4048
|
skin = inject(ListSkinService);
|
|
3244
4049
|
inferredForPath = new Set();
|
|
4050
|
+
schemaInferenceInFlight = new Set();
|
|
4051
|
+
schemaFieldsByPath = new Map();
|
|
4052
|
+
appliedRuntimeConfig;
|
|
3245
4053
|
data = inject(ListDataService);
|
|
3246
4054
|
settings = inject(SettingsPanelService);
|
|
3247
4055
|
cdr = inject(ChangeDetectorRef);
|
|
@@ -3279,7 +4087,8 @@ class PraxisList {
|
|
|
3279
4087
|
destroy$ = new Subject();
|
|
3280
4088
|
actionLoadingState = {};
|
|
3281
4089
|
ngOnInit() {
|
|
3282
|
-
this.
|
|
4090
|
+
this.initializeDataStreams();
|
|
4091
|
+
this.applyAuthoringPayload(this.config, 'external-input');
|
|
3283
4092
|
this.setupSearch();
|
|
3284
4093
|
this.applyPersistence();
|
|
3285
4094
|
}
|
|
@@ -3303,7 +4112,7 @@ class PraxisList {
|
|
|
3303
4112
|
// We already perform the initial setup in ngOnInit, so skip the first change here.
|
|
3304
4113
|
if (!changes['config'].firstChange) {
|
|
3305
4114
|
this.externalConfigRevision += 1;
|
|
3306
|
-
this.
|
|
4115
|
+
this.applyAuthoringPayload(changes['config'].currentValue, 'external-input');
|
|
3307
4116
|
}
|
|
3308
4117
|
}
|
|
3309
4118
|
if (shouldApplyPersistence) {
|
|
@@ -3334,8 +4143,8 @@ class PraxisList {
|
|
|
3334
4143
|
if (revisionAtLoadStart !== this.externalConfigRevision)
|
|
3335
4144
|
return;
|
|
3336
4145
|
const currentId = this.config?.id;
|
|
3337
|
-
|
|
3338
|
-
this.
|
|
4146
|
+
const persisted = currentId ? { ...saved, id: currentId } : { ...saved };
|
|
4147
|
+
this.applyAuthoringPayload(persisted, 'persistence');
|
|
3339
4148
|
}
|
|
3340
4149
|
},
|
|
3341
4150
|
error: () => {
|
|
@@ -3354,22 +4163,18 @@ class PraxisList {
|
|
|
3354
4163
|
},
|
|
3355
4164
|
});
|
|
3356
4165
|
}
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
this.data.setConfig(this.config);
|
|
4166
|
+
initializeDataStreams() {
|
|
4167
|
+
if (this.items$)
|
|
4168
|
+
return;
|
|
3361
4169
|
this.items$ = this.data.stream();
|
|
3362
4170
|
this.sections$ = this.data.groupedStream();
|
|
3363
4171
|
this.loading$ = this.data.loading$;
|
|
3364
4172
|
this.total$ = this.data.total$;
|
|
3365
4173
|
this.page$ = this.data.pageState$;
|
|
3366
|
-
this.setupSelectionBinding();
|
|
3367
|
-
this.applySkins();
|
|
3368
|
-
this.lastQuery = this.config?.dataSource?.query || {};
|
|
3369
4174
|
}
|
|
3370
4175
|
setupSearch() {
|
|
3371
4176
|
this.search$
|
|
3372
|
-
.pipe(debounceTime
|
|
4177
|
+
.pipe(debounceTime(300), distinctUntilChanged$1(), takeUntil$1(this.destroy$))
|
|
3373
4178
|
.subscribe((term) => {
|
|
3374
4179
|
const field = this.config?.ui?.searchField;
|
|
3375
4180
|
if (!field)
|
|
@@ -3578,19 +4383,18 @@ class PraxisList {
|
|
|
3578
4383
|
title: 'Lista – Configurações',
|
|
3579
4384
|
content: {
|
|
3580
4385
|
component: PraxisListConfigEditor,
|
|
3581
|
-
inputs: {
|
|
4386
|
+
inputs: {
|
|
4387
|
+
config: this.config,
|
|
4388
|
+
document: createListAuthoringDocument({ config: this.config }),
|
|
4389
|
+
listId: this.listId,
|
|
4390
|
+
},
|
|
3582
4391
|
},
|
|
3583
4392
|
});
|
|
3584
|
-
ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((applied) => {
|
|
3585
|
-
|
|
3586
|
-
this.onEditorApplied(newCfg);
|
|
4393
|
+
ref.applied$.pipe(takeUntil$1(this.destroy$)).subscribe((applied) => {
|
|
4394
|
+
this.applyAuthoringPayload(applied, 'settings-applied');
|
|
3587
4395
|
});
|
|
3588
|
-
ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((saved) => {
|
|
3589
|
-
|
|
3590
|
-
if (key) {
|
|
3591
|
-
this.persistConfig(key, newCfg);
|
|
3592
|
-
}
|
|
3593
|
-
this.onEditorApplied(newCfg);
|
|
4396
|
+
ref.saved$.pipe(takeUntil$1(this.destroy$)).subscribe((saved) => {
|
|
4397
|
+
this.applyAuthoringPayload(saved, 'settings-saved');
|
|
3594
4398
|
});
|
|
3595
4399
|
}
|
|
3596
4400
|
nextPage() {
|
|
@@ -3630,17 +4434,54 @@ class PraxisList {
|
|
|
3630
4434
|
return Math.min(to, total || 0);
|
|
3631
4435
|
}
|
|
3632
4436
|
applyConfigFromAdapter(newCfg) {
|
|
3633
|
-
this.
|
|
3634
|
-
// Optionally infer templates from backend schema if not defined
|
|
3635
|
-
this.tryInferTemplatingFromSchema(newCfg);
|
|
3636
|
-
// Re-apply runtime wiring
|
|
3637
|
-
this.data.setConfig(this.config);
|
|
3638
|
-
this.setupSelectionBinding();
|
|
3639
|
-
this.applySkins();
|
|
3640
|
-
this.cdr.markForCheck();
|
|
4437
|
+
this.applyAuthoringPayload(newCfg, 'adapter');
|
|
3641
4438
|
}
|
|
3642
|
-
|
|
3643
|
-
|
|
4439
|
+
applyAuthoringPayload(raw, trigger) {
|
|
4440
|
+
const doc = parseLegacyOrListDocument(raw);
|
|
4441
|
+
const plan = buildListApplyPlan(doc, this.buildListEditorRuntimeContext(doc), {
|
|
4442
|
+
saveConfig: trigger === 'settings-saved',
|
|
4443
|
+
});
|
|
4444
|
+
this.executeListEditorApplyPlan(plan, doc, trigger);
|
|
4445
|
+
}
|
|
4446
|
+
buildListEditorRuntimeContext(doc) {
|
|
4447
|
+
const resourcePath = doc?.config?.dataSource?.resourcePath ||
|
|
4448
|
+
this.appliedRuntimeConfig?.dataSource?.resourcePath;
|
|
4449
|
+
return {
|
|
4450
|
+
currentConfig: this.appliedRuntimeConfig,
|
|
4451
|
+
schemaFieldNames: resourcePath
|
|
4452
|
+
? this.schemaFieldsByPath.get(resourcePath)
|
|
4453
|
+
: undefined,
|
|
4454
|
+
};
|
|
4455
|
+
}
|
|
4456
|
+
executeListEditorApplyPlan(plan, _doc, trigger) {
|
|
4457
|
+
if (plan.diagnostics.some((item) => item.level === 'error')) {
|
|
4458
|
+
return;
|
|
4459
|
+
}
|
|
4460
|
+
this.initializeDataStreams();
|
|
4461
|
+
this.config = normalizeListConfig(plan.canonicalConfig);
|
|
4462
|
+
this.appliedRuntimeConfig = this.config;
|
|
4463
|
+
if (plan.runtime?.applyConfig) {
|
|
4464
|
+
this.data.setConfig(this.config);
|
|
4465
|
+
this.lastQuery = this.config?.dataSource?.query || {};
|
|
4466
|
+
}
|
|
4467
|
+
if (plan.runtime?.rebindSelection) {
|
|
4468
|
+
this.setupSelectionBinding();
|
|
4469
|
+
}
|
|
4470
|
+
if (plan.runtime?.reapplySkin) {
|
|
4471
|
+
this.applySkins();
|
|
4472
|
+
}
|
|
4473
|
+
if (plan.persistence?.saveConfig) {
|
|
4474
|
+
const key = this.storageKey();
|
|
4475
|
+
if (key) {
|
|
4476
|
+
this.persistConfig(key, this.config);
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
if (plan.runtime?.schemaInference) {
|
|
4480
|
+
this.executeSchemaInferenceForPlan(plan.runtime.schemaInference, plan.persistence?.saveConfig === true);
|
|
4481
|
+
}
|
|
4482
|
+
if (plan.runtime?.markForCheck) {
|
|
4483
|
+
this.cdr.markForCheck();
|
|
4484
|
+
}
|
|
3644
4485
|
}
|
|
3645
4486
|
storageKey() {
|
|
3646
4487
|
const id = this.componentKeyId();
|
|
@@ -3673,44 +4514,48 @@ class PraxisList {
|
|
|
3673
4514
|
}
|
|
3674
4515
|
}
|
|
3675
4516
|
crud = inject(GenericCrudService, { optional: true });
|
|
3676
|
-
|
|
3677
|
-
const
|
|
3678
|
-
if (!
|
|
4517
|
+
executeSchemaInferenceForPlan(inferencePlan, saveAfterInference = false) {
|
|
4518
|
+
const resourcePath = inferencePlan.resourcePath;
|
|
4519
|
+
if (!resourcePath || !this.crud)
|
|
4520
|
+
return;
|
|
4521
|
+
if (this.inferredForPath.has(resourcePath))
|
|
3679
4522
|
return;
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
if (
|
|
3683
|
-
return;
|
|
3684
|
-
|
|
3685
|
-
return; // avoid repeated schema calls for the same path
|
|
4523
|
+
if (this.schemaInferenceInFlight.has(resourcePath))
|
|
4524
|
+
return;
|
|
4525
|
+
if (inferencePlan.targetDocument.config?.templating?.primary)
|
|
4526
|
+
return;
|
|
4527
|
+
this.schemaInferenceInFlight.add(resourcePath);
|
|
3686
4528
|
try {
|
|
3687
|
-
this.crud.configure(
|
|
4529
|
+
this.crud.configure(resourcePath);
|
|
3688
4530
|
this.crud
|
|
3689
4531
|
.getSchema()
|
|
3690
4532
|
.pipe(take(1))
|
|
3691
4533
|
.subscribe({
|
|
3692
4534
|
next: (fields) => {
|
|
3693
|
-
this.inferredForPath.add(rp);
|
|
3694
4535
|
const names = (fields || [])
|
|
3695
|
-
.map((
|
|
4536
|
+
.map((field) => field?.name || field?.field || field?.key)
|
|
3696
4537
|
.filter(Boolean);
|
|
3697
4538
|
if (!names.length)
|
|
3698
4539
|
return;
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
4540
|
+
this.inferredForPath.add(resourcePath);
|
|
4541
|
+
this.schemaFieldsByPath.set(resourcePath, names);
|
|
4542
|
+
const inferredDocument = inferListAuthoringDocument(inferencePlan.sourceDocument, {
|
|
4543
|
+
schemaFieldNames: names,
|
|
4544
|
+
});
|
|
4545
|
+
this.applyAuthoringPayload(inferredDocument, saveAfterInference ? 'settings-saved' : 'schema-inference');
|
|
3705
4546
|
},
|
|
3706
4547
|
error: () => {
|
|
3707
|
-
|
|
3708
|
-
this.
|
|
4548
|
+
this.schemaInferenceInFlight.delete(resourcePath);
|
|
4549
|
+
this.schemaFieldsByPath.delete(resourcePath);
|
|
4550
|
+
},
|
|
4551
|
+
complete: () => {
|
|
4552
|
+
this.schemaInferenceInFlight.delete(resourcePath);
|
|
3709
4553
|
},
|
|
3710
4554
|
});
|
|
3711
4555
|
}
|
|
3712
4556
|
catch {
|
|
3713
|
-
|
|
4557
|
+
this.schemaInferenceInFlight.delete(resourcePath);
|
|
4558
|
+
this.schemaFieldsByPath.delete(resourcePath);
|
|
3714
4559
|
}
|
|
3715
4560
|
}
|
|
3716
4561
|
evalSlot(slot, item) {
|
|
@@ -4908,5 +5753,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
4908
5753
|
* Generated bundle index. Do not edit.
|
|
4909
5754
|
*/
|
|
4910
5755
|
|
|
4911
|
-
export { LIST_AI_CAPABILITIES, ListDataService, ListSkinService, PRAXIS_LIST_COMPONENT_METADATA, PraxisList, PraxisListConfigEditor, PraxisListDocPageComponent, adaptSelection, evalExpr, evaluateTemplate, inferTemplatingFromSchema, providePraxisListMetadata };
|
|
5756
|
+
export { LIST_AI_CAPABILITIES, ListDataService, ListSkinService, PRAXIS_LIST_COMPONENT_METADATA, PraxisList, PraxisListConfigEditor, PraxisListDocPageComponent, PraxisListJsonConfigEditorComponent, adaptSelection, buildListApplyPlan, createListAuthoringDocument, evalExpr, evaluateTemplate, inferListAuthoringDocument, inferTemplatingFromSchema, normalizeListActionPayloads, normalizeListAuthoringDocument, normalizeListConfig, parseLegacyOrListDocument, projectListAuthoringDocument, providePraxisListMetadata, serializeListAuthoringDocument, toCanonicalListConfig, validateListAuthoringDocument };
|
|
4912
5757
|
//# sourceMappingURL=praxisui-list.mjs.map
|