@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.
@@ -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 as debounceTime$1, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
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$2 from '@angular/material/tabs';
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 cfg = injected?.config || this.config;
1235
- if (cfg) {
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 = JSON.stringify(this.working);
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
- // Ensure pending template mapping is applied before returning
1286
- this.ensureMappingApplied();
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.ensureMappingApplied();
1292
- this.normalizeActionPayloads();
1293
- return { config: this.working, id: this.listId };
1294
- }
1295
- normalize(cfg) {
1296
- const out = {
1297
- ...cfg,
1298
- dataSource: cfg.dataSource || {},
1299
- layout: {
1300
- variant: cfg.layout?.variant || 'list',
1301
- lines: cfg.layout?.lines || 2,
1302
- density: cfg.layout?.density || 'default',
1303
- dividers: cfg.layout?.dividers || 'between',
1304
- model: cfg.layout?.model || 'standard',
1305
- groupBy: cfg.layout?.groupBy,
1306
- stickySectionHeader: cfg.layout?.stickySectionHeader,
1307
- virtualScroll: cfg.layout?.virtualScroll,
1308
- },
1309
- selection: {
1310
- mode: cfg.selection?.mode || 'none',
1311
- formControlName: cfg.selection?.formControlName,
1312
- formControlPath: cfg.selection?.formControlPath,
1313
- compareBy: cfg.selection?.compareBy,
1314
- return: cfg.selection?.return || 'value',
1315
- },
1316
- skin: {
1317
- type: cfg.skin?.type || 'elevated',
1318
- gradient: cfg.skin?.gradient || { from: '', to: '', angle: 135 },
1319
- radius: cfg.skin?.radius,
1320
- shadow: cfg.skin?.shadow,
1321
- border: cfg.skin?.border,
1322
- backdropBlur: cfg.skin?.backdropBlur,
1323
- class: cfg.skin?.class,
1324
- inlineStyle: cfg.skin?.inlineStyle,
1325
- },
1326
- i18n: cfg.i18n || {},
1327
- ui: cfg.ui || {},
1328
- a11y: cfg.a11y || {},
1329
- events: cfg.events || {},
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
- return out;
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.working = parsed;
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 n = Number(value);
1824
- const fixed = !Number.isFinite(n) || n < 1 ? 1 : Math.floor(n);
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 = fixed;
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
- this.working = produce(this.working, (draft) => {
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.applyTemplate();
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.working);
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
- // Basic constraints
2618
- const rp = (this.working?.dataSource?.resourcePath || '').trim();
2619
- const pageSizeRaw = this.working?.layout?.pageSize;
2620
- const pageSizeNum = pageSizeRaw == null ? undefined : Number(pageSizeRaw);
2621
- const lines = this.working?.layout?.lines;
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 updated = inferTemplatingFromSchema(this.working, this.fields);
2688
- this.working = updated;
2689
- // Keep mapping controls in sync with new templating
2690
- this.hydrateMappingFromTemplating(this.working.templating);
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.: &#123;\"status\":\"active\",\"department\":\"sales\"&#125;'></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.: &#36;&#123;item.status&#125; == '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: &#34;&#36;{item.campo} == &#39;valor&#39;&#34;. 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 &#123;\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n&#125;\n.my-list-skin .mat-mdc-list-item .list-item-content &#123;\n backdrop-filter: blur(6px);\n&#125;</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.: &#123;\"status\":\"active\",\"department\":\"sales\"&#125;'></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.: &#36;&#123;item.status&#125; == '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: &#34;&#36;{item.campo} == &#39;valor&#39;&#34;. 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 &#123;\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n&#125;\n.my-list-skin .mat-mdc-list-item .list-item-content &#123;\n backdrop-filter: blur(6px);\n&#125;</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.: &#123;\"status\":\"active\",\"department\":\"sales\"&#125;'></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.: &#36;&#123;item.status&#125; == '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: &#34;&#36;{item.campo} == &#39;valor&#39;&#34;. 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 &#123;\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n&#125;\n.my-list-skin .mat-mdc-list-item .list-item-content &#123;\n backdrop-filter: blur(6px);\n&#125;</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.: &#123;\"status\":\"active\",\"department\":\"sales\"&#125;'></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.: &#36;&#123;item.status&#125; == '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: &#34;&#36;{item.campo} == &#39;valor&#39;&#34;. 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 &#123;\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n&#125;\n.my-list-skin .mat-mdc-list-item .list-item-content &#123;\n backdrop-filter: blur(6px);\n&#125;</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.applyConfigChange();
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.applyConfigChange();
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
- this.config = currentId ? { ...saved, id: currentId } : { ...saved };
3338
- this.applyConfigChange();
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
- applyConfigChange() {
3358
- // Try infer templating upfront when using remote resource
3359
- this.tryInferTemplatingFromSchema(this.config);
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$1(300), distinctUntilChanged$1(), takeUntil(this.destroy$))
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: { config: this.config, listId: this.listId },
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
- const newCfg = applied?.config || applied;
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
- const newCfg = saved?.config || saved;
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.config = newCfg;
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
- onEditorApplied(newCfg) {
3643
- this.applyConfigFromAdapter(newCfg);
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
- tryInferTemplatingFromSchema(cfg) {
3677
- const rp = cfg.dataSource?.resourcePath;
3678
- if (!rp || !this.crud)
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
- const templ = cfg.templating || {};
3681
- const needsPrimary = !templ.primary;
3682
- if (!needsPrimary)
3683
- return; // do not override when user already defined
3684
- if (this.inferredForPath.has(rp))
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(rp);
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((f) => f.name || f.field || f.key)
4536
+ .map((field) => field?.name || field?.field || field?.key)
3696
4537
  .filter(Boolean);
3697
4538
  if (!names.length)
3698
4539
  return;
3699
- const updated = inferTemplatingFromSchema(cfg, names);
3700
- // Assign back to current config reference
3701
- // Only assign if primary is still not defined to avoid loops
3702
- if (!this.config?.templating?.primary) {
3703
- this.config = updated;
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
- // ignore schema errors; keep config as-is
3708
- this.inferredForPath.add(rp);
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
- // ignore
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