@praxisui/list 3.0.0-beta.0 → 3.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/fesm2022/praxisui-list.mjs +858 -162
- package/index.d.ts +479 -9
- package/package.json +2 -2
- package/fesm2022/praxisui-list.mjs.map +0 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Injectable, Input, ChangeDetectionStrategy, Component, CSP_NONCE, EventEmitter, Output, Optional, Inject, ChangeDetectorRef, isDevMode, booleanAttribute, ENVIRONMENT_INITIALIZER, signal, computed } from '@angular/core';
|
|
2
|
+
import { inject, Injectable, Input, ChangeDetectionStrategy, Component, LOCALE_ID, CSP_NONCE, EventEmitter, Output, Optional, Inject, ChangeDetectorRef, isDevMode, booleanAttribute, ENVIRONMENT_INITIALIZER, signal, computed } from '@angular/core';
|
|
3
3
|
import { ActivatedRoute } from '@angular/router';
|
|
4
4
|
import * as i1 from '@angular/common';
|
|
5
|
-
import { CommonModule } from '@angular/common';
|
|
5
|
+
import { formatDate, CommonModule } from '@angular/common';
|
|
6
6
|
import * as i3 from '@angular/material/list';
|
|
7
7
|
import { MatListModule } from '@angular/material/list';
|
|
8
8
|
import * as i4 from '@angular/material/icon';
|
|
9
9
|
import { MatIconModule } from '@angular/material/icon';
|
|
10
|
-
import { LoggerService, GenericCrudService, PraxisIconDirective, IconPickerService, GLOBAL_ACTION_CATALOG, PRAXIS_GLOBAL_ACTION_CATALOG, getGlobalActionCatalog,
|
|
10
|
+
import { resolveValuePresentation, LoggerService, GenericCrudService, PraxisIconDirective, providePraxisI18n, PraxisI18nService, IconPickerService, GLOBAL_ACTION_CATALOG, PRAXIS_GLOBAL_ACTION_CATALOG, getGlobalActionCatalog, SURFACE_OPEN_I18N_NAMESPACE, getGlobalActionUiSchema, SurfaceOpenActionEditorComponent, providePraxisI18nConfig, SURFACE_OPEN_I18N_CONFIG, deepMerge, ASYNC_CONFIG_STORAGE, ComponentKeyService, GlobalActionService, GLOBAL_DIALOG_SERVICE, ComponentMetadataRegistry, API_URL } from '@praxisui/core';
|
|
11
11
|
import * as i5 from '@angular/material/chips';
|
|
12
12
|
import { MatChipsModule } from '@angular/material/chips';
|
|
13
13
|
import { MatDividerModule } from '@angular/material/divider';
|
|
@@ -49,7 +49,7 @@ import { BaseAiAdapter, PraxisAiAssistantComponent } from '@praxisui/ai';
|
|
|
49
49
|
* Very small template evaluator that supports `${item.foo}` expressions.
|
|
50
50
|
* For security, only string concatenation is supported; no arbitrary code.
|
|
51
51
|
*/
|
|
52
|
-
function evalExpr(expr, ctx) {
|
|
52
|
+
function evalExpr(expr, ctx, options) {
|
|
53
53
|
if (!expr)
|
|
54
54
|
return '';
|
|
55
55
|
// Replace ${...} with values from context (item), supporting simple pipes inside placeholders
|
|
@@ -71,8 +71,12 @@ function evalExpr(expr, ctx) {
|
|
|
71
71
|
const [localeArg, styleArg] = parseTwoArgs(args);
|
|
72
72
|
const dt = value ? new Date(value) : null;
|
|
73
73
|
if (dt && !isNaN(dt.getTime())) {
|
|
74
|
-
const
|
|
75
|
-
|
|
74
|
+
const presentation = resolveTemplatePresentation('date', {
|
|
75
|
+
localeArg,
|
|
76
|
+
styleArg,
|
|
77
|
+
options,
|
|
78
|
+
});
|
|
79
|
+
out = formatDate(dt, presentation.format || 'shortDate', presentation.locale);
|
|
76
80
|
}
|
|
77
81
|
else {
|
|
78
82
|
out = '';
|
|
@@ -87,20 +91,33 @@ function evalExpr(expr, ctx) {
|
|
|
87
91
|
const [localeArg, styleArg] = parseTwoArgs(args);
|
|
88
92
|
const num = Number(value);
|
|
89
93
|
if (isFinite(num)) {
|
|
94
|
+
const presentation = resolveTemplatePresentation('number', {
|
|
95
|
+
localeArg,
|
|
96
|
+
styleArg,
|
|
97
|
+
options,
|
|
98
|
+
});
|
|
90
99
|
const opt = {};
|
|
91
100
|
if ((styleArg || '').toLowerCase() === 'compact')
|
|
92
101
|
opt.notation = 'compact';
|
|
93
|
-
|
|
102
|
+
applyNumberPrecision(opt, presentation.format);
|
|
103
|
+
out = new Intl.NumberFormat(presentation.locale || undefined, opt).format(num);
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
if (name === 'currency') {
|
|
97
107
|
const [currencyArg, localeArg] = parseTwoArgs(args);
|
|
98
108
|
const num = Number(value);
|
|
99
109
|
if (isFinite(num)) {
|
|
100
|
-
|
|
110
|
+
const presentation = resolveTemplatePresentation('currency', {
|
|
111
|
+
localeArg,
|
|
112
|
+
currencyArg,
|
|
113
|
+
options,
|
|
114
|
+
});
|
|
115
|
+
const opt = {
|
|
101
116
|
style: 'currency',
|
|
102
|
-
currency:
|
|
103
|
-
}
|
|
117
|
+
currency: presentation.currency || 'USD',
|
|
118
|
+
};
|
|
119
|
+
applyCurrencyPrecision(opt, presentation.format);
|
|
120
|
+
out = new Intl.NumberFormat(presentation.locale || undefined, opt).format(num);
|
|
104
121
|
}
|
|
105
122
|
}
|
|
106
123
|
}
|
|
@@ -116,13 +133,19 @@ function evalExpr(expr, ctx) {
|
|
|
116
133
|
* Supported pipes:
|
|
117
134
|
* - bool:TrueLabel:FalseLabel (maps truthy/falsey values to provided labels)
|
|
118
135
|
*/
|
|
119
|
-
function evaluateTemplate(def, item) {
|
|
136
|
+
function evaluateTemplate(def, item, options) {
|
|
120
137
|
if (!def)
|
|
121
138
|
return null;
|
|
139
|
+
if (def.type === 'currency') {
|
|
140
|
+
return evaluateCurrencyTemplate(def, item, options);
|
|
141
|
+
}
|
|
142
|
+
if (def.type === 'date') {
|
|
143
|
+
return evaluateDateTemplate(def, item, options);
|
|
144
|
+
}
|
|
122
145
|
// Support a top-level pipe outside of template placeholders,
|
|
123
146
|
// e.g.: "${item.active}|bool:Ativo:Inativo".
|
|
124
147
|
const { baseExpr, pipe } = splitFirstTopLevelPipe(def.expr || '');
|
|
125
|
-
let value = evalExpr(baseExpr, { item });
|
|
148
|
+
let value = evalExpr(baseExpr, { item }, options);
|
|
126
149
|
if (pipe) {
|
|
127
150
|
const { name, args } = parsePipe(pipe);
|
|
128
151
|
if (name === 'bool') {
|
|
@@ -134,7 +157,7 @@ function evaluateTemplate(def, item) {
|
|
|
134
157
|
const out = { type: def.type, value, class: def.class, style: def.style, color: def.color, variant: def.variant, props: def.props, imageAlt: def.imageAlt };
|
|
135
158
|
if (def.badge?.expr) {
|
|
136
159
|
const { baseExpr: b2, pipe: p2 } = splitFirstTopLevelPipe(def.badge.expr);
|
|
137
|
-
let badgeVal = evalExpr(b2, { item });
|
|
160
|
+
let badgeVal = evalExpr(b2, { item }, options);
|
|
138
161
|
if (p2) {
|
|
139
162
|
const { name, args } = parsePipe(p2);
|
|
140
163
|
if (name === 'bool') {
|
|
@@ -151,6 +174,48 @@ function evaluateTemplate(def, item) {
|
|
|
151
174
|
}
|
|
152
175
|
return out;
|
|
153
176
|
}
|
|
177
|
+
function evaluateCurrencyTemplate(def, item, options) {
|
|
178
|
+
const { baseExpr, pipe } = splitFirstTopLevelPipe(def.expr || '');
|
|
179
|
+
const [currencyArg, localeArg] = parseTwoArgs(pipe);
|
|
180
|
+
const raw = Number(evalExpr(baseExpr, { item }, options));
|
|
181
|
+
const presentation = resolveTemplatePresentation('currency', {
|
|
182
|
+
localeArg,
|
|
183
|
+
currencyArg,
|
|
184
|
+
options,
|
|
185
|
+
});
|
|
186
|
+
const intlOptions = {
|
|
187
|
+
style: 'currency',
|
|
188
|
+
currency: presentation.currency || 'USD',
|
|
189
|
+
};
|
|
190
|
+
applyCurrencyPrecision(intlOptions, presentation.format);
|
|
191
|
+
return {
|
|
192
|
+
type: def.type,
|
|
193
|
+
value: isFinite(raw)
|
|
194
|
+
? new Intl.NumberFormat(presentation.locale || undefined, intlOptions).format(raw)
|
|
195
|
+
: '',
|
|
196
|
+
class: def.class,
|
|
197
|
+
style: def.style,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function evaluateDateTemplate(def, item, options) {
|
|
201
|
+
const { baseExpr, pipe } = splitFirstTopLevelPipe(def.expr || '');
|
|
202
|
+
const [localeArg, styleArg] = parseTwoArgs(pipe);
|
|
203
|
+
const raw = evalExpr(baseExpr, { item }, options);
|
|
204
|
+
const dt = raw ? new Date(raw) : null;
|
|
205
|
+
const presentation = resolveTemplatePresentation('date', {
|
|
206
|
+
localeArg,
|
|
207
|
+
styleArg,
|
|
208
|
+
options,
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
type: def.type,
|
|
212
|
+
value: dt && !isNaN(dt.getTime())
|
|
213
|
+
? formatDate(dt, presentation.format || 'shortDate', presentation.locale)
|
|
214
|
+
: '',
|
|
215
|
+
class: def.class,
|
|
216
|
+
style: def.style,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
154
219
|
function splitFirstPipe(expr) {
|
|
155
220
|
const idx = expr.indexOf('|');
|
|
156
221
|
if (idx === -1)
|
|
@@ -226,6 +291,87 @@ function parseMap(spec) {
|
|
|
226
291
|
}
|
|
227
292
|
return out;
|
|
228
293
|
}
|
|
294
|
+
function resolveTemplatePresentation(type, params) {
|
|
295
|
+
const listI18n = params.options?.i18n;
|
|
296
|
+
const localization = listI18n?.localization ||
|
|
297
|
+
(listI18n?.locale || listI18n?.currency
|
|
298
|
+
? {
|
|
299
|
+
locale: listI18n.locale || undefined,
|
|
300
|
+
direction: 'ltr',
|
|
301
|
+
currency: listI18n.currency
|
|
302
|
+
? { code: listI18n.currency }
|
|
303
|
+
: undefined,
|
|
304
|
+
}
|
|
305
|
+
: undefined);
|
|
306
|
+
const presentation = resolveValuePresentation({
|
|
307
|
+
type,
|
|
308
|
+
style: mapValuePresentationStyle(params.styleArg),
|
|
309
|
+
currency: params.currencyArg
|
|
310
|
+
? {
|
|
311
|
+
code: params.currencyArg,
|
|
312
|
+
}
|
|
313
|
+
: undefined,
|
|
314
|
+
}, {
|
|
315
|
+
valueLocale: params.localeArg || undefined,
|
|
316
|
+
localization: localization,
|
|
317
|
+
surfaceLocale: listI18n?.locale || undefined,
|
|
318
|
+
appLocale: params.options?.appLocale || undefined,
|
|
319
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
locale: presentation.locale,
|
|
322
|
+
format: presentation.format,
|
|
323
|
+
currency: presentation.currency?.code,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function mapValuePresentationStyle(style) {
|
|
327
|
+
switch ((style || '').toLowerCase()) {
|
|
328
|
+
case 'short':
|
|
329
|
+
return 'short';
|
|
330
|
+
case 'medium':
|
|
331
|
+
return 'medium';
|
|
332
|
+
case 'long':
|
|
333
|
+
return 'long';
|
|
334
|
+
case 'full':
|
|
335
|
+
return 'full';
|
|
336
|
+
case 'compact':
|
|
337
|
+
return 'compact';
|
|
338
|
+
default:
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function applyNumberPrecision(options, format) {
|
|
343
|
+
const digits = parseDigitsFormat(format);
|
|
344
|
+
if (!digits)
|
|
345
|
+
return;
|
|
346
|
+
options.minimumIntegerDigits = digits.minimumIntegerDigits;
|
|
347
|
+
options.minimumFractionDigits = digits.minimumFractionDigits;
|
|
348
|
+
options.maximumFractionDigits = digits.maximumFractionDigits;
|
|
349
|
+
}
|
|
350
|
+
function applyCurrencyPrecision(options, format) {
|
|
351
|
+
if (!format)
|
|
352
|
+
return;
|
|
353
|
+
const parts = format.split('|').filter(Boolean);
|
|
354
|
+
const precision = Number(parts[2]);
|
|
355
|
+
if (Number.isFinite(precision)) {
|
|
356
|
+
options.minimumFractionDigits = precision;
|
|
357
|
+
options.maximumFractionDigits = precision;
|
|
358
|
+
}
|
|
359
|
+
if ((parts[1] || '').trim() === 'code') {
|
|
360
|
+
options.currencyDisplay = 'code';
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function parseDigitsFormat(format) {
|
|
364
|
+
if (!format)
|
|
365
|
+
return null;
|
|
366
|
+
const match = /^(\d+)\.(\d+)-(\d+)$/.exec(format.trim());
|
|
367
|
+
if (!match)
|
|
368
|
+
return null;
|
|
369
|
+
return {
|
|
370
|
+
minimumIntegerDigits: Number(match[1]),
|
|
371
|
+
minimumFractionDigits: Number(match[2]),
|
|
372
|
+
maximumFractionDigits: Number(match[3]),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
229
375
|
|
|
230
376
|
function adaptSelection(config, items) {
|
|
231
377
|
const mode = (config?.selection?.mode ?? 'none');
|
|
@@ -1122,6 +1268,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
1122
1268
|
class PraxisListSkinPreviewComponent {
|
|
1123
1269
|
skin;
|
|
1124
1270
|
static nextSkinScopeId = 0;
|
|
1271
|
+
hostLocale = inject(LOCALE_ID, { optional: true }) ?? 'en-US';
|
|
1125
1272
|
config;
|
|
1126
1273
|
items = [];
|
|
1127
1274
|
theme = 'light';
|
|
@@ -1196,30 +1343,7 @@ class PraxisListSkinPreviewComponent {
|
|
|
1196
1343
|
const def = this.config?.templating?.[slot];
|
|
1197
1344
|
if (!def)
|
|
1198
1345
|
return null;
|
|
1199
|
-
|
|
1200
|
-
const { baseExpr, param } = this.splitPipe(def.expr || '');
|
|
1201
|
-
const [codeFromExpr, localeFromExpr] = this.parseTwoParams(param);
|
|
1202
|
-
const currencyCode = codeFromExpr || this.config?.i18n?.currency || 'USD';
|
|
1203
|
-
const locale = localeFromExpr || this.config?.i18n?.locale || undefined;
|
|
1204
|
-
const rawStr = this.evalString(baseExpr, { item });
|
|
1205
|
-
const raw = Number(rawStr?.replace?.(/[^0-9.,-]/g, '').replace(',', '.') ?? rawStr);
|
|
1206
|
-
const fmt = new Intl.NumberFormat(locale, {
|
|
1207
|
-
style: 'currency',
|
|
1208
|
-
currency: currencyCode,
|
|
1209
|
-
}).format(raw ?? 0);
|
|
1210
|
-
return this.applyMaps({ type: def.type, value: fmt, class: def.class, style: def.style }, item);
|
|
1211
|
-
}
|
|
1212
|
-
if (def.type === 'date') {
|
|
1213
|
-
const { baseExpr, param } = this.splitPipe(def.expr || '');
|
|
1214
|
-
const [localeFromExpr, styleFromExpr] = this.parseTwoParams(param);
|
|
1215
|
-
const locale = localeFromExpr || this.config?.i18n?.locale || undefined;
|
|
1216
|
-
const dateStyle = this.mapDateStyle(styleFromExpr);
|
|
1217
|
-
const raw = this.evalString(baseExpr, { item });
|
|
1218
|
-
const dt = raw ? new Date(raw) : null;
|
|
1219
|
-
const fmt = dt ? new Intl.DateTimeFormat(locale, dateStyle).format(dt) : '';
|
|
1220
|
-
return this.applyMaps({ type: def.type, value: fmt, class: def.class, style: def.style }, item);
|
|
1221
|
-
}
|
|
1222
|
-
const out = evaluateTemplate(def, item);
|
|
1346
|
+
const out = evaluateTemplate(def, item, this.templateEvaluatorOptions());
|
|
1223
1347
|
return this.applyMaps(out, item);
|
|
1224
1348
|
}
|
|
1225
1349
|
applyMaps(out, item) {
|
|
@@ -1300,33 +1424,14 @@ class PraxisListSkinPreviewComponent {
|
|
|
1300
1424
|
return { baseExpr: expr.slice(0, idx).trim(), param: expr.slice(idx + 1).trim() };
|
|
1301
1425
|
}
|
|
1302
1426
|
evalString(expr, ctx) {
|
|
1303
|
-
const res = evaluateTemplate({ type: 'text', expr }, ctx?.item ?? ctx);
|
|
1427
|
+
const res = evaluateTemplate({ type: 'text', expr }, ctx?.item ?? ctx, this.templateEvaluatorOptions());
|
|
1304
1428
|
return res?.value ?? '';
|
|
1305
1429
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
const second = parts[1]?.trim();
|
|
1312
|
-
if (first === '')
|
|
1313
|
-
return [undefined, second || undefined];
|
|
1314
|
-
if (parts.length === 1)
|
|
1315
|
-
return [first || undefined, undefined];
|
|
1316
|
-
return [first || undefined, second || undefined];
|
|
1317
|
-
}
|
|
1318
|
-
mapDateStyle(style) {
|
|
1319
|
-
switch ((style || '').toLowerCase()) {
|
|
1320
|
-
case 'short':
|
|
1321
|
-
return { dateStyle: 'short' };
|
|
1322
|
-
case 'long':
|
|
1323
|
-
return { dateStyle: 'long' };
|
|
1324
|
-
case 'full':
|
|
1325
|
-
return { dateStyle: 'full' };
|
|
1326
|
-
case 'medium':
|
|
1327
|
-
default:
|
|
1328
|
-
return { dateStyle: 'medium' };
|
|
1329
|
-
}
|
|
1430
|
+
templateEvaluatorOptions() {
|
|
1431
|
+
return {
|
|
1432
|
+
i18n: this.config?.i18n,
|
|
1433
|
+
appLocale: this.hostLocale,
|
|
1434
|
+
};
|
|
1330
1435
|
}
|
|
1331
1436
|
isThemeColor(color) {
|
|
1332
1437
|
return color === 'primary' || color === 'accent' || color === 'warn';
|
|
@@ -3087,6 +3192,468 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
3087
3192
|
type: Output
|
|
3088
3193
|
}] } });
|
|
3089
3194
|
|
|
3195
|
+
const PRAXIS_LIST_EN_US = {
|
|
3196
|
+
configEdit: 'Edit configuration',
|
|
3197
|
+
expandDetails: 'Expand details',
|
|
3198
|
+
collapseDetails: 'Collapse details',
|
|
3199
|
+
itemDetails: 'Item details',
|
|
3200
|
+
emptyState: 'No items available',
|
|
3201
|
+
Data: 'Data',
|
|
3202
|
+
Actions: 'Actions',
|
|
3203
|
+
Layout: 'Layout',
|
|
3204
|
+
Content: 'Content',
|
|
3205
|
+
'i18n/A11y': 'i18n/A11y',
|
|
3206
|
+
Selection: 'Selection',
|
|
3207
|
+
Appearance: 'Appearance',
|
|
3208
|
+
'Assistant-applied adjustments replace the entire configuration object.': 'Assistant-applied adjustments replace the entire configuration object.',
|
|
3209
|
+
'applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.': 'applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.',
|
|
3210
|
+
'Resource (API)': 'Resource (API)',
|
|
3211
|
+
'e.g.: users': 'e.g.: users',
|
|
3212
|
+
'Resource endpoint (resourcePath).': 'Resource endpoint (resourcePath).',
|
|
3213
|
+
'Query (JSON)': 'Query (JSON)',
|
|
3214
|
+
'e.g.: {"status":"active","department":"sales"}': 'e.g.: {"status":"active","department":"sales"}',
|
|
3215
|
+
'Optional. Use valid JSON for initial filters.': 'Optional. Use valid JSON for initial filters.',
|
|
3216
|
+
'Sort by': 'Sort by',
|
|
3217
|
+
'Resource base field.': 'Resource base field.',
|
|
3218
|
+
Direction: 'Direction',
|
|
3219
|
+
Ascending: 'Ascending',
|
|
3220
|
+
Descending: 'Descending',
|
|
3221
|
+
'Configure per-item action buttons (icon, label, color, visibility)': 'Configure per-item action buttons (icon, label, color, visibility)',
|
|
3222
|
+
'Add action': 'Add action',
|
|
3223
|
+
'Global action (Praxis)': 'Global action (Praxis)',
|
|
3224
|
+
'-- Select --': '-- Select --',
|
|
3225
|
+
'No global action registered.': 'No global action registered.',
|
|
3226
|
+
'Select to add with a global `command`.': 'Select to add with a global `command`.',
|
|
3227
|
+
'Action type': 'Action type',
|
|
3228
|
+
Icon: 'Icon',
|
|
3229
|
+
Button: 'Button',
|
|
3230
|
+
'e.g.: edit, delete': 'e.g.: edit, delete',
|
|
3231
|
+
Label: 'Label',
|
|
3232
|
+
Variant: 'Variant',
|
|
3233
|
+
Outlined: 'Outlined',
|
|
3234
|
+
Raised: 'Raised',
|
|
3235
|
+
Filled: 'Filled',
|
|
3236
|
+
'Action color': 'Action color',
|
|
3237
|
+
'Custom color': 'Custom color',
|
|
3238
|
+
'Use custom color': 'Use custom color',
|
|
3239
|
+
'Action payload': 'Action payload',
|
|
3240
|
+
Default: 'Default',
|
|
3241
|
+
"Show when (e.g.: ${item.status} == 'done')": "Show when (e.g.: ${item.status} == 'done')",
|
|
3242
|
+
"Supported syntax: \"${item.field} == 'value'\". Advanced expressions are not evaluated.": "Supported syntax: \"${item.field} == 'value'\". Advanced expressions are not evaluated.",
|
|
3243
|
+
Action: 'Action',
|
|
3244
|
+
Preview: 'Preview',
|
|
3245
|
+
Remove: 'Remove',
|
|
3246
|
+
'Show loading': 'Show loading',
|
|
3247
|
+
Confirmation: 'Confirmation',
|
|
3248
|
+
Type: 'Type',
|
|
3249
|
+
Title: 'Title',
|
|
3250
|
+
Message: 'Message',
|
|
3251
|
+
'Confirm action': 'Confirm action',
|
|
3252
|
+
'Are you sure you want to continue?': 'Are you sure you want to continue?',
|
|
3253
|
+
'Set a title or message for the confirmation.': 'Set a title or message for the confirmation.',
|
|
3254
|
+
'Payload (JSON/Template)': 'Payload (JSON/Template)',
|
|
3255
|
+
'Invalid JSON': 'Invalid JSON',
|
|
3256
|
+
'Insert example': 'Insert example',
|
|
3257
|
+
'Emit local event too': 'Emit local event too',
|
|
3258
|
+
JSON: 'JSON',
|
|
3259
|
+
ID: 'ID',
|
|
3260
|
+
'Command (global)': 'Command (global)',
|
|
3261
|
+
Catalog: 'Catalog',
|
|
3262
|
+
Danger: 'Danger',
|
|
3263
|
+
Warning: 'Warning',
|
|
3264
|
+
Info: 'Info',
|
|
3265
|
+
'Primary (Theme)': 'Primary (Theme)',
|
|
3266
|
+
'Accent (Theme)': 'Accent (Theme)',
|
|
3267
|
+
'Warn (Theme)': 'Warn (Theme)',
|
|
3268
|
+
'M3 Primary': 'M3 Primary',
|
|
3269
|
+
'M3 Secondary': 'M3 Secondary',
|
|
3270
|
+
'M3 Tertiary': 'M3 Tertiary',
|
|
3271
|
+
'M3 Error': 'M3 Error',
|
|
3272
|
+
'M3 On Surface': 'M3 On Surface',
|
|
3273
|
+
'M3 On Surface Variant': 'M3 On Surface Variant',
|
|
3274
|
+
'e.g.: {{currency}}': 'e.g.: {{currency}}',
|
|
3275
|
+
'e.g.: 1.25rem': 'e.g.: 1.25rem',
|
|
3276
|
+
'e.g.: var(--md-sys-elevation-level2)': 'e.g.: var(--md-sys-elevation-level2)',
|
|
3277
|
+
'e.g.: 8px': 'e.g.: 8px',
|
|
3278
|
+
'e.g.: my-list-skin': 'e.g.: my-list-skin',
|
|
3279
|
+
'Example:': 'Example:',
|
|
3280
|
+
Item: 'Item',
|
|
3281
|
+
Value: 'Value',
|
|
3282
|
+
'Payload emitted by the action.': 'Payload emitted by the action.',
|
|
3283
|
+
'Modern tiles preset': 'Modern tiles preset',
|
|
3284
|
+
List: 'List',
|
|
3285
|
+
Lines: 'Lines',
|
|
3286
|
+
'Items per page': 'Items per page',
|
|
3287
|
+
Density: 'Density',
|
|
3288
|
+
Comfortable: 'Comfortable',
|
|
3289
|
+
Compact: 'Compact',
|
|
3290
|
+
'List tools': 'List tools',
|
|
3291
|
+
'Show search': 'Show search',
|
|
3292
|
+
'Show sorting': 'Show sorting',
|
|
3293
|
+
'Show total X-Y range': 'Show total X-Y range',
|
|
3294
|
+
'Field to search': 'Field to search',
|
|
3295
|
+
'Search placeholder': 'Search placeholder',
|
|
3296
|
+
'e.g.: Search by title': 'e.g.: Search by title',
|
|
3297
|
+
'Sorting options (label → field+direction)': 'Sorting options (label → field+direction)',
|
|
3298
|
+
'Add option': 'Add option',
|
|
3299
|
+
Field: 'Field',
|
|
3300
|
+
'Duplicate option (field+direction)': 'Duplicate option (field+direction)',
|
|
3301
|
+
'Primary (Title)': 'Primary (Title)',
|
|
3302
|
+
'Not mapped': 'Not mapped',
|
|
3303
|
+
'Secondary (Summary)': 'Secondary (Summary)',
|
|
3304
|
+
'Meta (Detail/Side)': 'Meta (Detail/Side)',
|
|
3305
|
+
'Composed field ({{count}})': 'Composed field ({{count}})',
|
|
3306
|
+
Features: 'Features',
|
|
3307
|
+
'Enable features': 'Enable features',
|
|
3308
|
+
'Sync with Meta': 'Sync with Meta',
|
|
3309
|
+
'Expression/Text': 'Expression/Text',
|
|
3310
|
+
'Add feature': 'Add feature',
|
|
3311
|
+
'Section header': 'Section header',
|
|
3312
|
+
'Not configured': 'Not configured',
|
|
3313
|
+
'Empty state': 'Empty state',
|
|
3314
|
+
'Apply mapping': 'Apply mapping',
|
|
3315
|
+
'Infer from schema': 'Infer from schema',
|
|
3316
|
+
'Theme preview': 'Theme preview',
|
|
3317
|
+
Light: 'Light',
|
|
3318
|
+
Dark: 'Dark',
|
|
3319
|
+
Grid: 'Grid',
|
|
3320
|
+
'Default locale': 'Default locale',
|
|
3321
|
+
'e.g.: en-US': 'e.g.: en-US',
|
|
3322
|
+
'Default currency': 'Default currency',
|
|
3323
|
+
'Sticky section header': 'Sticky section header',
|
|
3324
|
+
'Virtual scroll': 'Virtual scroll',
|
|
3325
|
+
'aria-label': 'aria-label',
|
|
3326
|
+
'aria-labelledby': 'aria-labelledby',
|
|
3327
|
+
itemClick: 'itemClick',
|
|
3328
|
+
Radius: 'Radius',
|
|
3329
|
+
Shadow: 'Shadow',
|
|
3330
|
+
Blur: 'Blur',
|
|
3331
|
+
'Extra CSS class (skin.class)': 'Extra CSS class (skin.class)',
|
|
3332
|
+
'Inline style (skin.inlineStyle)': 'Inline style (skin.inlineStyle)',
|
|
3333
|
+
'Inline style': 'Inline style',
|
|
3334
|
+
Accessibility: 'Accessibility',
|
|
3335
|
+
'High contrast': 'High contrast',
|
|
3336
|
+
'Reduce motion': 'Reduce motion',
|
|
3337
|
+
Events: 'Events',
|
|
3338
|
+
Mode: 'Mode',
|
|
3339
|
+
'No selection': 'No selection',
|
|
3340
|
+
Single: 'Single',
|
|
3341
|
+
Multiple: 'Multiple',
|
|
3342
|
+
'Form name': 'Form name',
|
|
3343
|
+
'Form path': 'Form path',
|
|
3344
|
+
'Compare by (field)': 'Compare by (field)',
|
|
3345
|
+
'Unique item key.': 'Unique item key.',
|
|
3346
|
+
Return: 'Return',
|
|
3347
|
+
'Composition mode': 'Composition mode',
|
|
3348
|
+
'Fields to compose (multi-select)': 'Fields to compose (multi-select)',
|
|
3349
|
+
Separator: 'Separator',
|
|
3350
|
+
'(Second) in parentheses': '(Second) in parentheses',
|
|
3351
|
+
'-- None --': '-- None --',
|
|
3352
|
+
'Advanced options': 'Advanced options',
|
|
3353
|
+
Position: 'Position',
|
|
3354
|
+
'Side (right)': 'Side (right)',
|
|
3355
|
+
'Inline (below)': 'Inline (below)',
|
|
3356
|
+
'Trailing (right)': 'Trailing (right)',
|
|
3357
|
+
'Status chip': 'Status chip',
|
|
3358
|
+
Price: 'Price',
|
|
3359
|
+
'URL / Expr': 'URL / Expr',
|
|
3360
|
+
'https://... or ${item.imageUrl}': 'https://... or ${item.imageUrl}',
|
|
3361
|
+
'Use an absolute/relative URL or a ${item.field} expression.': 'Use an absolute/relative URL or a ${item.field} expression.',
|
|
3362
|
+
'URL/expr is required': 'URL/expr is required',
|
|
3363
|
+
'Set the URL/expr to render the image.': 'Set the URL/expr to render the image.',
|
|
3364
|
+
Class: 'Class',
|
|
3365
|
+
Style: 'Style',
|
|
3366
|
+
'Leading (left)': 'Leading (left)',
|
|
3367
|
+
'Static icon': 'Static icon',
|
|
3368
|
+
'Static image': 'Static image',
|
|
3369
|
+
'Avatar icon': 'Avatar icon',
|
|
3370
|
+
'Avatar image + badge': 'Avatar image + badge',
|
|
3371
|
+
'Tag chip': 'Tag chip',
|
|
3372
|
+
'Use the `|iconMap` pipe in the extra pipe for dynamic rendering.': 'Use the `|iconMap` pipe in the extra pipe for dynamic rendering.',
|
|
3373
|
+
'Image URL': 'Image URL',
|
|
3374
|
+
'Alt text': 'Alt text',
|
|
3375
|
+
'Badge text': 'Badge text',
|
|
3376
|
+
'Message / Expr': 'Message / Expr',
|
|
3377
|
+
'Default message': 'Default message',
|
|
3378
|
+
'Default image': 'Default image',
|
|
3379
|
+
'No results': 'No results',
|
|
3380
|
+
'Skeleton count': 'Skeleton count',
|
|
3381
|
+
Model: 'Model',
|
|
3382
|
+
'Spacing between items': 'Spacing between items',
|
|
3383
|
+
'No extra space': 'No extra space',
|
|
3384
|
+
Tight: 'Tight',
|
|
3385
|
+
Standard: 'Standard',
|
|
3386
|
+
Relaxed: 'Relaxed',
|
|
3387
|
+
Dividers: 'Dividers',
|
|
3388
|
+
None: 'None',
|
|
3389
|
+
'Between groups': 'Between groups',
|
|
3390
|
+
All: 'All',
|
|
3391
|
+
'Group by': 'Group by',
|
|
3392
|
+
Cards: 'Cards',
|
|
3393
|
+
Tiles: 'Tiles',
|
|
3394
|
+
'Pill Soft': 'Pill Soft',
|
|
3395
|
+
'Gradient Tile': 'Gradient Tile',
|
|
3396
|
+
Glass: 'Glass',
|
|
3397
|
+
Elevated: 'Elevated',
|
|
3398
|
+
Outline: 'Outline',
|
|
3399
|
+
Flat: 'Flat',
|
|
3400
|
+
Neumorphism: 'Neumorphism',
|
|
3401
|
+
Text: 'Text',
|
|
3402
|
+
Chip: 'Chip',
|
|
3403
|
+
Rating: 'Rating',
|
|
3404
|
+
Image: 'Image',
|
|
3405
|
+
HTML: 'HTML',
|
|
3406
|
+
Currency: 'Currency',
|
|
3407
|
+
Date: 'Date',
|
|
3408
|
+
'Default text': 'Default text',
|
|
3409
|
+
'Default chip': 'Default chip',
|
|
3410
|
+
'Fields:': 'Fields:',
|
|
3411
|
+
' (Required)': ' (Required)',
|
|
3412
|
+
'Example available': 'Example available',
|
|
3413
|
+
'No schema available.': 'No schema available.',
|
|
3414
|
+
'This action may be irreversible. Do you want to continue?': 'This action may be irreversible. Do you want to continue?',
|
|
3415
|
+
'Do you want to proceed with this action?': 'Do you want to proceed with this action?',
|
|
3416
|
+
Attention: 'Attention',
|
|
3417
|
+
'CSS class example (add this to your global styles):': 'CSS class example (add this to your global styles):',
|
|
3418
|
+
};
|
|
3419
|
+
|
|
3420
|
+
const PRAXIS_LIST_PT_BR = {
|
|
3421
|
+
configEdit: 'Editar configurações',
|
|
3422
|
+
expandDetails: 'Expandir detalhes',
|
|
3423
|
+
collapseDetails: 'Recolher detalhes',
|
|
3424
|
+
itemDetails: 'Detalhes do item',
|
|
3425
|
+
emptyState: 'Nenhum item disponÃvel',
|
|
3426
|
+
Data: 'Dados',
|
|
3427
|
+
Actions: 'Ações',
|
|
3428
|
+
Layout: 'Layout',
|
|
3429
|
+
Content: 'Conteúdo',
|
|
3430
|
+
'i18n/A11y': 'i18n/A11y',
|
|
3431
|
+
Selection: 'Seleção',
|
|
3432
|
+
Appearance: 'Aparência',
|
|
3433
|
+
'Assistant-applied adjustments replace the entire configuration object.': 'Observação: ajustes aplicados pelo assistente substituem o objeto de configuração inteiro.',
|
|
3434
|
+
'applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.': 'O applyConfigFromAdapter não faz merge profundo. Garanta que o adapter envie a config completa.',
|
|
3435
|
+
'Resource (API)': 'Recurso (API)',
|
|
3436
|
+
'e.g.: users': 'ex.: users',
|
|
3437
|
+
'Resource endpoint (resourcePath).': 'Endpoint do recurso (resourcePath).',
|
|
3438
|
+
'Query (JSON)': 'Query (JSON)',
|
|
3439
|
+
'e.g.: {"status":"active","department":"sales"}': 'ex.: {"status":"active","department":"sales"}',
|
|
3440
|
+
'Optional. Use valid JSON for initial filters.': 'Opcional. Use JSON válido para filtros iniciais.',
|
|
3441
|
+
'Sort by': 'Ordenar por',
|
|
3442
|
+
'Resource base field.': 'Campo base do recurso.',
|
|
3443
|
+
Direction: 'Direção',
|
|
3444
|
+
Ascending: 'Ascendente',
|
|
3445
|
+
Descending: 'Descendente',
|
|
3446
|
+
'Configure per-item action buttons (icon, label, color, visibility)': 'Configure botões de ação por item (Ãcone, rótulo, cor, visibilidade)',
|
|
3447
|
+
'Add action': 'Adicionar ação',
|
|
3448
|
+
'Global action (Praxis)': 'Ação global (Praxis)',
|
|
3449
|
+
'-- Select --': '-- Selecionar --',
|
|
3450
|
+
'No global action registered.': 'Nenhuma ação global registrada.',
|
|
3451
|
+
'Select to add with a global `command`.': 'Selecione para adicionar com `command` global.',
|
|
3452
|
+
'Action type': 'Tipo de ação',
|
|
3453
|
+
Icon: 'Ãcone',
|
|
3454
|
+
Button: 'Botão',
|
|
3455
|
+
'e.g.: edit, delete': 'ex.: edit, delete',
|
|
3456
|
+
Label: 'Rótulo',
|
|
3457
|
+
Variant: 'Variante',
|
|
3458
|
+
Outlined: 'Contorno',
|
|
3459
|
+
Raised: 'Elevado',
|
|
3460
|
+
Filled: 'Preenchido',
|
|
3461
|
+
'Action color': 'Cor da ação',
|
|
3462
|
+
'Custom color': 'Cor personalizada',
|
|
3463
|
+
'Use custom color': 'Usar cor personalizada',
|
|
3464
|
+
'Action payload': 'Payload da ação',
|
|
3465
|
+
Default: 'Padrão',
|
|
3466
|
+
"Show when (e.g.: ${item.status} == 'done')": "Exibir quando (ex.: ${item.status} == 'done')",
|
|
3467
|
+
"Supported syntax: \"${item.field} == 'value'\". Advanced expressions are not evaluated.": 'Sintaxe suportada: "${item.field} == \'value\'". Expressões avançadas não são avaliadas.',
|
|
3468
|
+
Action: 'Ação',
|
|
3469
|
+
Preview: 'Pré-visualização',
|
|
3470
|
+
Remove: 'Remover',
|
|
3471
|
+
'Show loading': 'Mostrar loading',
|
|
3472
|
+
Confirmation: 'Confirmação',
|
|
3473
|
+
Type: 'Tipo',
|
|
3474
|
+
Title: 'TÃtulo',
|
|
3475
|
+
Message: 'Mensagem',
|
|
3476
|
+
'Confirm action': 'Confirmar ação',
|
|
3477
|
+
'Are you sure you want to continue?': 'Tem certeza que deseja continuar?',
|
|
3478
|
+
'Set a title or message for the confirmation.': 'Defina um tÃtulo ou mensagem para a confirmação.',
|
|
3479
|
+
'Payload (JSON/Template)': 'Payload (JSON/Template)',
|
|
3480
|
+
'Invalid JSON': 'JSON inválido',
|
|
3481
|
+
'Insert example': 'Inserir exemplo',
|
|
3482
|
+
'Emit local event too': 'Emitir evento local também',
|
|
3483
|
+
JSON: 'JSON',
|
|
3484
|
+
ID: 'ID',
|
|
3485
|
+
'Command (global)': 'Comando (global)',
|
|
3486
|
+
Catalog: 'Catalogo',
|
|
3487
|
+
Danger: 'Perigo',
|
|
3488
|
+
Warning: 'Aviso',
|
|
3489
|
+
Info: 'Info',
|
|
3490
|
+
'Primary (Theme)': 'Primario (Tema)',
|
|
3491
|
+
'Accent (Theme)': 'Destaque (Tema)',
|
|
3492
|
+
'Warn (Theme)': 'Aviso (Tema)',
|
|
3493
|
+
'M3 Primary': 'M3 Primario',
|
|
3494
|
+
'M3 Secondary': 'M3 Secundario',
|
|
3495
|
+
'M3 Tertiary': 'M3 Terciario',
|
|
3496
|
+
'M3 Error': 'M3 Erro',
|
|
3497
|
+
'M3 On Surface': 'M3 Na Superficie',
|
|
3498
|
+
'M3 On Surface Variant': 'M3 Variacao Na Superficie',
|
|
3499
|
+
'e.g.: {{currency}}': 'ex.: {{currency}}',
|
|
3500
|
+
'e.g.: 1.25rem': 'ex.: 1.25rem',
|
|
3501
|
+
'e.g.: var(--md-sys-elevation-level2)': 'ex.: var(--md-sys-elevation-level2)',
|
|
3502
|
+
'e.g.: 8px': 'ex.: 8px',
|
|
3503
|
+
'e.g.: my-list-skin': 'ex.: my-list-skin',
|
|
3504
|
+
'Example:': 'Exemplo:',
|
|
3505
|
+
Item: 'Item',
|
|
3506
|
+
Value: 'Valor',
|
|
3507
|
+
'Payload emitted by the action.': 'Payload emitido pela ação.',
|
|
3508
|
+
'Modern tiles preset': 'Preset Tiles Moderno',
|
|
3509
|
+
List: 'Lista',
|
|
3510
|
+
Lines: 'Linhas',
|
|
3511
|
+
'Items per page': 'Itens por página',
|
|
3512
|
+
Density: 'Densidade',
|
|
3513
|
+
Comfortable: 'Confortável',
|
|
3514
|
+
Compact: 'Compacta',
|
|
3515
|
+
'List tools': 'Ferramentas da lista',
|
|
3516
|
+
'Show search': 'Mostrar busca',
|
|
3517
|
+
'Show sorting': 'Mostrar ordenar',
|
|
3518
|
+
'Show total X-Y range': 'Mostrar faixa X-Y de Total',
|
|
3519
|
+
'Field to search': 'Campo para buscar',
|
|
3520
|
+
'Search placeholder': 'Placeholder da busca',
|
|
3521
|
+
'e.g.: Search by title': 'ex.: Buscar por tÃtulo',
|
|
3522
|
+
'Sorting options (label → field+direction)': 'Opções de ordenação (rótulo → campo+direção)',
|
|
3523
|
+
'Add option': 'Adicionar opção',
|
|
3524
|
+
Field: 'Campo',
|
|
3525
|
+
'Duplicate option (field+direction)': 'Opção duplicada (campo+direção)',
|
|
3526
|
+
'Primary (Title)': 'Primary (TÃtulo)',
|
|
3527
|
+
'Not mapped': 'Não mapeado',
|
|
3528
|
+
'Secondary (Summary)': 'Secondary (Resumo)',
|
|
3529
|
+
'Meta (Detail/Side)': 'Meta (Detalhe/Lateral)',
|
|
3530
|
+
'Composed field ({{count}})': 'Campo composto ({{count}})',
|
|
3531
|
+
Features: 'Recursos',
|
|
3532
|
+
'Enable features': 'Ativar recursos',
|
|
3533
|
+
'Sync with Meta': 'Sincronizar com Meta',
|
|
3534
|
+
'Expression/Text': 'Expr/Texto',
|
|
3535
|
+
'Add feature': 'Adicionar recurso',
|
|
3536
|
+
'Section header': 'Cabeçalho de Seção',
|
|
3537
|
+
'Not configured': 'Não configurado',
|
|
3538
|
+
'Empty state': 'Estado Vazio',
|
|
3539
|
+
'Apply mapping': 'Aplicar mapeamento',
|
|
3540
|
+
'Infer from schema': 'Inferir do schema',
|
|
3541
|
+
'Theme preview': 'Prévia de tema',
|
|
3542
|
+
Light: 'Claro',
|
|
3543
|
+
Dark: 'Escuro',
|
|
3544
|
+
Grid: 'Grade',
|
|
3545
|
+
'Default locale': 'Locale padrão',
|
|
3546
|
+
'e.g.: en-US': 'ex.: en-US',
|
|
3547
|
+
'Default currency': 'Moeda padrão',
|
|
3548
|
+
'Sticky section header': 'Cabeçalho da seção fixo',
|
|
3549
|
+
'Virtual scroll': 'Rolagem virtual',
|
|
3550
|
+
'aria-label': 'aria-label',
|
|
3551
|
+
'aria-labelledby': 'aria-labelledby',
|
|
3552
|
+
itemClick: 'itemClick',
|
|
3553
|
+
Radius: 'Raio',
|
|
3554
|
+
Shadow: 'Sombra',
|
|
3555
|
+
Blur: 'Desfoque',
|
|
3556
|
+
'Extra CSS class (skin.class)': 'Classe CSS extra (skin.class)',
|
|
3557
|
+
'Inline style (skin.inlineStyle)': 'Estilo inline (skin.inlineStyle)',
|
|
3558
|
+
'Inline style': 'Estilo inline',
|
|
3559
|
+
Accessibility: 'Acessibilidade',
|
|
3560
|
+
'High contrast': 'Alto contraste',
|
|
3561
|
+
'Reduce motion': 'Reduzir movimento',
|
|
3562
|
+
Events: 'Eventos',
|
|
3563
|
+
Mode: 'Modo',
|
|
3564
|
+
'No selection': 'Sem seleção',
|
|
3565
|
+
Single: 'Única',
|
|
3566
|
+
Multiple: 'Múltipla',
|
|
3567
|
+
'Form name': 'Nome no formulário',
|
|
3568
|
+
'Form path': 'Caminho no formulário',
|
|
3569
|
+
'Compare by (field)': 'Comparar por (campo)',
|
|
3570
|
+
'Unique item key.': 'Chave única do item.',
|
|
3571
|
+
Return: 'Retorno',
|
|
3572
|
+
'Composition mode': 'Modo de composição',
|
|
3573
|
+
'Fields to compose (multi-select)': 'Campos para compor (múltipla seleção)',
|
|
3574
|
+
Separator: 'Separador',
|
|
3575
|
+
'(Second) in parentheses': '(Segundo) entre parênteses',
|
|
3576
|
+
'-- None --': '-- Nenhum --',
|
|
3577
|
+
'Advanced options': 'Opções avançadas',
|
|
3578
|
+
Position: 'Posição',
|
|
3579
|
+
'Side (right)': 'Lateral (direita)',
|
|
3580
|
+
'Inline (below)': 'Na linha (abaixo)',
|
|
3581
|
+
'Trailing (right)': 'Trailing (direita)',
|
|
3582
|
+
'Status chip': 'Chip de status',
|
|
3583
|
+
Price: 'Preço',
|
|
3584
|
+
'URL / Expr': 'URL / Expr',
|
|
3585
|
+
'https://... or ${item.imageUrl}': 'https://... ou ${item.imageUrl}',
|
|
3586
|
+
'Use an absolute/relative URL or a ${item.field} expression.': 'Use uma URL absoluta/relativa ou uma expressão ${item.field}.',
|
|
3587
|
+
'URL/expr is required': 'URL/expr obrigatória',
|
|
3588
|
+
'Set the URL/expr to render the image.': 'Defina a URL/expr para renderizar a imagem.',
|
|
3589
|
+
Class: 'Classe',
|
|
3590
|
+
Style: 'Estilo',
|
|
3591
|
+
'Leading (left)': 'Leading (esquerda)',
|
|
3592
|
+
'Static icon': 'ÃÂcone estático',
|
|
3593
|
+
'Static image': 'Imagem estática',
|
|
3594
|
+
'Avatar icon': 'Avatar com ÃÂcone',
|
|
3595
|
+
'Avatar image + badge': 'Avatar com imagem + badge',
|
|
3596
|
+
'Tag chip': 'Chip de tag',
|
|
3597
|
+
'Use the `|iconMap` pipe in the extra pipe for dynamic rendering.': 'Use o pipe `|iconMap` no extra pipe para renderização dinâmica.',
|
|
3598
|
+
'Image URL': 'URL da imagem',
|
|
3599
|
+
'Alt text': 'Texto alternativo',
|
|
3600
|
+
'Badge text': 'Texto do badge',
|
|
3601
|
+
'Message / Expr': 'Mensagem / Expr',
|
|
3602
|
+
'Default message': 'Mensagem padrão',
|
|
3603
|
+
'Default image': 'Imagem padrão',
|
|
3604
|
+
'No results': 'Sem resultados',
|
|
3605
|
+
'Skeleton count': 'Quantidade de skeleton',
|
|
3606
|
+
Model: 'Modelo',
|
|
3607
|
+
'Spacing between items': 'Espaçamento entre itens',
|
|
3608
|
+
'No extra space': 'Sem espaço extra',
|
|
3609
|
+
Tight: 'Ajustado',
|
|
3610
|
+
Standard: 'Padrão',
|
|
3611
|
+
Relaxed: 'Relaxado',
|
|
3612
|
+
Dividers: 'Divisores',
|
|
3613
|
+
None: 'Nenhum',
|
|
3614
|
+
'Between groups': 'Entre grupos',
|
|
3615
|
+
All: 'Todos',
|
|
3616
|
+
'Group by': 'Agrupar por',
|
|
3617
|
+
Cards: 'Cards',
|
|
3618
|
+
Tiles: 'Tiles',
|
|
3619
|
+
'Pill Soft': 'Pill Soft',
|
|
3620
|
+
'Gradient Tile': 'Tile com degradê',
|
|
3621
|
+
Glass: 'Vidro',
|
|
3622
|
+
Elevated: 'Elevado',
|
|
3623
|
+
Outline: 'Contorno',
|
|
3624
|
+
Flat: 'Plano',
|
|
3625
|
+
Neumorphism: 'Neumorfismo',
|
|
3626
|
+
Text: 'Texto',
|
|
3627
|
+
Chip: 'Chip',
|
|
3628
|
+
Rating: 'Rating',
|
|
3629
|
+
Image: 'Imagem',
|
|
3630
|
+
HTML: 'HTML',
|
|
3631
|
+
Currency: 'Moeda',
|
|
3632
|
+
Date: 'Data',
|
|
3633
|
+
'Default text': 'Texto padrão',
|
|
3634
|
+
'Default chip': 'Chip padrão',
|
|
3635
|
+
'Fields:': 'Campos:',
|
|
3636
|
+
' (Required)': ' (Obrigatório)',
|
|
3637
|
+
'Example available': 'Exemplo disponÃÂvel',
|
|
3638
|
+
'No schema available.': 'Nenhum schema disponÃÂvel.',
|
|
3639
|
+
'This action may be irreversible. Do you want to continue?': 'Esta ação pode ser irreversÃÂvel. Deseja continuar?',
|
|
3640
|
+
'Do you want to proceed with this action?': 'Deseja prosseguir com esta ação?',
|
|
3641
|
+
Attention: 'Atenção',
|
|
3642
|
+
'CSS class example (add this to your global styles):': 'Exemplo de classe CSS (adicione isso aos estilos globais):',
|
|
3643
|
+
};
|
|
3644
|
+
|
|
3645
|
+
const PRAXIS_LIST_I18N_NAMESPACE = 'praxis-list';
|
|
3646
|
+
function providePraxisListI18n() {
|
|
3647
|
+
return providePraxisI18n({
|
|
3648
|
+
namespaces: {
|
|
3649
|
+
[PRAXIS_LIST_I18N_NAMESPACE]: {
|
|
3650
|
+
'pt-BR': PRAXIS_LIST_PT_BR,
|
|
3651
|
+
'en-US': PRAXIS_LIST_EN_US,
|
|
3652
|
+
},
|
|
3653
|
+
},
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3090
3657
|
const SKIN_PRESETS = [
|
|
3091
3658
|
'pill-soft',
|
|
3092
3659
|
'gradient-tile',
|
|
@@ -3132,7 +3699,7 @@ class PraxisListConfigEditor {
|
|
|
3132
3699
|
mappingTrailing = {};
|
|
3133
3700
|
mappingLeading = { type: 'icon' };
|
|
3134
3701
|
mappingSectionHeader = { type: 'text', expr: '${item.key}' };
|
|
3135
|
-
mappingEmptyState = { type: 'text', expr: '
|
|
3702
|
+
mappingEmptyState = { type: 'text', expr: '' };
|
|
3136
3703
|
mapping = {};
|
|
3137
3704
|
mappingDirty = false;
|
|
3138
3705
|
appliedMappingSnapshot = '';
|
|
@@ -3146,7 +3713,7 @@ class PraxisListConfigEditor {
|
|
|
3146
3713
|
uiSortRows = [];
|
|
3147
3714
|
// Meta composition (multi-field)
|
|
3148
3715
|
mappingMetaFields = [];
|
|
3149
|
-
mappingMetaSeparator = '
|
|
3716
|
+
mappingMetaSeparator = ' • ';
|
|
3150
3717
|
mappingMetaWrapSecondInParens = false;
|
|
3151
3718
|
featuresSyncWithMeta = false;
|
|
3152
3719
|
// Features editor state
|
|
@@ -3157,6 +3724,7 @@ class PraxisListConfigEditor {
|
|
|
3157
3724
|
queryJson = '';
|
|
3158
3725
|
queryError = '';
|
|
3159
3726
|
crud = null;
|
|
3727
|
+
i18n = inject(PraxisI18nService);
|
|
3160
3728
|
resourcePathChanges = new Subject();
|
|
3161
3729
|
constructor(injected) {
|
|
3162
3730
|
// Disable Immer auto-freeze to allow [(ngModel)] mutations on produced state
|
|
@@ -3167,6 +3735,9 @@ class PraxisListConfigEditor {
|
|
|
3167
3735
|
});
|
|
3168
3736
|
const incoming = injected?.document ?? injected?.config ?? this.config;
|
|
3169
3737
|
this.applyIncomingDocument(incoming);
|
|
3738
|
+
if (!this.mappingEmptyState.expr) {
|
|
3739
|
+
this.mappingEmptyState.expr = this.tx('No items available');
|
|
3740
|
+
}
|
|
3170
3741
|
this.globalActionCatalog = this.resolveGlobalActionCatalog();
|
|
3171
3742
|
this.appliedMappingSnapshot = this.getMappingSnapshot();
|
|
3172
3743
|
this.initialJson = this.snapshotCurrentDocument();
|
|
@@ -3176,6 +3747,22 @@ class PraxisListConfigEditor {
|
|
|
3176
3747
|
this.resourcePathChanges.next(this.working?.dataSource?.resourcePath || '');
|
|
3177
3748
|
}
|
|
3178
3749
|
ip = inject(IconPickerService);
|
|
3750
|
+
tx(text, params) {
|
|
3751
|
+
return this.i18n.t(text, params, text, PRAXIS_LIST_I18N_NAMESPACE);
|
|
3752
|
+
}
|
|
3753
|
+
queryPlaceholder() {
|
|
3754
|
+
return this.tx('e.g.: {"status":"active","department":"sales"}');
|
|
3755
|
+
}
|
|
3756
|
+
currencyPlaceholder() {
|
|
3757
|
+
const currency = this.defaultPresetCurrency(this.defaultPresetLocale());
|
|
3758
|
+
return this.tx('e.g.: {{currency}}', { currency });
|
|
3759
|
+
}
|
|
3760
|
+
actionVisibilityLabel() {
|
|
3761
|
+
return this.tx("Show when (e.g.: ${item.status} == 'done')");
|
|
3762
|
+
}
|
|
3763
|
+
actionConditionTooltip() {
|
|
3764
|
+
return this.tx(`Supported syntax: "\${item.field} == 'value'". Advanced expressions are not evaluated.`);
|
|
3765
|
+
}
|
|
3179
3766
|
async pickLeadingIcon() {
|
|
3180
3767
|
const chosen = await this.ip.openDialog({
|
|
3181
3768
|
value: this.mappingLeading?.icon,
|
|
@@ -3281,11 +3868,11 @@ class PraxisListConfigEditor {
|
|
|
3281
3868
|
this.mappingSectionHeader = { type: 'text', expr: '${item.key}' };
|
|
3282
3869
|
this.mappingEmptyState = {
|
|
3283
3870
|
type: 'text',
|
|
3284
|
-
expr: '
|
|
3871
|
+
expr: this.tx('No items available'),
|
|
3285
3872
|
};
|
|
3286
3873
|
this.mappingMetaPrefixIcon = undefined;
|
|
3287
3874
|
this.mappingMetaFields = [];
|
|
3288
|
-
this.mappingMetaSeparator = '
|
|
3875
|
+
this.mappingMetaSeparator = ' • ';
|
|
3289
3876
|
this.mappingMetaWrapSecondInParens = false;
|
|
3290
3877
|
this.features = [];
|
|
3291
3878
|
this.featuresVisible = true;
|
|
@@ -3411,6 +3998,8 @@ class PraxisListConfigEditor {
|
|
|
3411
3998
|
applyLayoutPreset(preset) {
|
|
3412
3999
|
if (preset === 'tiles-modern') {
|
|
3413
4000
|
const currentClass = this.working?.skin?.class;
|
|
4001
|
+
const locale = this.defaultPresetLocale();
|
|
4002
|
+
const currency = this.defaultPresetCurrency(locale);
|
|
3414
4003
|
this.working = produce(this.working, (draft) => {
|
|
3415
4004
|
draft.layout = {
|
|
3416
4005
|
...(draft.layout || {}),
|
|
@@ -3433,27 +4022,26 @@ class PraxisListConfigEditor {
|
|
|
3433
4022
|
};
|
|
3434
4023
|
draft.i18n = {
|
|
3435
4024
|
...(draft.i18n || {}),
|
|
3436
|
-
locale
|
|
3437
|
-
currency
|
|
4025
|
+
locale,
|
|
4026
|
+
currency,
|
|
3438
4027
|
};
|
|
3439
4028
|
draft.templating = {
|
|
3440
4029
|
...(draft.templating || {}),
|
|
3441
4030
|
leading: {
|
|
3442
4031
|
type: 'image',
|
|
3443
4032
|
expr: '${item.imageUrl}',
|
|
3444
|
-
imageAlt: 'Catalog',
|
|
4033
|
+
imageAlt: this.tx('Catalog'),
|
|
3445
4034
|
},
|
|
3446
4035
|
primary: { type: 'text', expr: '${item.name}' },
|
|
3447
4036
|
secondary: { type: 'text', expr: '${item.subtitle}' },
|
|
3448
|
-
meta: {
|
|
4037
|
+
meta: {
|
|
4038
|
+
type: 'currency',
|
|
4039
|
+
expr: '${item.price}|' + currency + ':' + locale,
|
|
4040
|
+
},
|
|
3449
4041
|
trailing: { type: 'chip', expr: '${item.status}' },
|
|
3450
4042
|
metaPlacement: 'side',
|
|
3451
4043
|
statusPosition: 'top-right',
|
|
3452
|
-
chipColorMap:
|
|
3453
|
-
disponível: 'primary',
|
|
3454
|
-
'últimas vagas': 'accent',
|
|
3455
|
-
indisponível: 'warn',
|
|
3456
|
-
},
|
|
4044
|
+
chipColorMap: this.defaultTilesModernChipColorMap(locale),
|
|
3457
4045
|
};
|
|
3458
4046
|
});
|
|
3459
4047
|
this.hydrateMappingFromTemplating(this.working.templating);
|
|
@@ -3462,6 +4050,67 @@ class PraxisListConfigEditor {
|
|
|
3462
4050
|
this.verify();
|
|
3463
4051
|
}
|
|
3464
4052
|
}
|
|
4053
|
+
defaultPresetLocale() {
|
|
4054
|
+
const current = this.working?.i18n?.locale?.trim();
|
|
4055
|
+
if (current)
|
|
4056
|
+
return current;
|
|
4057
|
+
const serviceLocale = this.i18n.getLocale()?.trim();
|
|
4058
|
+
return serviceLocale || 'en-US';
|
|
4059
|
+
}
|
|
4060
|
+
defaultPresetCurrency(locale) {
|
|
4061
|
+
const current = this.working?.i18n?.currency?.trim();
|
|
4062
|
+
if (current)
|
|
4063
|
+
return current;
|
|
4064
|
+
return locale.toLowerCase().startsWith('pt') ? 'BRL' : 'USD';
|
|
4065
|
+
}
|
|
4066
|
+
defaultTilesModernChipColorMap(locale) {
|
|
4067
|
+
const normalized = (locale || '').toLowerCase();
|
|
4068
|
+
if (normalized.startsWith('pt')) {
|
|
4069
|
+
return {
|
|
4070
|
+
disponivel: 'primary',
|
|
4071
|
+
'ultimas vagas': 'accent',
|
|
4072
|
+
indisponivel: 'warn',
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
return {
|
|
4076
|
+
Available: 'primary',
|
|
4077
|
+
'Last spots': 'accent',
|
|
4078
|
+
Unavailable: 'warn',
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
applySalaryPreset() {
|
|
4082
|
+
this.applyCurrencyPreset(this.mappingSecondary, 'salary');
|
|
4083
|
+
}
|
|
4084
|
+
applyPricePreset() {
|
|
4085
|
+
this.applyCurrencyPreset(this.mappingTrailing, 'price');
|
|
4086
|
+
}
|
|
4087
|
+
applyCurrencyPreset(target, field) {
|
|
4088
|
+
const locale = this.defaultPresetLocale();
|
|
4089
|
+
target.type = 'currency';
|
|
4090
|
+
target.field = field;
|
|
4091
|
+
target.currencyCode = this.defaultPresetCurrency(locale);
|
|
4092
|
+
target.locale = locale;
|
|
4093
|
+
this.onMappingChanged();
|
|
4094
|
+
}
|
|
4095
|
+
defaultStatusColorMapEntries() {
|
|
4096
|
+
const locale = this.defaultPresetLocale().toLowerCase();
|
|
4097
|
+
if (locale.startsWith('pt')) {
|
|
4098
|
+
return [
|
|
4099
|
+
{ key: 'PLANEJADA', color: 'primary' },
|
|
4100
|
+
{ key: 'EM_ANDAMENTO', color: 'accent' },
|
|
4101
|
+
{ key: 'PAUSADA', color: 'accent' },
|
|
4102
|
+
{ key: 'CONCLUIDA', color: 'primary' },
|
|
4103
|
+
{ key: 'FALHOU', color: 'warn' },
|
|
4104
|
+
];
|
|
4105
|
+
}
|
|
4106
|
+
return [
|
|
4107
|
+
{ key: 'PLANNED', color: 'primary' },
|
|
4108
|
+
{ key: 'IN_PROGRESS', color: 'accent' },
|
|
4109
|
+
{ key: 'PAUSED', color: 'accent' },
|
|
4110
|
+
{ key: 'COMPLETED', color: 'primary' },
|
|
4111
|
+
{ key: 'FAILED', color: 'warn' },
|
|
4112
|
+
];
|
|
4113
|
+
}
|
|
3465
4114
|
previewData = [
|
|
3466
4115
|
{
|
|
3467
4116
|
title: 'Maria',
|
|
@@ -3471,19 +4120,19 @@ class PraxisListConfigEditor {
|
|
|
3471
4120
|
hireDate: '2022-03-01',
|
|
3472
4121
|
rating: 4.5,
|
|
3473
4122
|
name: 'Aurora Deluxe',
|
|
3474
|
-
status: '
|
|
4123
|
+
status: 'Available',
|
|
3475
4124
|
price: 980.0,
|
|
3476
4125
|
imageUrl: '/list-demo-hotel-1.svg',
|
|
3477
4126
|
},
|
|
3478
4127
|
{
|
|
3479
|
-
title: '
|
|
4128
|
+
title: 'João',
|
|
3480
4129
|
subtitle: 'Frontend Engineer',
|
|
3481
4130
|
tag: 'B',
|
|
3482
4131
|
salary: 9876.0,
|
|
3483
4132
|
hireDate: '2023-01-12',
|
|
3484
4133
|
rating: 3.8,
|
|
3485
4134
|
name: 'Skyline Classic',
|
|
3486
|
-
status: '
|
|
4135
|
+
status: 'Last spots',
|
|
3487
4136
|
price: 640.0,
|
|
3488
4137
|
imageUrl: '/list-demo-hotel-2.svg',
|
|
3489
4138
|
},
|
|
@@ -3495,14 +4144,14 @@ class PraxisListConfigEditor {
|
|
|
3495
4144
|
hireDate: '2021-10-05',
|
|
3496
4145
|
rating: 4.9,
|
|
3497
4146
|
name: 'Urban Loft',
|
|
3498
|
-
status: '
|
|
4147
|
+
status: 'Unavailable',
|
|
3499
4148
|
price: 520.0,
|
|
3500
4149
|
imageUrl: '/list-demo-hotel-1.svg',
|
|
3501
4150
|
},
|
|
3502
4151
|
];
|
|
3503
4152
|
skinPreviewTheme = 'light';
|
|
3504
4153
|
metaTypeConfigs = [
|
|
3505
|
-
{ type: 'text', label: '
|
|
4154
|
+
{ type: 'text', label: 'Text', icon: 'text_fields' },
|
|
3506
4155
|
{
|
|
3507
4156
|
type: 'chip',
|
|
3508
4157
|
label: 'Chip',
|
|
@@ -3517,14 +4166,14 @@ class PraxisListConfigEditor {
|
|
|
3517
4166
|
},
|
|
3518
4167
|
{
|
|
3519
4168
|
type: 'icon',
|
|
3520
|
-
label: '
|
|
4169
|
+
label: 'Icon',
|
|
3521
4170
|
icon: 'emoji_symbols',
|
|
3522
4171
|
props: ['iconColor'],
|
|
3523
4172
|
},
|
|
3524
|
-
{ type: 'image', label: '
|
|
4173
|
+
{ type: 'image', label: 'Image', icon: 'image', props: ['imageAlt'] },
|
|
3525
4174
|
{ type: 'html', label: 'HTML', icon: 'code' },
|
|
3526
|
-
{ type: 'currency', label: '
|
|
3527
|
-
{ type: 'date', label: '
|
|
4175
|
+
{ type: 'currency', label: 'Currency', icon: 'attach_money' },
|
|
4176
|
+
{ type: 'date', label: 'Date', icon: 'calendar_today' },
|
|
3528
4177
|
];
|
|
3529
4178
|
primaryTypeConfigs = this.metaTypeConfigs.filter((t) => ['text', 'currency', 'date', 'html'].includes(t.type));
|
|
3530
4179
|
secondaryTypeConfigs = this.primaryTypeConfigs;
|
|
@@ -3541,21 +4190,23 @@ class PraxisListConfigEditor {
|
|
|
3541
4190
|
].includes(t.type));
|
|
3542
4191
|
sectionHeaderTypeConfigs = this.metaTypeConfigs.filter((t) => ['text', 'chip', 'icon', 'image', 'rating', 'html'].includes(t.type));
|
|
3543
4192
|
emptyStateTypeConfigs = this.metaTypeConfigs.filter((t) => ['text', 'chip', 'icon', 'image', 'rating', 'html'].includes(t.type));
|
|
3544
|
-
paletteOptions
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
4193
|
+
get paletteOptions() {
|
|
4194
|
+
return [
|
|
4195
|
+
{ value: undefined, label: this.tx('Default') },
|
|
4196
|
+
{ value: 'primary', label: this.tx('Primary (Theme)') },
|
|
4197
|
+
{ value: 'accent', label: this.tx('Accent (Theme)') },
|
|
4198
|
+
{ value: 'warn', label: this.tx('Warn (Theme)') },
|
|
4199
|
+
{ value: 'var(--md-sys-color-primary)', label: this.tx('M3 Primary') },
|
|
4200
|
+
{ value: 'var(--md-sys-color-secondary)', label: this.tx('M3 Secondary') },
|
|
4201
|
+
{ value: 'var(--md-sys-color-tertiary)', label: this.tx('M3 Tertiary') },
|
|
4202
|
+
{ value: 'var(--md-sys-color-error)', label: this.tx('M3 Error') },
|
|
4203
|
+
{ value: 'var(--md-sys-color-on-surface)', label: this.tx('M3 On Surface') },
|
|
4204
|
+
{
|
|
4205
|
+
value: 'var(--md-sys-color-on-surface-variant)',
|
|
4206
|
+
label: this.tx('M3 On Surface Variant'),
|
|
4207
|
+
},
|
|
4208
|
+
];
|
|
4209
|
+
}
|
|
3559
4210
|
isThemeColor(value) {
|
|
3560
4211
|
return value === 'primary' || value === 'accent' || value === 'warn';
|
|
3561
4212
|
}
|
|
@@ -3608,7 +4259,7 @@ class PraxisListConfigEditor {
|
|
|
3608
4259
|
draft.actions.push({
|
|
3609
4260
|
id: 'newAction',
|
|
3610
4261
|
icon: 'bolt',
|
|
3611
|
-
label: '
|
|
4262
|
+
label: this.tx('Action'),
|
|
3612
4263
|
color: undefined,
|
|
3613
4264
|
kind: 'icon',
|
|
3614
4265
|
});
|
|
@@ -3649,22 +4300,21 @@ class PraxisListConfigEditor {
|
|
|
3649
4300
|
action.confirmation.type = normalized || undefined;
|
|
3650
4301
|
if (normalized === 'danger') {
|
|
3651
4302
|
if (!action.confirmation.title)
|
|
3652
|
-
action.confirmation.title = '
|
|
4303
|
+
action.confirmation.title = this.tx('Confirm action');
|
|
3653
4304
|
if (!action.confirmation.message)
|
|
3654
|
-
action.confirmation.message =
|
|
3655
|
-
'Esta ação pode ser irreversível. Deseja continuar?';
|
|
4305
|
+
action.confirmation.message = this.tx('This action may be irreversible. Do you want to continue?');
|
|
3656
4306
|
}
|
|
3657
4307
|
if (normalized === 'warning') {
|
|
3658
4308
|
if (!action.confirmation.title)
|
|
3659
|
-
action.confirmation.title = '
|
|
4309
|
+
action.confirmation.title = this.tx('Attention');
|
|
3660
4310
|
if (!action.confirmation.message)
|
|
3661
|
-
action.confirmation.message = '
|
|
4311
|
+
action.confirmation.message = this.tx('Do you want to proceed with this action?');
|
|
3662
4312
|
}
|
|
3663
4313
|
if (normalized === 'info') {
|
|
3664
4314
|
if (!action.confirmation.title)
|
|
3665
|
-
action.confirmation.title = '
|
|
4315
|
+
action.confirmation.title = this.tx('Confirmation');
|
|
3666
4316
|
if (!action.confirmation.message)
|
|
3667
|
-
action.confirmation.message = '
|
|
4317
|
+
action.confirmation.message = this.tx('Are you sure you want to continue?');
|
|
3668
4318
|
}
|
|
3669
4319
|
this.onActionsChanged();
|
|
3670
4320
|
}
|
|
@@ -3701,16 +4351,16 @@ class PraxisListConfigEditor {
|
|
|
3701
4351
|
return '';
|
|
3702
4352
|
const cmd = action.command.replace(/^global[:.]/, '').trim();
|
|
3703
4353
|
const entry = this.globalActionCatalog.find((e) => e.id === cmd);
|
|
3704
|
-
return entry?.payloadSchema?.example ? '
|
|
4354
|
+
return entry?.payloadSchema?.example ? this.tx('Example available') : '';
|
|
3705
4355
|
}
|
|
3706
4356
|
globalPayloadSchemaTooltip(action) {
|
|
3707
4357
|
if (!action?.command)
|
|
3708
|
-
return '
|
|
4358
|
+
return this.tx('No schema available.');
|
|
3709
4359
|
const cmd = action.command.replace(/^global[:.]/, '').trim();
|
|
3710
4360
|
const entry = this.globalActionCatalog.find((e) => e.id === cmd);
|
|
3711
4361
|
const schema = entry?.payloadSchema;
|
|
3712
4362
|
if (!schema)
|
|
3713
|
-
return '
|
|
4363
|
+
return this.tx('No schema available.');
|
|
3714
4364
|
const lines = [];
|
|
3715
4365
|
if (entry?.label)
|
|
3716
4366
|
lines.push(`${entry.label}`);
|
|
@@ -3719,11 +4369,11 @@ class PraxisListConfigEditor {
|
|
|
3719
4369
|
const required = new Set((schema.required || []).map((r) => String(r)));
|
|
3720
4370
|
const props = schema.properties ? Object.entries(schema.properties) : [];
|
|
3721
4371
|
if (props.length) {
|
|
3722
|
-
lines.push('
|
|
4372
|
+
lines.push(this.tx('Fields:'));
|
|
3723
4373
|
for (const [name, meta] of props) {
|
|
3724
|
-
const req = required.has(name) ? ' (
|
|
4374
|
+
const req = required.has(name) ? this.tx(' (Required)') : '';
|
|
3725
4375
|
const type = meta?.type ? `: ${meta.type}` : '';
|
|
3726
|
-
const desc = meta?.description ? `
|
|
4376
|
+
const desc = meta?.description ? ` — ${meta.description}` : '';
|
|
3727
4377
|
lines.push(`- ${name}${type}${req}${desc}`);
|
|
3728
4378
|
}
|
|
3729
4379
|
}
|
|
@@ -3731,11 +4381,34 @@ class PraxisListConfigEditor {
|
|
|
3731
4381
|
const example = typeof schema.example === 'object'
|
|
3732
4382
|
? JSON.stringify(schema.example, null, 2)
|
|
3733
4383
|
: String(schema.example);
|
|
3734
|
-
lines.push('
|
|
4384
|
+
lines.push(this.tx('Example:'));
|
|
3735
4385
|
lines.push(example);
|
|
3736
4386
|
}
|
|
3737
4387
|
return lines.join('\n');
|
|
3738
4388
|
}
|
|
4389
|
+
isSurfaceOpenCommand(action) {
|
|
4390
|
+
return this.getGlobalActionSchema(action)?.editorMode === 'surface-open';
|
|
4391
|
+
}
|
|
4392
|
+
getSurfaceOpenGlobalPayload(action) {
|
|
4393
|
+
const raw = String(action?.globalPayload || '').trim();
|
|
4394
|
+
if (raw) {
|
|
4395
|
+
try {
|
|
4396
|
+
const parsed = JSON.parse(raw);
|
|
4397
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
4398
|
+
return this.normalizeSurfaceOpenPayload(parsed);
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
catch {
|
|
4402
|
+
// keep default payload while authoring invalid JSON elsewhere
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
return this.normalizeSurfaceOpenPayload(undefined);
|
|
4406
|
+
}
|
|
4407
|
+
onSurfaceOpenGlobalPayloadChange(action, payload) {
|
|
4408
|
+
action.command = 'global:surface.open';
|
|
4409
|
+
action.globalPayload = JSON.stringify(this.normalizeSurfaceOpenPayload(payload));
|
|
4410
|
+
this.onActionsChanged();
|
|
4411
|
+
}
|
|
3739
4412
|
onGlobalActionSelected(id) {
|
|
3740
4413
|
const action = this.globalActionCatalog.find((a) => a.id === id);
|
|
3741
4414
|
if (!action)
|
|
@@ -3856,7 +4529,7 @@ class PraxisListConfigEditor {
|
|
|
3856
4529
|
this.verify();
|
|
3857
4530
|
}
|
|
3858
4531
|
catch {
|
|
3859
|
-
this.queryError = 'JSON
|
|
4532
|
+
this.queryError = this.tx('Invalid JSON');
|
|
3860
4533
|
this.verify();
|
|
3861
4534
|
}
|
|
3862
4535
|
}
|
|
@@ -4165,7 +4838,7 @@ class PraxisListConfigEditor {
|
|
|
4165
4838
|
t.primary = p;
|
|
4166
4839
|
if (s) {
|
|
4167
4840
|
t.secondary = s;
|
|
4168
|
-
// Garantir que a UI tenha
|
|
4841
|
+
// Garantir que a UI tenha espaço para exibir o secondary
|
|
4169
4842
|
const currentLines = Number(this.working.layout.lines) || 1;
|
|
4170
4843
|
if (currentLines < 2)
|
|
4171
4844
|
this.working.layout.lines = 2;
|
|
@@ -4564,8 +5237,8 @@ class PraxisListConfigEditor {
|
|
|
4564
5237
|
// Try to detect composed text: split by common separators if it contains ${item.
|
|
4565
5238
|
if (typeof templ.meta?.expr === 'string' &&
|
|
4566
5239
|
/\$\{\s*item\./.test(templ.meta.expr)) {
|
|
4567
|
-
const guessSep = /
|
|
4568
|
-
? '
|
|
5240
|
+
const guessSep = / • /.test(templ.meta.expr)
|
|
5241
|
+
? ' • '
|
|
4569
5242
|
: /,\s*/.test(templ.meta.expr)
|
|
4570
5243
|
? ', '
|
|
4571
5244
|
: ' ';
|
|
@@ -4625,13 +5298,7 @@ class PraxisListConfigEditor {
|
|
|
4625
5298
|
}));
|
|
4626
5299
|
if ((this.mappingTrailing?.field || '').toLowerCase() === 'status' &&
|
|
4627
5300
|
(!this.iconColorMapEntries || this.iconColorMapEntries.length === 0)) {
|
|
4628
|
-
this.iconColorMapEntries =
|
|
4629
|
-
{ key: 'PLANEJADA', color: 'primary' },
|
|
4630
|
-
{ key: 'EM_ANDAMENTO', color: 'accent' },
|
|
4631
|
-
{ key: 'PAUSADA', color: 'accent' },
|
|
4632
|
-
{ key: 'CONCLUIDA', color: 'primary' },
|
|
4633
|
-
{ key: 'FALHOU', color: 'warn' },
|
|
4634
|
-
];
|
|
5301
|
+
this.iconColorMapEntries = this.defaultStatusColorMapEntries();
|
|
4635
5302
|
}
|
|
4636
5303
|
if (templ.leading?.type === 'icon' &&
|
|
4637
5304
|
typeof templ.leading.expr === 'string') {
|
|
@@ -4808,7 +5475,51 @@ class PraxisListConfigEditor {
|
|
|
4808
5475
|
const injected = inject(GLOBAL_ACTION_CATALOG, { optional: true });
|
|
4809
5476
|
if (!injected)
|
|
4810
5477
|
return PRAXIS_GLOBAL_ACTION_CATALOG.slice();
|
|
4811
|
-
return getGlobalActionCatalog(injected)
|
|
5478
|
+
return getGlobalActionCatalog(injected).map((entry) => ({
|
|
5479
|
+
...entry,
|
|
5480
|
+
label: this.getGlobalActionEntryLabel(entry),
|
|
5481
|
+
description: this.getGlobalActionEntryDescription(entry),
|
|
5482
|
+
}));
|
|
5483
|
+
}
|
|
5484
|
+
getGlobalActionEntryLabel(entry) {
|
|
5485
|
+
if (entry?.id === 'surface.open') {
|
|
5486
|
+
return this.i18n.t('action.surfaceOpen.label', undefined, entry.label || '', SURFACE_OPEN_I18N_NAMESPACE);
|
|
5487
|
+
}
|
|
5488
|
+
return entry?.label || '';
|
|
5489
|
+
}
|
|
5490
|
+
getGlobalActionEntryDescription(entry) {
|
|
5491
|
+
if (entry?.id === 'surface.open') {
|
|
5492
|
+
return this.i18n.t('action.surfaceOpen.description', undefined, entry.description || '', SURFACE_OPEN_I18N_NAMESPACE);
|
|
5493
|
+
}
|
|
5494
|
+
return entry?.description || '';
|
|
5495
|
+
}
|
|
5496
|
+
getGlobalActionSchema(action) {
|
|
5497
|
+
const id = String(action?.command || '').replace(/^global[:.]/, '').trim();
|
|
5498
|
+
return getGlobalActionUiSchema(id);
|
|
5499
|
+
}
|
|
5500
|
+
normalizeSurfaceOpenPayload(payload) {
|
|
5501
|
+
return {
|
|
5502
|
+
presentation: payload?.presentation === 'drawer' ? 'drawer' : 'modal',
|
|
5503
|
+
title: payload?.title,
|
|
5504
|
+
subtitle: payload?.subtitle,
|
|
5505
|
+
icon: payload?.icon,
|
|
5506
|
+
size: payload?.size && Object.keys(payload.size).length
|
|
5507
|
+
? { ...payload.size }
|
|
5508
|
+
: undefined,
|
|
5509
|
+
widget: {
|
|
5510
|
+
id: String(payload?.widget?.id || ''),
|
|
5511
|
+
inputs: { ...(payload?.widget?.inputs || {}) },
|
|
5512
|
+
bindingOrder: payload?.widget?.bindingOrder?.length
|
|
5513
|
+
? [...payload.widget.bindingOrder]
|
|
5514
|
+
: undefined,
|
|
5515
|
+
},
|
|
5516
|
+
bindings: payload?.bindings?.length
|
|
5517
|
+
? payload.bindings.map((binding) => ({ ...binding }))
|
|
5518
|
+
: [],
|
|
5519
|
+
context: payload?.context && typeof payload.context === 'object' && !Array.isArray(payload.context)
|
|
5520
|
+
? { ...payload.context }
|
|
5521
|
+
: undefined,
|
|
5522
|
+
};
|
|
4812
5523
|
}
|
|
4813
5524
|
verify() {
|
|
4814
5525
|
setTimeout(() => {
|
|
@@ -4981,11 +5692,17 @@ class PraxisListConfigEditor {
|
|
|
4981
5692
|
}
|
|
4982
5693
|
}
|
|
4983
5694
|
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 });
|
|
4984
|
-
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\">\n Observa\u00E7\u00E3o: ajustes aplicados pelo assistente substituem o objeto de\n configura\u00E7\u00E3o inteiro.\n </div>\n <button\n mat-icon-button\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"O applyConfigFromAdapter n\u00E3o faz merge profundo. Garanta que o adapter envie a config completa.\"\n >\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\n matInput\n [(ngModel)]=\"working.dataSource.resourcePath\"\n (ngModelChange)=\"onResourcePathChange($event)\"\n placeholder=\"ex.: users\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Endpoint do recurso (resourcePath).\"\n >\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\n matInput\n rows=\"3\"\n [(ngModel)]=\"queryJson\"\n (ngModelChange)=\"onQueryChanged($event)\"\n placeholder='ex.: {\"status\":\"active\",\"department\":\"sales\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Opcional. Use JSON v\u00E1lido para filtros iniciais.\"\n *ngIf=\"!queryError\"\n >\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\n [(ngModel)]=\"sortField\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Campo base do recurso.\"\n >\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\n [(ngModel)]=\"sortDir\"\n (ngModelChange)=\"updateSortConfig()\"\n >\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 >\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\">\n Configure bot\u00F5es de a\u00E7\u00E3o por item (\u00EDcone, r\u00F3tulo, cor, visibilidade)\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addAction()\">\n Adicionar a\u00E7\u00E3o\n </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\n [(ngModel)]=\"selectedGlobalActionId\"\n (ngModelChange)=\"onGlobalActionSelected($event)\"\n >\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\n *ngIf=\"!globalActionCatalog.length\"\n class=\"text-caption muted\"\n >Nenhuma a\u00E7\u00E3o global registrada.</mat-hint\n >\n </mat-form-field>\n <div class=\"muted text-caption\">\n Selecione para adicionar com `command` global.\n </div>\n </div>\n <div\n *ngFor=\"let a of working.actions || []; let i = index\"\n class=\"g g-auto-200 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>ID</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.id\"\n (ngModelChange)=\"onActionsChanged()\"\n />\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\n matInput\n [(ngModel)]=\"a.icon\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder=\"ex.: edit, delete\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Command (global)</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.command\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder=\"global:toast.success\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.label\"\n (ngModelChange)=\"onActionsChanged()\"\n />\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\n [(ngModel)]=\"a.buttonVariant\"\n (ngModelChange)=\"onActionsChanged()\"\n >\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\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span\n class=\"color-dot\"\n [style.background]=\"colorDotBackground(c.value)\"\n ></span\n >{{ c.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g gap-8\"\n *ngIf=\"isCustomColor(a.color); else actionCustomBtn\"\n >\n <pdx-color-picker\n label=\"Cor personalizada\"\n [format]=\"'hex'\"\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n ></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n Usar cor personalizada\n </button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>Payload da a\u00E7\u00E3o</mat-label>\n <mat-select\n [(ngModel)]=\"a.emitPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n >\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\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"emitPayload\"\n >\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\n >Exibir quando (ex.: ${item.status} ==\n 'done')</mat-label\n >\n <input\n matInput\n [(ngModel)]=\"a.showIf\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Sintaxe suportada: "${item.campo} == 'valor'". Express\u00F5es avan\u00E7adas n\u00E3o s\u00E3o avaliadas.\"\n >\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\n *ngIf=\"(a.kind || 'icon') === 'icon'\"\n mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n >\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button\n *ngIf=\"a.buttonVariant === 'stroked'\"\n mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'stroked')\"\n >\n {{ a.label || a.id || \"A\u00E7\u00E3o\" }}\n </button>\n <button\n *ngIf=\"a.buttonVariant === 'raised'\"\n mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'raised')\"\n >\n {{ a.label || a.id || \"A\u00E7\u00E3o\" }}\n </button>\n <button\n *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\"\n mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'flat')\"\n >\n {{ a.label || a.id || \"A\u00E7\u00E3o\" }}\n </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)\">\n Remover\n </button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle\n [(ngModel)]=\"a.showLoading\"\n (ngModelChange)=\"onActionsChanged()\"\n >Mostrar loading</mat-slide-toggle\n >\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Confirma\u00E7\u00E3o</mat-panel-title\n ></mat-expansion-panel-header\n >\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\n [value]=\"a.confirmation?.type || ''\"\n (change)=\"applyConfirmationPreset(a, $event.value)\"\n >\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\n matInput\n [ngModel]=\"a.confirmation?.title\"\n (ngModelChange)=\"setConfirmationField(a, 'title', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.message\"\n (ngModelChange)=\"setConfirmationField(a, 'message', $event)\"\n />\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>{{\n a.confirmation?.title || \"Confirmar a\u00E7\u00E3o\"\n }}</strong>\n </div>\n <div class=\"text-caption muted\">\n {{\n a.confirmation?.message ||\n \"Tem certeza que deseja continuar?\"\n }}\n </div>\n <div class=\"text-caption\">\n <span\n class=\"confirm-type\"\n [ngClass]=\"a.confirmation?.type || 'default'\"\n >Tipo: {{ a.confirmation?.type || \"padr\u00E3o\" }}</span\n >\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\"\n >\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\n matInput\n rows=\"4\"\n [(ngModel)]=\"a.globalPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\"\n >JSON inv\u00E1lido</mat-error\n >\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalPayloadExample(a)\"\n >\n Inserir exemplo\n </button>\n <span class=\"muted text-caption\">{{\n globalPayloadExampleHint(a)\n }}</span>\n </div>\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >Emitir evento local tamb\u00E9m</mat-slide-toggle\n >\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')\">\n Preset Tiles Moderno\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.variant\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\n [(ngModel)]=\"working.layout.model\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <ng-container\n *ngIf=\"working.layout.variant === 'list'; else cardModels\"\n >\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\n *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\"\n >\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\n [(ngModel)]=\"working.layout.lines\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\n matInput\n type=\"number\"\n min=\"1\"\n [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Densidade</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.density\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\">\n <mat-label>Espa\u00E7amento entre itens</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.itemSpacing\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">Sem espa\u00E7o extra</mat-option>\n <mat-option value=\"tight\">Justo</mat-option>\n <mat-option value=\"default\">Padr\u00E3o</mat-option>\n <mat-option value=\"relaxed\">Relaxado</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"working.layout.variant !== 'tiles'\"\n >\n <mat-label>Divisores</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.dividers\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"\">Nenhum</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</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\n matInput\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n placeholder=\"ex.: departamento\"\n />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.stickySectionHeader\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n Header de se\u00E7\u00E3o fixo\n </mat-slide-toggle>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.virtualScroll\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\n [(ngModel)]=\"working.ui.showSearch\"\n (ngModelChange)=\"onUiChanged()\"\n >Mostrar busca</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSort\"\n (ngModelChange)=\"onUiChanged()\"\n >Mostrar ordenar</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showRange\"\n (ngModelChange)=\"onUiChanged()\"\n >Mostrar faixa X\u2013Y de Total</mat-slide-toggle\n >\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngIf=\"working.ui?.showSearch\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo para buscar</mat-label>\n <mat-select\n [(ngModel)]=\"working.ui.searchField\"\n (ngModelChange)=\"onUiChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</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\n matInput\n [(ngModel)]=\"working.ui.searchPlaceholder\"\n (ngModelChange)=\"onUiChanged()\"\n placeholder=\"ex.: Buscar por t\u00EDtulo\"\n />\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()\">\n Adicionar op\u00E7\u00E3o\n </button>\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngFor=\"let r of uiSortRows; let i = index\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input\n matInput\n [(ngModel)]=\"r.label\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n placeholder=\"ex.: Mais recentes\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select\n [(ngModel)]=\"r.field\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</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\n [(ngModel)]=\"r.dir\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\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)\">\n Op\u00E7\u00E3o duplicada (campo+dire\u00E7\u00E3o)\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n Remover\n </button>\n </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>{{\n mappingPrimary.field || \"N\u00E3o mapeado\"\n }}</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\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'name';\n onMappingChanged()\n \"\n >\n Nome\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'title';\n onMappingChanged()\n \"\n >\n T\u00EDtulo\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'name';\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'role';\n onMappingChanged()\n \"\n >\n Nome + Papel\n </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\n [(ngModel)]=\"mappingPrimary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of primaryTypeConfigs\"\n [value]=\"mt.type\"\n >\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\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>Classe CSS</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>Estilo Inline</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo Inline</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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>{{\n mappingSecondary.field || \"N\u00E3o mapeado\"\n }}</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\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'subtitle';\n onMappingChanged()\n \"\n >\n Subt\u00EDtulo\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'date';\n mappingSecondary.field = 'hireDate';\n mappingSecondary.dateStyle = 'short';\n onMappingChanged()\n \"\n >\n Data curta\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'currency';\n mappingSecondary.field = 'salary';\n mappingSecondary.currencyCode = 'BRL';\n mappingSecondary.locale = 'pt-BR';\n onMappingChanged()\n \"\n >\n Sal\u00E1rio\n </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\n [(ngModel)]=\"mappingSecondary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of secondaryTypeConfigs\"\n [value]=\"mt.type\"\n >\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\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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\"\n ><mat-label>Classe CSS</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Estilo Inline</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel\n [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingMeta.type || \"text\")\n }}</mat-icon>\n <span>Meta (Detalhe/Lateral)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingMetaFields.length\n ? \"Campo composto (\" + mappingMetaFields.length + \")\"\n : mappingMeta.field || \"N\u00E3o mapeado\"\n }}\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\n [(ngModel)]=\"mappingMetaFields\"\n multiple\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g g-1-1 ai-center gap-12\"\n *ngIf=\"mappingMetaFields.length\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>Separador</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMetaSeparator\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"mappingMetaWrapSecondInParens\"\n (ngModelChange)=\"onMappingChanged()\"\n >\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\n [(ngModel)]=\"mappingMeta.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of metaTypeConfigs\"\n [value]=\"mt.type\"\n >\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\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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()\"\n ></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()\"\n ></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()\"\n ></praxis-meta-editor-icon>\n }\n @case (\"image\") {\n <praxis-meta-editor-image\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Op\u00E7\u00F5es avan\u00E7adas</mat-panel-title\n ></mat-expansion-panel-header\n >\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\n [(ngModel)]=\"mappingMeta.placement\"\n (ngModelChange)=\"onMappingChanged()\"\n >\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\n matInput\n [(ngModel)]=\"mappingMeta.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMeta.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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>{{\n getTypeIcon(mappingTrailing.type || \"text\")\n }}</mat-icon>\n <span>Trailing (Direita)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n 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\n [(ngModel)]=\"mappingTrailing.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of trailingTypeConfigs\"\n [value]=\"mt.type\"\n >\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\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'chip';\n mappingTrailing.chipColor = 'primary';\n mappingTrailing.chipVariant = 'filled';\n mappingTrailing.field = 'status';\n onMappingChanged()\n \"\n >\n Status Chip\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'icon';\n mappingTrailing.field = 'status';\n mappingTrailing.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n Status \u00CDcone\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'currency';\n mappingTrailing.field = 'price';\n mappingTrailing.currencyCode = 'BRL';\n mappingTrailing.locale = 'pt-BR';\n onMappingChanged()\n \"\n >\n Pre\u00E7o\n </button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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\n matInput\n [(ngModel)]=\"mappingTrailing.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n \"\n >URL/expr obrigat\u00F3ria</mat-error\n >\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingTrailing.imageUrl\"\n >\n Defina a URL/expr para renderizar a imagem.\n </div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Estilo</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>Classe</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Style</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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]=\"\n !!mappingLeading.field ||\n (mappingLeading.type === 'icon' && !!mappingLeading.icon) ||\n (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\n \"\n >\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 {{\n mappingLeading.type === \"icon\"\n ? mappingLeading.icon || \"\u00CDcone est\u00E1tico\"\n : mappingLeading.field ||\n (mappingLeading.imageUrl\n ? \"Imagem est\u00E1tica\"\n : \"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\n [(ngModel)]=\"mappingLeading.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of leadingTypeConfigs\"\n [value]=\"mt.type\"\n >\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\n appearance=\"outline\"\n *ngIf=\"\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n \"\n >\n <mat-label>Campo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\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\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'icon';\n mappingLeading.icon = 'person';\n mappingLeading.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n Avatar \u00CDcone\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'image';\n mappingLeading.imageUrl = 'https://placehold.co/64x64';\n mappingLeading.imageAlt = 'Avatar';\n mappingLeading.badgeText = '${item.status}';\n onMappingChanged()\n \"\n >\n Avatar Imagem + Badge\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'chip';\n mappingLeading.field = 'tag';\n mappingLeading.chipColor = 'accent';\n mappingLeading.chipVariant = 'filled';\n onMappingChanged()\n \"\n >\n Chip Tag\n </button>\n </div>\n\n <!-- Icon Specific -->\n <div\n class=\"g g-1-auto gap-12 ai-center\"\n *ngIf=\"mappingLeading.type === 'icon'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.icon\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\">\n <mat-icon>search</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"text-caption muted\">\n Use pipe <code>|iconMap</code> no extra pipe para din\u00E2mico\n </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()\"\n ></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div\n class=\"g g-1-1 gap-12\"\n *ngIf=\"mappingLeading.type === 'image'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>URL da Imagem</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\"\n >URL/expr obrigat\u00F3ria</mat-error\n >\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Alt Text</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageAlt\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Badge Texto</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.badgeText\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></praxis-meta-editor-rating>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Estilo</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>Classe</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingLeading.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Style</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingLeading.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Features -->\n <mat-expansion-panel\n [expanded]=\"featuresVisible && features.length > 0\"\n >\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\n >{{ features.length }} item(s)</mat-panel-description\n >\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\n [(ngModel)]=\"featuresVisible\"\n (ngModelChange)=\"onFeaturesChanged()\"\n >Ativar recursos</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"featuresSyncWithMeta\"\n (ngModelChange)=\"onMappingChanged()\"\n >Sincronizar com Meta</mat-slide-toggle\n >\n <span class=\"flex-1\"></span>\n <mat-button-toggle-group\n [(ngModel)]=\"featuresMode\"\n (change)=\"onFeaturesChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle value=\"icons+labels\"\n ><mat-icon>view_list</mat-icon></mat-button-toggle\n >\n <mat-button-toggle value=\"icons-only\"\n ><mat-icon>more_horiz</mat-icon></mat-button-toggle\n >\n </mat-button-toggle-group>\n </div>\n\n <div\n *ngFor=\"let f of features; let i = index\"\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\">\n <mat-icon>{{ f.icon || \"search\" }}</mat-icon>\n </button>\n <mat-form-field\n appearance=\"outline\"\n class=\"dense-form-field no-sub\"\n >\n <input\n matInput\n [(ngModel)]=\"f.expr\"\n (ngModelChange)=\"onFeaturesChanged()\"\n placeholder=\"Expr/Texto\"\n />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\">\n <mat-icon>add</mat-icon> Adicionar recurso\n </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>{{\n getTypeIcon(mappingSectionHeader.type)\n }}</mat-icon>\n <span>Cabe\u00E7alho de Se\u00E7\u00E3o</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n 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\n [(ngModel)]=\"mappingSectionHeader.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of sectionHeaderTypeConfigs\"\n [value]=\"mt.type\"\n >\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\n matInput\n [(ngModel)]=\"mappingSectionHeader.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'text';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n Texto padr\u00E3o\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'chip';\n mappingSectionHeader.chipColor = 'primary';\n mappingSectionHeader.chipVariant = 'filled';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n Chip padr\u00E3o\n </button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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\n matInput\n [(ngModel)]=\"mappingSectionHeader.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n \"\n >URL/expr obrigat\u00F3ria</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingSectionHeader.imageUrl\"\n >\n Defina a URL/expr para renderizar a imagem.\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingSectionHeader\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Estilo</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>Classe</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Style</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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>{{\n mappingEmptyState.expr || \"Padr\u00E3o\"\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\n [(ngModel)]=\"mappingEmptyState.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of emptyStateTypeConfigs\"\n [value]=\"mt.type\"\n >\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\n matInput\n [(ngModel)]=\"mappingEmptyState.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'text';\n mappingEmptyState.expr = 'Nenhum item dispon\u00EDvel';\n onMappingChanged()\n \"\n >\n Mensagem padr\u00E3o\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'image';\n mappingEmptyState.imageUrl = '/list-empty-state.svg';\n mappingEmptyState.imageAlt = 'Sem resultados';\n onMappingChanged()\n \"\n >\n Imagem padr\u00E3o\n </button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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\n matInput\n [(ngModel)]=\"mappingEmptyState.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n \"\n >URL/expr obrigat\u00F3ria</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingEmptyState.imageUrl\"\n >\n Defina a URL/expr para renderizar a imagem.\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingEmptyState\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Estilo</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>Classe</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Style</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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()\">\n Aplicar mapeamento\n </button>\n <button\n mat-button\n (click)=\"inferFromFields()\"\n [disabled]=\"!fields.length\"\n >\n Inferir do schema\n </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\n matInput\n type=\"number\"\n min=\"0\"\n [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\"\n />\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\n [(ngModel)]=\"skinPreviewTheme\"\n (change)=\"onSkinChanged()\"\n appearance=\"legacy\"\n >\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\n [config]=\"working\"\n [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"\n ></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"i18n/A11y\">\n <div\n class=\"editor-content grid gap-3\"\n *ngIf=\"working?.a11y && working?.events\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>Locale padr\u00E3o</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.locale\"\n (ngModelChange)=\"markDirty()\"\n placeholder=\"ex.: pt-BR\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Moeda padr\u00E3o</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.currency\"\n (ngModelChange)=\"markDirty()\"\n placeholder=\"ex.: BRL\"\n />\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\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabel\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-labelledby</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabelledBy\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.highContrast\"\n (ngModelChange)=\"markDirty()\"\n >Alto contraste</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.reduceMotion\"\n (ngModelChange)=\"markDirty()\"\n >Reduzir movimento</mat-slide-toggle\n >\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\n matInput\n [(ngModel)]=\"working!.events!.itemClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.actionClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.selectionChange\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.loaded\"\n (ngModelChange)=\"markDirty()\"\n />\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\n [(ngModel)]=\"working.selection.mode\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\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\n matInput\n [(ngModel)]=\"working.selection.formControlName\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n >\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\n matInput\n [(ngModel)]=\"working.selection.formControlPath\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n >\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\n matInput\n [(ngModel)]=\"working.selection.compareBy\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Chave unica do item.\"\n >\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\n [(ngModel)]=\"working.selection.return\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\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')\">\n Pill Soft\n </button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">\n Gradient Tile\n </button>\n <button mat-button (click)=\"applySkinPreset('glass')\">Glass</button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">\n Elevated\n </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')\">\n Neumorphism\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <mat-select\n [(ngModel)]=\"working.skin.type\"\n (ngModelChange)=\"onSkinTypeChanged($event)\"\n >\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\n matInput\n [(ngModel)]=\"working.skin.radius\"\n (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 1.25rem\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Sombra</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.shadow\"\n (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: var(--md-sys-elevation-level2)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Borda</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.border\"\n (ngModelChange)=\"onSkinChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n *ngIf=\"working.skin.type === 'glass'\"\n appearance=\"outline\"\n >\n <mat-label>Desfoque</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.backdropBlur\"\n (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 8px\"\n />\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\n matInput\n [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA at\u00E9</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00C2ngulo</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\"\n />\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\n matInput\n [(ngModel)]=\"working.skin.class\"\n (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: my-list-skin\"\n />\n </mat-form-field>\n\n <div\n *ngIf=\"working.skin.type === 'custom'\"\n class=\"g g-auto-220 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Estilo inline (skin.inlineStyle)</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"working.skin.inlineStyle\"\n (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"\n ></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\">\n.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre\n >\n </div>\n </div>\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"] }] });
|
|
5695
|
+
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" }, providers: [
|
|
5696
|
+
providePraxisI18nConfig(SURFACE_OPEN_I18N_CONFIG),
|
|
5697
|
+
providePraxisListI18n(),
|
|
5698
|
+
], ngImport: i0, template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">\n {{ tx('Assistant-applied adjustments replace the entire configuration object.') }}\n </div>\n <button\n mat-icon-button\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Resource (API)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.dataSource.resourcePath\"\n (ngModelChange)=\"onResourcePathChange($event)\"\n [placeholder]=\"tx('e.g.: users')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n >\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>{{ tx('Query (JSON)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [(ngModel)]=\"queryJson\"\n (ngModelChange)=\"onQueryChanged($event)\"\n [placeholder]=\"queryPlaceholder()\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n *ngIf=\"!queryError\"\n >\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>{{ tx('Sort by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortField\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortDir\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('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 >\n </praxis-list-json-config-editor>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('Actions')\">\n <div class=\"editor-content g gap-12\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">\n {{ tx('Configure per-item action buttons (icon, label, color, visibility)') }}\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addAction()\">\n {{ tx('Add action') }}\n </button>\n </div>\n <div class=\"g g-1-auto gap-8 ai-center\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Global action (Praxis)') }}</mat-label>\n <mat-select\n [(ngModel)]=\"selectedGlobalActionId\"\n (ngModelChange)=\"onGlobalActionSelected($event)\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- Select --') }}</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\n *ngIf=\"!globalActionCatalog.length\"\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a global `command`.') }}\n </div>\n </div>\n <div\n *ngFor=\"let a of working.actions || []; let i = index\"\n class=\"g g-auto-200 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('ID') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.id\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action type') }}</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">{{ tx('Icon') }}</mat-option>\n <mat-option value=\"button\">{{ tx('Button') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.icon\"\n (ngModelChange)=\"onActionsChanged()\"\n [placeholder]=\"tx('e.g.: edit, delete')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Command (global)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.command\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder=\"global:toast.success\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.label\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Variant') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.buttonVariant\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option value=\"stroked\">{{ tx('Outlined') }}</mat-option>\n <mat-option value=\"raised\">{{ tx('Raised') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Filled') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action color') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span\n class=\"color-dot\"\n [style.background]=\"colorDotBackground(c.value)\"\n ></span\n >{{ tx(c.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g gap-8\"\n *ngIf=\"isCustomColor(a.color); else actionCustomBtn\"\n >\n <pdx-color-picker\n [label]=\"tx('Custom color')\"\n [format]=\"'hex'\"\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n ></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action payload') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.emitPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Payload emitted by the action.')\"\n >\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>{{ actionVisibilityLabel() }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.showIf\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n >\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\n *ngIf=\"(a.kind || 'icon') === 'icon'\"\n mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n >\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button\n *ngIf=\"a.buttonVariant === 'stroked'\"\n mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'stroked')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"a.buttonVariant === 'raised'\"\n mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'raised')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\"\n mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'flat')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n </ng-container>\n <span class=\"muted\">{{ tx('Preview') }}</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle\n [(ngModel)]=\"a.showLoading\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Show loading') }}</mat-slide-toggle\n >\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >{{ tx('Confirmation') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">{{ tx('Type') }}</span>\n <mat-button-toggle-group\n [value]=\"a.confirmation?.type || ''\"\n (change)=\"applyConfirmationPreset(a, $event.value)\"\n >\n <mat-button-toggle value=\"\">{{ tx('Default') }}</mat-button-toggle>\n <mat-button-toggle value=\"danger\">{{ tx('Danger') }}</mat-button-toggle>\n <mat-button-toggle value=\"warning\">{{ tx('Warning') }}</mat-button-toggle>\n <mat-button-toggle value=\"info\">{{ tx('Info') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.title\"\n (ngModelChange)=\"setConfirmationField(a, 'title', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.message\"\n (ngModelChange)=\"setConfirmationField(a, 'message', $event)\"\n />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">{{ tx('Preview') }}</div>\n <div class=\"text-caption\">\n <strong>{{\n a.confirmation?.title || tx('Confirm action')\n }}</strong>\n </div>\n <div class=\"text-caption muted\">\n {{\n a.confirmation?.message ||\n tx('Are you sure you want to continue?')\n }}\n </div>\n <div class=\"text-caption\">\n <span\n class=\"confirm-type\"\n [ngClass]=\"a.confirmation?.type || 'default'\"\n >{{ tx('Type') }}:\n {{ a.confirmation?.type || tx('Default').toLowerCase() }}</span\n >\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <ng-container *ngIf=\"isSurfaceOpenCommand(a); else defaultGlobalPayloadEditor\">\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n </ng-container>\n <ng-template #defaultGlobalPayloadEditor>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload (JSON/Template)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"a.globalPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\"\n >{{ tx('Invalid JSON') }}</mat-error\n >\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalPayloadExampleHint(a)\n }}</span>\n </div>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('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')\">\n {{ tx('Modern tiles preset') }}\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Variant') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.variant\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"list\">{{ tx('List') }}</mat-option>\n <mat-option value=\"cards\">{{ tx('Cards') }}</mat-option>\n <mat-option value=\"tiles\">{{ tx('Tiles') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Model') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.model\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <ng-container\n *ngIf=\"working.layout.variant === 'list'; else cardModels\"\n >\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Media on the left') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel (large media)') }}</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container\n *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\"\n >\n <mat-option value=\"standard\">{{ tx('Standard tile') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Tile with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel tile') }}</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Card with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('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>{{ tx('Lines') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.lines\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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>{{ tx('Items per page') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Density') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.density\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"default\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"comfortable\">{{ tx('Comfortable') }}</mat-option>\n <mat-option value=\"compact\">{{ tx('Compact') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Spacing between items') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.itemSpacing\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No extra space') }}</mat-option>\n <mat-option value=\"tight\">{{ tx('Tight') }}</mat-option>\n <mat-option value=\"default\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"relaxed\">{{ tx('Relaxed') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"working.layout.variant !== 'tiles'\"\n >\n <mat-label>{{ tx('Dividers') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.dividers\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('None') }}</mat-option>\n <mat-option value=\"between\">{{ tx('Between groups') }}</mat-option>\n <mat-option value=\"all\">{{ tx('All') }}</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>{{ tx('Group by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"\">{{ tx('None') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</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>{{ tx('Group by') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n [placeholder]=\"tx('e.g.: department')\"\n />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.stickySectionHeader\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Sticky section header') }}\n </mat-slide-toggle>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.virtualScroll\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Virtual scroll') }}\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('List tools') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSearch\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show search') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSort\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show sorting') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showRange\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show total X-Y range') }}</mat-slide-toggle\n >\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngIf=\"working.ui?.showSearch\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field to search') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.ui.searchField\"\n (ngModelChange)=\"onUiChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Search placeholder') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.ui.searchPlaceholder\"\n (ngModelChange)=\"onUiChanged()\"\n [placeholder]=\"tx('e.g.: Search by title')\"\n />\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\">\n {{ tx('Sorting options (label \u2192 field+direction)') }}\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">\n {{ tx('Add option') }}\n </button>\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngFor=\"let r of uiSortRows; let i = index\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"r.label\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n [placeholder]=\"tx('e.g.: Most recent')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.field\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.dir\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('Content')\">\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>{{ tx('Primary (Title)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingPrimary.field || tx('Not mapped')\n }}</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\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'name';\n onMappingChanged()\n \"\n >\n {{ tx('Name') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'title';\n onMappingChanged()\n \"\n >\n {{ tx('Title') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'name';\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'role';\n onMappingChanged()\n \"\n >\n {{ tx('Name + role') }}\n </button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of primaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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>{{ tx('Secondary (Summary)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSecondary.field || tx('Not mapped')\n }}</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\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'subtitle';\n onMappingChanged()\n \"\n >\n {{ tx('Subtitle') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'date';\n mappingSecondary.field = 'hireDate';\n mappingSecondary.dateStyle = 'short';\n onMappingChanged()\n \"\n >\n {{ tx('Short date') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applySalaryPreset()\"\n >\n {{ tx('Salary') }}\n </button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of secondaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</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\"\n ><mat-label>{{ tx('CSS class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Inline style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel\n [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingMeta.type || \"text\")\n }}</mat-icon>\n <span>{{ tx('Meta (Detail/Side)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingMetaFields.length\n ? tx('Composed field ({{count}})', { count: mappingMetaFields.length })\n : mappingMeta.field || tx('Not mapped')\n }}\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\">{{ tx('Composition mode') }}</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Fields to compose (multi-select)') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMetaFields\"\n multiple\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g g-1-1 ai-center gap-12\"\n *ngIf=\"mappingMetaFields.length\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Separator') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMetaSeparator\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"mappingMetaWrapSecondInParens\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n {{ tx('(Second) in parentheses') }}\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>{{ tx('Single field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- None --') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of metaTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(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\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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()\"\n ></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()\"\n ></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()\"\n ></praxis-meta-editor-icon>\n }\n @case (\"image\") {\n <praxis-meta-editor-image\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Advanced options') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Position') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.placement\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option value=\"side\">{{ tx('Side (right)') }}</mat-option>\n <mat-option value=\"line\">{{ tx('Inline (below)') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMeta.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMeta.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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>{{\n getTypeIcon(mappingTrailing.type || \"text\")\n }}</mat-icon>\n <span>{{ tx('Trailing (right)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingTrailing.field || tx('Not mapped')\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>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- None --') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of trailingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(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\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'chip';\n mappingTrailing.chipColor = 'primary';\n mappingTrailing.chipVariant = 'filled';\n mappingTrailing.field = 'status';\n onMappingChanged()\n \"\n >\n {{ tx('Status chip') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'icon';\n mappingTrailing.field = 'status';\n mappingTrailing.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Status icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyPricePreset()\"\n >\n {{ tx('Price') }}\n </button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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>{{ tx('URL / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingTrailing.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingTrailing.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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]=\"\n !!mappingLeading.field ||\n (mappingLeading.type === 'icon' && !!mappingLeading.icon) ||\n (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\n \"\n >\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>{{ tx('Leading (left)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingLeading.type === \"icon\"\n ? mappingLeading.icon || tx('Static icon')\n : mappingLeading.field ||\n (mappingLeading.imageUrl\n ? tx('Static image')\n : tx('Not mapped'))\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>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of leadingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(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\n appearance=\"outline\"\n *ngIf=\"\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n \"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\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\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'icon';\n mappingLeading.icon = 'person';\n mappingLeading.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'image';\n mappingLeading.imageUrl = 'https://placehold.co/64x64';\n mappingLeading.imageAlt = 'Avatar';\n mappingLeading.badgeText = '${item.status}';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar image + badge') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'chip';\n mappingLeading.field = 'tag';\n mappingLeading.chipColor = 'accent';\n mappingLeading.chipVariant = 'filled';\n onMappingChanged()\n \"\n >\n {{ tx('Tag chip') }}\n </button>\n </div>\n\n <!-- Icon Specific -->\n <div\n class=\"g g-1-auto gap-12 ai-center\"\n *ngIf=\"mappingLeading.type === 'icon'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.icon\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\">\n <mat-icon>search</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"text-caption muted\">\n {{ tx('Use the `|iconMap` pipe in the extra pipe for dynamic rendering.') }}\n </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()\"\n ></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div\n class=\"g g-1-1 gap-12\"\n *ngIf=\"mappingLeading.type === 'image'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Image URL') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Alt text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageAlt\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Badge text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.badgeText\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></praxis-meta-editor-rating>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingLeading.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingLeading.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Features -->\n <mat-expansion-panel\n [expanded]=\"featuresVisible && features.length > 0\"\n >\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>{{ tx('Features') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description\n >{{ features.length }} item(s)</mat-panel-description\n >\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\n [(ngModel)]=\"featuresVisible\"\n (ngModelChange)=\"onFeaturesChanged()\"\n >{{ tx('Enable features') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"featuresSyncWithMeta\"\n (ngModelChange)=\"onMappingChanged()\"\n >{{ tx('Sync with Meta') }}</mat-slide-toggle\n >\n <span class=\"flex-1\"></span>\n <mat-button-toggle-group\n [(ngModel)]=\"featuresMode\"\n (change)=\"onFeaturesChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle value=\"icons+labels\"\n ><mat-icon>view_list</mat-icon></mat-button-toggle\n >\n <mat-button-toggle value=\"icons-only\"\n ><mat-icon>more_horiz</mat-icon></mat-button-toggle\n >\n </mat-button-toggle-group>\n </div>\n\n <div\n *ngFor=\"let f of features; let i = index\"\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\">\n <mat-icon>{{ f.icon || \"search\" }}</mat-icon>\n </button>\n <mat-form-field\n appearance=\"outline\"\n class=\"dense-form-field no-sub\"\n >\n <input\n matInput\n [(ngModel)]=\"f.expr\"\n (ngModelChange)=\"onFeaturesChanged()\"\n [placeholder]=\"tx('Expression/Text')\"\n />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\">\n <mat-icon>add</mat-icon> {{ tx('Add feature') }}\n </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>{{\n getTypeIcon(mappingSectionHeader.type)\n }}</mat-icon>\n <span>{{ tx('Section header') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSectionHeader.expr || tx('Not configured')\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>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSectionHeader.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of sectionHeaderTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Expression (item.key)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingSectionHeader.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'text';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default text') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'chip';\n mappingSectionHeader.chipColor = 'primary';\n mappingSectionHeader.chipVariant = 'filled';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default chip') }}\n </button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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>{{ tx('Image URL') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingSectionHeader.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingSectionHeader.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingSectionHeader\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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>{{ tx('Empty state') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingEmptyState.expr || tx('Default')\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>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingEmptyState.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of emptyStateTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingEmptyState.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'text';\n mappingEmptyState.expr = tx('No items available');\n onMappingChanged()\n \"\n >\n {{ tx('Default message') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'image';\n mappingEmptyState.imageUrl = '/list-empty-state.svg';\n mappingEmptyState.imageAlt = tx('No results');\n onMappingChanged()\n \"\n >\n {{ tx('Default image') }}\n </button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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>{{ tx('Image URL') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingEmptyState.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingEmptyState\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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()\">\n {{ tx('Apply mapping') }}\n </button>\n <button\n mat-button\n (click)=\"inferFromFields()\"\n [disabled]=\"!fields.length\"\n >\n {{ tx('Infer from schema') }}\n </button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Skeleton count') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"0\"\n [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\"\n />\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\">{{ tx('Theme preview') }}</span>\n <mat-button-toggle-group\n [(ngModel)]=\"skinPreviewTheme\"\n (change)=\"onSkinChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle [value]=\"'light'\">{{ tx('Light') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">{{ tx('Dark') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">{{ tx('Grid') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview\n [config]=\"working\"\n [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"\n ></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <div\n class=\"editor-content grid gap-3\"\n *ngIf=\"working?.a11y && working?.events\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default locale') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.locale\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"tx('e.g.: en-US')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default currency') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.currency\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"currencyPlaceholder()\"\n />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Accessibility') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabel\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-labelledby') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabelledBy\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.highContrast\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('High contrast') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.reduceMotion\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('Reduce motion') }}</mat-slide-toggle\n >\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Events') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('itemClick') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.itemClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.actionClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.selectionChange\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.loaded\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.mode\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No selection') }}</mat-option>\n <mat-option value=\"single\">{{ tx('Single') }}</mat-option>\n <mat-option value=\"multiple\">{{ tx('Multiple') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form name') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlName\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form path') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlPath\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Compare by (field)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.compareBy\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Return') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.return\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\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')\">\n {{ tx('Pill Soft') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">\n {{ tx('Gradient Tile') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('glass')\">\n {{ tx('Glass') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">\n {{ tx('Elevated') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('outline')\">\n {{ tx('Outline') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('flat')\">\n {{ tx('Flat') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">\n {{ tx('Neumorphism') }}\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Style') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.skin.type\"\n (ngModelChange)=\"onSkinTypeChanged($event)\"\n >\n <mat-option value=\"pill-soft\">{{ tx('Pill Soft') }}</mat-option>\n <mat-option value=\"gradient-tile\">{{ tx('Gradient Tile') }}</mat-option>\n <mat-option value=\"glass\">{{ tx('Glass') }}</mat-option>\n <mat-option value=\"elevated\">{{ tx('Elevated') }}</mat-option>\n <mat-option value=\"outline\">{{ tx('Outline') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Flat') }}</mat-option>\n <mat-option value=\"neumorphism\">{{ tx('Neumorphism') }}</mat-option>\n <mat-option value=\"custom\">{{ tx('Custom') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Radius') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.radius\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 1.25rem')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shadow') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.shadow\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: var(--md-sys-elevation-level2)')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.border\"\n (ngModelChange)=\"onSkinChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n *ngIf=\"working.skin.type === 'glass'\"\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Blur') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.backdropBlur\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 8px')\"\n />\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>{{ tx('Gradient from') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient to') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Angle') }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\"\n />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Extra CSS class (skin.class)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.class\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: my-list-skin')\"\n />\n </mat-form-field>\n\n <div\n *ngIf=\"working.skin.type === 'custom'\"\n class=\"g g-auto-220 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Inline style (skin.inlineStyle)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"working.skin.inlineStyle\"\n (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"\n ></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n {{ tx('CSS class example (add this to your global styles):') }}\n <pre class=\"code-block\">\n.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre\n >\n </div>\n </div>\n </div>\n </mat-tab>\n</mat-tab-group>\n\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"] }, { kind: "component", type: SurfaceOpenActionEditorComponent, selector: "praxis-surface-open-action-editor", inputs: ["value", "hostKind"], outputs: ["valueChange"] }] });
|
|
4985
5699
|
}
|
|
4986
5700
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisListConfigEditor, decorators: [{
|
|
4987
5701
|
type: Component,
|
|
4988
|
-
args: [{ selector: 'praxis-list-config-editor', standalone: true,
|
|
5702
|
+
args: [{ selector: 'praxis-list-config-editor', standalone: true, providers: [
|
|
5703
|
+
providePraxisI18nConfig(SURFACE_OPEN_I18N_CONFIG),
|
|
5704
|
+
providePraxisListI18n(),
|
|
5705
|
+
], imports: [
|
|
4989
5706
|
CommonModule,
|
|
4990
5707
|
FormsModule,
|
|
4991
5708
|
MatTabsModule,
|
|
@@ -5012,7 +5729,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
5012
5729
|
PraxisMetaEditorImageComponent,
|
|
5013
5730
|
PraxisListJsonConfigEditorComponent,
|
|
5014
5731
|
PdxColorPickerComponent,
|
|
5015
|
-
], 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\">\n Observa\u00E7\u00E3o: ajustes aplicados pelo assistente substituem o objeto de\n configura\u00E7\u00E3o inteiro.\n </div>\n <button\n mat-icon-button\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"O applyConfigFromAdapter n\u00E3o faz merge profundo. Garanta que o adapter envie a config completa.\"\n >\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\n matInput\n [(ngModel)]=\"working.dataSource.resourcePath\"\n (ngModelChange)=\"onResourcePathChange($event)\"\n placeholder=\"ex.: users\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Endpoint do recurso (resourcePath).\"\n >\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\n matInput\n rows=\"3\"\n [(ngModel)]=\"queryJson\"\n (ngModelChange)=\"onQueryChanged($event)\"\n placeholder='ex.: {\"status\":\"active\",\"department\":\"sales\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Opcional. Use JSON v\u00E1lido para filtros iniciais.\"\n *ngIf=\"!queryError\"\n >\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\n [(ngModel)]=\"sortField\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Campo base do recurso.\"\n >\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\n [(ngModel)]=\"sortDir\"\n (ngModelChange)=\"updateSortConfig()\"\n >\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 >\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\">\n Configure bot\u00F5es de a\u00E7\u00E3o por item (\u00EDcone, r\u00F3tulo, cor, visibilidade)\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addAction()\">\n Adicionar a\u00E7\u00E3o\n </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\n [(ngModel)]=\"selectedGlobalActionId\"\n (ngModelChange)=\"onGlobalActionSelected($event)\"\n >\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\n *ngIf=\"!globalActionCatalog.length\"\n class=\"text-caption muted\"\n >Nenhuma a\u00E7\u00E3o global registrada.</mat-hint\n >\n </mat-form-field>\n <div class=\"muted text-caption\">\n Selecione para adicionar com `command` global.\n </div>\n </div>\n <div\n *ngFor=\"let a of working.actions || []; let i = index\"\n class=\"g g-auto-200 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>ID</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.id\"\n (ngModelChange)=\"onActionsChanged()\"\n />\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\n matInput\n [(ngModel)]=\"a.icon\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder=\"ex.: edit, delete\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Command (global)</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.command\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder=\"global:toast.success\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.label\"\n (ngModelChange)=\"onActionsChanged()\"\n />\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\n [(ngModel)]=\"a.buttonVariant\"\n (ngModelChange)=\"onActionsChanged()\"\n >\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\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span\n class=\"color-dot\"\n [style.background]=\"colorDotBackground(c.value)\"\n ></span\n >{{ c.label }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g gap-8\"\n *ngIf=\"isCustomColor(a.color); else actionCustomBtn\"\n >\n <pdx-color-picker\n label=\"Cor personalizada\"\n [format]=\"'hex'\"\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n ></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n Usar cor personalizada\n </button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>Payload da a\u00E7\u00E3o</mat-label>\n <mat-select\n [(ngModel)]=\"a.emitPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n >\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\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"emitPayload\"\n >\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\n >Exibir quando (ex.: ${item.status} ==\n 'done')</mat-label\n >\n <input\n matInput\n [(ngModel)]=\"a.showIf\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Sintaxe suportada: "${item.campo} == 'valor'". Express\u00F5es avan\u00E7adas n\u00E3o s\u00E3o avaliadas.\"\n >\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\n *ngIf=\"(a.kind || 'icon') === 'icon'\"\n mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n >\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button\n *ngIf=\"a.buttonVariant === 'stroked'\"\n mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'stroked')\"\n >\n {{ a.label || a.id || \"A\u00E7\u00E3o\" }}\n </button>\n <button\n *ngIf=\"a.buttonVariant === 'raised'\"\n mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'raised')\"\n >\n {{ a.label || a.id || \"A\u00E7\u00E3o\" }}\n </button>\n <button\n *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\"\n mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'flat')\"\n >\n {{ a.label || a.id || \"A\u00E7\u00E3o\" }}\n </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)\">\n Remover\n </button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle\n [(ngModel)]=\"a.showLoading\"\n (ngModelChange)=\"onActionsChanged()\"\n >Mostrar loading</mat-slide-toggle\n >\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Confirma\u00E7\u00E3o</mat-panel-title\n ></mat-expansion-panel-header\n >\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\n [value]=\"a.confirmation?.type || ''\"\n (change)=\"applyConfirmationPreset(a, $event.value)\"\n >\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\n matInput\n [ngModel]=\"a.confirmation?.title\"\n (ngModelChange)=\"setConfirmationField(a, 'title', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Mensagem</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.message\"\n (ngModelChange)=\"setConfirmationField(a, 'message', $event)\"\n />\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>{{\n a.confirmation?.title || \"Confirmar a\u00E7\u00E3o\"\n }}</strong>\n </div>\n <div class=\"text-caption muted\">\n {{\n a.confirmation?.message ||\n \"Tem certeza que deseja continuar?\"\n }}\n </div>\n <div class=\"text-caption\">\n <span\n class=\"confirm-type\"\n [ngClass]=\"a.confirmation?.type || 'default'\"\n >Tipo: {{ a.confirmation?.type || \"padr\u00E3o\" }}</span\n >\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\"\n >\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\n matInput\n rows=\"4\"\n [(ngModel)]=\"a.globalPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\"\n >JSON inv\u00E1lido</mat-error\n >\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalPayloadExample(a)\"\n >\n Inserir exemplo\n </button>\n <span class=\"muted text-caption\">{{\n globalPayloadExampleHint(a)\n }}</span>\n </div>\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >Emitir evento local tamb\u00E9m</mat-slide-toggle\n >\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')\">\n Preset Tiles Moderno\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Variante</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.variant\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\n [(ngModel)]=\"working.layout.model\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <ng-container\n *ngIf=\"working.layout.variant === 'list'; else cardModels\"\n >\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\n *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\"\n >\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\n [(ngModel)]=\"working.layout.lines\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\n matInput\n type=\"number\"\n min=\"1\"\n [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Densidade</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.density\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\">\n <mat-label>Espa\u00E7amento entre itens</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.itemSpacing\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">Sem espa\u00E7o extra</mat-option>\n <mat-option value=\"tight\">Justo</mat-option>\n <mat-option value=\"default\">Padr\u00E3o</mat-option>\n <mat-option value=\"relaxed\">Relaxado</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"working.layout.variant !== 'tiles'\"\n >\n <mat-label>Divisores</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.dividers\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"\">Nenhum</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</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\n matInput\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n placeholder=\"ex.: departamento\"\n />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.stickySectionHeader\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n Header de se\u00E7\u00E3o fixo\n </mat-slide-toggle>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.virtualScroll\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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\n [(ngModel)]=\"working.ui.showSearch\"\n (ngModelChange)=\"onUiChanged()\"\n >Mostrar busca</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSort\"\n (ngModelChange)=\"onUiChanged()\"\n >Mostrar ordenar</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showRange\"\n (ngModelChange)=\"onUiChanged()\"\n >Mostrar faixa X\u2013Y de Total</mat-slide-toggle\n >\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngIf=\"working.ui?.showSearch\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo para buscar</mat-label>\n <mat-select\n [(ngModel)]=\"working.ui.searchField\"\n (ngModelChange)=\"onUiChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</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\n matInput\n [(ngModel)]=\"working.ui.searchPlaceholder\"\n (ngModelChange)=\"onUiChanged()\"\n placeholder=\"ex.: Buscar por t\u00EDtulo\"\n />\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()\">\n Adicionar op\u00E7\u00E3o\n </button>\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngFor=\"let r of uiSortRows; let i = index\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>R\u00F3tulo</mat-label>\n <input\n matInput\n [(ngModel)]=\"r.label\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n placeholder=\"ex.: Mais recentes\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Campo</mat-label>\n <mat-select\n [(ngModel)]=\"r.field\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</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\n [(ngModel)]=\"r.dir\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\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)\">\n Op\u00E7\u00E3o duplicada (campo+dire\u00E7\u00E3o)\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n Remover\n </button>\n </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>{{\n mappingPrimary.field || \"N\u00E3o mapeado\"\n }}</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\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'name';\n onMappingChanged()\n \"\n >\n Nome\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'title';\n onMappingChanged()\n \"\n >\n T\u00EDtulo\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'name';\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'role';\n onMappingChanged()\n \"\n >\n Nome + Papel\n </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\n [(ngModel)]=\"mappingPrimary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of primaryTypeConfigs\"\n [value]=\"mt.type\"\n >\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\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>Classe CSS</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>Estilo Inline</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo Inline</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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>{{\n mappingSecondary.field || \"N\u00E3o mapeado\"\n }}</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\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'subtitle';\n onMappingChanged()\n \"\n >\n Subt\u00EDtulo\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'date';\n mappingSecondary.field = 'hireDate';\n mappingSecondary.dateStyle = 'short';\n onMappingChanged()\n \"\n >\n Data curta\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'currency';\n mappingSecondary.field = 'salary';\n mappingSecondary.currencyCode = 'BRL';\n mappingSecondary.locale = 'pt-BR';\n onMappingChanged()\n \"\n >\n Sal\u00E1rio\n </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\n [(ngModel)]=\"mappingSecondary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of secondaryTypeConfigs\"\n [value]=\"mt.type\"\n >\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\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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\"\n ><mat-label>Classe CSS</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Estilo Inline</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel\n [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingMeta.type || \"text\")\n }}</mat-icon>\n <span>Meta (Detalhe/Lateral)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingMetaFields.length\n ? \"Campo composto (\" + mappingMetaFields.length + \")\"\n : mappingMeta.field || \"N\u00E3o mapeado\"\n }}\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\n [(ngModel)]=\"mappingMetaFields\"\n multiple\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g g-1-1 ai-center gap-12\"\n *ngIf=\"mappingMetaFields.length\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>Separador</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMetaSeparator\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"mappingMetaWrapSecondInParens\"\n (ngModelChange)=\"onMappingChanged()\"\n >\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\n [(ngModel)]=\"mappingMeta.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of metaTypeConfigs\"\n [value]=\"mt.type\"\n >\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\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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()\"\n ></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()\"\n ></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()\"\n ></praxis-meta-editor-icon>\n }\n @case (\"image\") {\n <praxis-meta-editor-image\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Op\u00E7\u00F5es avan\u00E7adas</mat-panel-title\n ></mat-expansion-panel-header\n >\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\n [(ngModel)]=\"mappingMeta.placement\"\n (ngModelChange)=\"onMappingChanged()\"\n >\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\n matInput\n [(ngModel)]=\"mappingMeta.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMeta.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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>{{\n getTypeIcon(mappingTrailing.type || \"text\")\n }}</mat-icon>\n <span>Trailing (Direita)</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n 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\n [(ngModel)]=\"mappingTrailing.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">-- Nenhum --</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Tipo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of trailingTypeConfigs\"\n [value]=\"mt.type\"\n >\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\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'chip';\n mappingTrailing.chipColor = 'primary';\n mappingTrailing.chipVariant = 'filled';\n mappingTrailing.field = 'status';\n onMappingChanged()\n \"\n >\n Status Chip\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'icon';\n mappingTrailing.field = 'status';\n mappingTrailing.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n Status \u00CDcone\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'currency';\n mappingTrailing.field = 'price';\n mappingTrailing.currencyCode = 'BRL';\n mappingTrailing.locale = 'pt-BR';\n onMappingChanged()\n \"\n >\n Pre\u00E7o\n </button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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\n matInput\n [(ngModel)]=\"mappingTrailing.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n \"\n >URL/expr obrigat\u00F3ria</mat-error\n >\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingTrailing.imageUrl\"\n >\n Defina a URL/expr para renderizar a imagem.\n </div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Estilo</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>Classe</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Style</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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]=\"\n !!mappingLeading.field ||\n (mappingLeading.type === 'icon' && !!mappingLeading.icon) ||\n (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\n \"\n >\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 {{\n mappingLeading.type === \"icon\"\n ? mappingLeading.icon || \"\u00CDcone est\u00E1tico\"\n : mappingLeading.field ||\n (mappingLeading.imageUrl\n ? \"Imagem est\u00E1tica\"\n : \"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\n [(ngModel)]=\"mappingLeading.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of leadingTypeConfigs\"\n [value]=\"mt.type\"\n >\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\n appearance=\"outline\"\n *ngIf=\"\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n \"\n >\n <mat-label>Campo</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\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\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'icon';\n mappingLeading.icon = 'person';\n mappingLeading.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n Avatar \u00CDcone\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'image';\n mappingLeading.imageUrl = 'https://placehold.co/64x64';\n mappingLeading.imageAlt = 'Avatar';\n mappingLeading.badgeText = '${item.status}';\n onMappingChanged()\n \"\n >\n Avatar Imagem + Badge\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'chip';\n mappingLeading.field = 'tag';\n mappingLeading.chipColor = 'accent';\n mappingLeading.chipVariant = 'filled';\n onMappingChanged()\n \"\n >\n Chip Tag\n </button>\n </div>\n\n <!-- Icon Specific -->\n <div\n class=\"g g-1-auto gap-12 ai-center\"\n *ngIf=\"mappingLeading.type === 'icon'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00CDcone</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.icon\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\">\n <mat-icon>search</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"text-caption muted\">\n Use pipe <code>|iconMap</code> no extra pipe para din\u00E2mico\n </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()\"\n ></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div\n class=\"g g-1-1 gap-12\"\n *ngIf=\"mappingLeading.type === 'image'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>URL da Imagem</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"https://... ou ${item.imageUrl}\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Use URL absoluta/relativa ou express\u00E3o ${item.campo}.\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\"\n >URL/expr obrigat\u00F3ria</mat-error\n >\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Alt Text</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageAlt\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Badge Texto</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.badgeText\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></praxis-meta-editor-rating>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Estilo</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>Classe</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingLeading.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Style</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingLeading.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Features -->\n <mat-expansion-panel\n [expanded]=\"featuresVisible && features.length > 0\"\n >\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\n >{{ features.length }} item(s)</mat-panel-description\n >\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\n [(ngModel)]=\"featuresVisible\"\n (ngModelChange)=\"onFeaturesChanged()\"\n >Ativar recursos</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"featuresSyncWithMeta\"\n (ngModelChange)=\"onMappingChanged()\"\n >Sincronizar com Meta</mat-slide-toggle\n >\n <span class=\"flex-1\"></span>\n <mat-button-toggle-group\n [(ngModel)]=\"featuresMode\"\n (change)=\"onFeaturesChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle value=\"icons+labels\"\n ><mat-icon>view_list</mat-icon></mat-button-toggle\n >\n <mat-button-toggle value=\"icons-only\"\n ><mat-icon>more_horiz</mat-icon></mat-button-toggle\n >\n </mat-button-toggle-group>\n </div>\n\n <div\n *ngFor=\"let f of features; let i = index\"\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\">\n <mat-icon>{{ f.icon || \"search\" }}</mat-icon>\n </button>\n <mat-form-field\n appearance=\"outline\"\n class=\"dense-form-field no-sub\"\n >\n <input\n matInput\n [(ngModel)]=\"f.expr\"\n (ngModelChange)=\"onFeaturesChanged()\"\n placeholder=\"Expr/Texto\"\n />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\">\n <mat-icon>add</mat-icon> Adicionar recurso\n </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>{{\n getTypeIcon(mappingSectionHeader.type)\n }}</mat-icon>\n <span>Cabe\u00E7alho de Se\u00E7\u00E3o</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n 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\n [(ngModel)]=\"mappingSectionHeader.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of sectionHeaderTypeConfigs\"\n [value]=\"mt.type\"\n >\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\n matInput\n [(ngModel)]=\"mappingSectionHeader.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'text';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n Texto padr\u00E3o\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'chip';\n mappingSectionHeader.chipColor = 'primary';\n mappingSectionHeader.chipVariant = 'filled';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n Chip padr\u00E3o\n </button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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\n matInput\n [(ngModel)]=\"mappingSectionHeader.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n \"\n >URL/expr obrigat\u00F3ria</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingSectionHeader.imageUrl\"\n >\n Defina a URL/expr para renderizar a imagem.\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingSectionHeader\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Estilo</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>Classe</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Style</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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>{{\n mappingEmptyState.expr || \"Padr\u00E3o\"\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\n [(ngModel)]=\"mappingEmptyState.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of emptyStateTypeConfigs\"\n [value]=\"mt.type\"\n >\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\n matInput\n [(ngModel)]=\"mappingEmptyState.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">Presets</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'text';\n mappingEmptyState.expr = 'Nenhum item dispon\u00EDvel';\n onMappingChanged()\n \"\n >\n Mensagem padr\u00E3o\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'image';\n mappingEmptyState.imageUrl = '/list-empty-state.svg';\n mappingEmptyState.imageAlt = 'Sem resultados';\n onMappingChanged()\n \"\n >\n Imagem padr\u00E3o\n </button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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\n matInput\n [(ngModel)]=\"mappingEmptyState.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n \"\n >URL/expr obrigat\u00F3ria</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingEmptyState.imageUrl\"\n >\n Defina a URL/expr para renderizar a imagem.\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingEmptyState\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >Estilo</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>Classe</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>Style</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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()\">\n Aplicar mapeamento\n </button>\n <button\n mat-button\n (click)=\"inferFromFields()\"\n [disabled]=\"!fields.length\"\n >\n Inferir do schema\n </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\n matInput\n type=\"number\"\n min=\"0\"\n [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\"\n />\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\n [(ngModel)]=\"skinPreviewTheme\"\n (change)=\"onSkinChanged()\"\n appearance=\"legacy\"\n >\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\n [config]=\"working\"\n [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"\n ></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"i18n/A11y\">\n <div\n class=\"editor-content grid gap-3\"\n *ngIf=\"working?.a11y && working?.events\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>Locale padr\u00E3o</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.locale\"\n (ngModelChange)=\"markDirty()\"\n placeholder=\"ex.: pt-BR\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Moeda padr\u00E3o</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.currency\"\n (ngModelChange)=\"markDirty()\"\n placeholder=\"ex.: BRL\"\n />\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\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabel\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>aria-labelledby</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabelledBy\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.highContrast\"\n (ngModelChange)=\"markDirty()\"\n >Alto contraste</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.reduceMotion\"\n (ngModelChange)=\"markDirty()\"\n >Reduzir movimento</mat-slide-toggle\n >\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\n matInput\n [(ngModel)]=\"working!.events!.itemClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.actionClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.selectionChange\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.loaded\"\n (ngModelChange)=\"markDirty()\"\n />\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\n [(ngModel)]=\"working.selection.mode\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\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\n matInput\n [(ngModel)]=\"working.selection.formControlName\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n >\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\n matInput\n [(ngModel)]=\"working.selection.formControlPath\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n >\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\n matInput\n [(ngModel)]=\"working.selection.compareBy\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"Chave unica do item.\"\n >\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\n [(ngModel)]=\"working.selection.return\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\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')\">\n Pill Soft\n </button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">\n Gradient Tile\n </button>\n <button mat-button (click)=\"applySkinPreset('glass')\">Glass</button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">\n Elevated\n </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')\">\n Neumorphism\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>Estilo</mat-label>\n <mat-select\n [(ngModel)]=\"working.skin.type\"\n (ngModelChange)=\"onSkinTypeChanged($event)\"\n >\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\n matInput\n [(ngModel)]=\"working.skin.radius\"\n (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 1.25rem\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Sombra</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.shadow\"\n (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: var(--md-sys-elevation-level2)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Borda</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.border\"\n (ngModelChange)=\"onSkinChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n *ngIf=\"working.skin.type === 'glass'\"\n appearance=\"outline\"\n >\n <mat-label>Desfoque</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.backdropBlur\"\n (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: 8px\"\n />\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\n matInput\n [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Degrad\u00EA at\u00E9</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>\u00C2ngulo</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\"\n />\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\n matInput\n [(ngModel)]=\"working.skin.class\"\n (ngModelChange)=\"onSkinChanged()\"\n placeholder=\"ex.: my-list-skin\"\n />\n </mat-form-field>\n\n <div\n *ngIf=\"working.skin.type === 'custom'\"\n class=\"g g-auto-220 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>Estilo inline (skin.inlineStyle)</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"working.skin.inlineStyle\"\n (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"\n ></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\">\n.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre\n >\n </div>\n </div>\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"] }]
|
|
5732
|
+
SurfaceOpenActionEditorComponent,
|
|
5733
|
+
], template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">\n {{ tx('Assistant-applied adjustments replace the entire configuration object.') }}\n </div>\n <button\n mat-icon-button\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Resource (API)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.dataSource.resourcePath\"\n (ngModelChange)=\"onResourcePathChange($event)\"\n [placeholder]=\"tx('e.g.: users')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n >\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>{{ tx('Query (JSON)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [(ngModel)]=\"queryJson\"\n (ngModelChange)=\"onQueryChanged($event)\"\n [placeholder]=\"queryPlaceholder()\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n *ngIf=\"!queryError\"\n >\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>{{ tx('Sort by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortField\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortDir\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('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 >\n </praxis-list-json-config-editor>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('Actions')\">\n <div class=\"editor-content g gap-12\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">\n {{ tx('Configure per-item action buttons (icon, label, color, visibility)') }}\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addAction()\">\n {{ tx('Add action') }}\n </button>\n </div>\n <div class=\"g g-1-auto gap-8 ai-center\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Global action (Praxis)') }}</mat-label>\n <mat-select\n [(ngModel)]=\"selectedGlobalActionId\"\n (ngModelChange)=\"onGlobalActionSelected($event)\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- Select --') }}</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\n *ngIf=\"!globalActionCatalog.length\"\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a global `command`.') }}\n </div>\n </div>\n <div\n *ngFor=\"let a of working.actions || []; let i = index\"\n class=\"g g-auto-200 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('ID') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.id\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action type') }}</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">{{ tx('Icon') }}</mat-option>\n <mat-option value=\"button\">{{ tx('Button') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.icon\"\n (ngModelChange)=\"onActionsChanged()\"\n [placeholder]=\"tx('e.g.: edit, delete')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Command (global)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.command\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder=\"global:toast.success\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.label\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Variant') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.buttonVariant\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option value=\"stroked\">{{ tx('Outlined') }}</mat-option>\n <mat-option value=\"raised\">{{ tx('Raised') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Filled') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action color') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span\n class=\"color-dot\"\n [style.background]=\"colorDotBackground(c.value)\"\n ></span\n >{{ tx(c.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g gap-8\"\n *ngIf=\"isCustomColor(a.color); else actionCustomBtn\"\n >\n <pdx-color-picker\n [label]=\"tx('Custom color')\"\n [format]=\"'hex'\"\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n ></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action payload') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.emitPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Payload emitted by the action.')\"\n >\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>{{ actionVisibilityLabel() }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.showIf\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n >\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\n *ngIf=\"(a.kind || 'icon') === 'icon'\"\n mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n >\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button\n *ngIf=\"a.buttonVariant === 'stroked'\"\n mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'stroked')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"a.buttonVariant === 'raised'\"\n mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'raised')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\"\n mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'flat')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n </ng-container>\n <span class=\"muted\">{{ tx('Preview') }}</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle\n [(ngModel)]=\"a.showLoading\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Show loading') }}</mat-slide-toggle\n >\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >{{ tx('Confirmation') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">{{ tx('Type') }}</span>\n <mat-button-toggle-group\n [value]=\"a.confirmation?.type || ''\"\n (change)=\"applyConfirmationPreset(a, $event.value)\"\n >\n <mat-button-toggle value=\"\">{{ tx('Default') }}</mat-button-toggle>\n <mat-button-toggle value=\"danger\">{{ tx('Danger') }}</mat-button-toggle>\n <mat-button-toggle value=\"warning\">{{ tx('Warning') }}</mat-button-toggle>\n <mat-button-toggle value=\"info\">{{ tx('Info') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.title\"\n (ngModelChange)=\"setConfirmationField(a, 'title', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.message\"\n (ngModelChange)=\"setConfirmationField(a, 'message', $event)\"\n />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">{{ tx('Preview') }}</div>\n <div class=\"text-caption\">\n <strong>{{\n a.confirmation?.title || tx('Confirm action')\n }}</strong>\n </div>\n <div class=\"text-caption muted\">\n {{\n a.confirmation?.message ||\n tx('Are you sure you want to continue?')\n }}\n </div>\n <div class=\"text-caption\">\n <span\n class=\"confirm-type\"\n [ngClass]=\"a.confirmation?.type || 'default'\"\n >{{ tx('Type') }}:\n {{ a.confirmation?.type || tx('Default').toLowerCase() }}</span\n >\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <ng-container *ngIf=\"isSurfaceOpenCommand(a); else defaultGlobalPayloadEditor\">\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n </ng-container>\n <ng-template #defaultGlobalPayloadEditor>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload (JSON/Template)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"a.globalPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\"\n >{{ tx('Invalid JSON') }}</mat-error\n >\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalPayloadExampleHint(a)\n }}</span>\n </div>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('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')\">\n {{ tx('Modern tiles preset') }}\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Variant') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.variant\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"list\">{{ tx('List') }}</mat-option>\n <mat-option value=\"cards\">{{ tx('Cards') }}</mat-option>\n <mat-option value=\"tiles\">{{ tx('Tiles') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Model') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.model\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <ng-container\n *ngIf=\"working.layout.variant === 'list'; else cardModels\"\n >\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Media on the left') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel (large media)') }}</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container\n *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\"\n >\n <mat-option value=\"standard\">{{ tx('Standard tile') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Tile with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel tile') }}</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Card with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('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>{{ tx('Lines') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.lines\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\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>{{ tx('Items per page') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Density') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.density\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"default\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"comfortable\">{{ tx('Comfortable') }}</mat-option>\n <mat-option value=\"compact\">{{ tx('Compact') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Spacing between items') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.itemSpacing\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No extra space') }}</mat-option>\n <mat-option value=\"tight\">{{ tx('Tight') }}</mat-option>\n <mat-option value=\"default\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"relaxed\">{{ tx('Relaxed') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"working.layout.variant !== 'tiles'\"\n >\n <mat-label>{{ tx('Dividers') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.dividers\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('None') }}</mat-option>\n <mat-option value=\"between\">{{ tx('Between groups') }}</mat-option>\n <mat-option value=\"all\">{{ tx('All') }}</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>{{ tx('Group by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"\">{{ tx('None') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</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>{{ tx('Group by') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n [placeholder]=\"tx('e.g.: department')\"\n />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.stickySectionHeader\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Sticky section header') }}\n </mat-slide-toggle>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.virtualScroll\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Virtual scroll') }}\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('List tools') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSearch\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show search') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSort\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show sorting') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showRange\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show total X-Y range') }}</mat-slide-toggle\n >\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngIf=\"working.ui?.showSearch\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field to search') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.ui.searchField\"\n (ngModelChange)=\"onUiChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Search placeholder') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.ui.searchPlaceholder\"\n (ngModelChange)=\"onUiChanged()\"\n [placeholder]=\"tx('e.g.: Search by title')\"\n />\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\">\n {{ tx('Sorting options (label \u2192 field+direction)') }}\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">\n {{ tx('Add option') }}\n </button>\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngFor=\"let r of uiSortRows; let i = index\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"r.label\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n [placeholder]=\"tx('e.g.: Most recent')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.field\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.dir\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('Content')\">\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>{{ tx('Primary (Title)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingPrimary.field || tx('Not mapped')\n }}</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\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'name';\n onMappingChanged()\n \"\n >\n {{ tx('Name') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'title';\n onMappingChanged()\n \"\n >\n {{ tx('Title') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingPrimary.type = 'text';\n mappingPrimary.field = 'name';\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'role';\n onMappingChanged()\n \"\n >\n {{ tx('Name + role') }}\n </button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of primaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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>{{ tx('Secondary (Summary)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSecondary.field || tx('Not mapped')\n }}</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\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'subtitle';\n onMappingChanged()\n \"\n >\n {{ tx('Subtitle') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'date';\n mappingSecondary.field = 'hireDate';\n mappingSecondary.dateStyle = 'short';\n onMappingChanged()\n \"\n >\n {{ tx('Short date') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applySalaryPreset()\"\n >\n {{ tx('Salary') }}\n </button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of secondaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</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\"\n ><mat-label>{{ tx('CSS class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Inline style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel\n [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingMeta.type || \"text\")\n }}</mat-icon>\n <span>{{ tx('Meta (Detail/Side)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingMetaFields.length\n ? tx('Composed field ({{count}})', { count: mappingMetaFields.length })\n : mappingMeta.field || tx('Not mapped')\n }}\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\">{{ tx('Composition mode') }}</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Fields to compose (multi-select)') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMetaFields\"\n multiple\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g g-1-1 ai-center gap-12\"\n *ngIf=\"mappingMetaFields.length\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Separator') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMetaSeparator\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"mappingMetaWrapSecondInParens\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n {{ tx('(Second) in parentheses') }}\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>{{ tx('Single field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- None --') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of metaTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(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\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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()\"\n ></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()\"\n ></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()\"\n ></praxis-meta-editor-icon>\n }\n @case (\"image\") {\n <praxis-meta-editor-image\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Advanced options') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Position') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.placement\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option value=\"side\">{{ tx('Side (right)') }}</mat-option>\n <mat-option value=\"line\">{{ tx('Inline (below)') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMeta.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMeta.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\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>{{\n getTypeIcon(mappingTrailing.type || \"text\")\n }}</mat-icon>\n <span>{{ tx('Trailing (right)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingTrailing.field || tx('Not mapped')\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>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- None --') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of trailingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(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\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'chip';\n mappingTrailing.chipColor = 'primary';\n mappingTrailing.chipVariant = 'filled';\n mappingTrailing.field = 'status';\n onMappingChanged()\n \"\n >\n {{ tx('Status chip') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'icon';\n mappingTrailing.field = 'status';\n mappingTrailing.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Status icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyPricePreset()\"\n >\n {{ tx('Price') }}\n </button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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>{{ tx('URL / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingTrailing.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingTrailing.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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]=\"\n !!mappingLeading.field ||\n (mappingLeading.type === 'icon' && !!mappingLeading.icon) ||\n (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\n \"\n >\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>{{ tx('Leading (left)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingLeading.type === \"icon\"\n ? mappingLeading.icon || tx('Static icon')\n : mappingLeading.field ||\n (mappingLeading.imageUrl\n ? tx('Static image')\n : tx('Not mapped'))\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>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of leadingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(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\n appearance=\"outline\"\n *ngIf=\"\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n \"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\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\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'icon';\n mappingLeading.icon = 'person';\n mappingLeading.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'image';\n mappingLeading.imageUrl = 'https://placehold.co/64x64';\n mappingLeading.imageAlt = 'Avatar';\n mappingLeading.badgeText = '${item.status}';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar image + badge') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'chip';\n mappingLeading.field = 'tag';\n mappingLeading.chipColor = 'accent';\n mappingLeading.chipVariant = 'filled';\n onMappingChanged()\n \"\n >\n {{ tx('Tag chip') }}\n </button>\n </div>\n\n <!-- Icon Specific -->\n <div\n class=\"g g-1-auto gap-12 ai-center\"\n *ngIf=\"mappingLeading.type === 'icon'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.icon\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\">\n <mat-icon>search</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"text-caption muted\">\n {{ tx('Use the `|iconMap` pipe in the extra pipe for dynamic rendering.') }}\n </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()\"\n ></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div\n class=\"g g-1-1 gap-12\"\n *ngIf=\"mappingLeading.type === 'image'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Image URL') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Alt text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageAlt\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Badge text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.badgeText\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></praxis-meta-editor-rating>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingLeading.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingLeading.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Features -->\n <mat-expansion-panel\n [expanded]=\"featuresVisible && features.length > 0\"\n >\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>{{ tx('Features') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description\n >{{ features.length }} item(s)</mat-panel-description\n >\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\n [(ngModel)]=\"featuresVisible\"\n (ngModelChange)=\"onFeaturesChanged()\"\n >{{ tx('Enable features') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"featuresSyncWithMeta\"\n (ngModelChange)=\"onMappingChanged()\"\n >{{ tx('Sync with Meta') }}</mat-slide-toggle\n >\n <span class=\"flex-1\"></span>\n <mat-button-toggle-group\n [(ngModel)]=\"featuresMode\"\n (change)=\"onFeaturesChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle value=\"icons+labels\"\n ><mat-icon>view_list</mat-icon></mat-button-toggle\n >\n <mat-button-toggle value=\"icons-only\"\n ><mat-icon>more_horiz</mat-icon></mat-button-toggle\n >\n </mat-button-toggle-group>\n </div>\n\n <div\n *ngFor=\"let f of features; let i = index\"\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\">\n <mat-icon>{{ f.icon || \"search\" }}</mat-icon>\n </button>\n <mat-form-field\n appearance=\"outline\"\n class=\"dense-form-field no-sub\"\n >\n <input\n matInput\n [(ngModel)]=\"f.expr\"\n (ngModelChange)=\"onFeaturesChanged()\"\n [placeholder]=\"tx('Expression/Text')\"\n />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\">\n <mat-icon>add</mat-icon> {{ tx('Add feature') }}\n </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>{{\n getTypeIcon(mappingSectionHeader.type)\n }}</mat-icon>\n <span>{{ tx('Section header') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSectionHeader.expr || tx('Not configured')\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>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSectionHeader.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of sectionHeaderTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Expression (item.key)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingSectionHeader.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'text';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default text') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'chip';\n mappingSectionHeader.chipColor = 'primary';\n mappingSectionHeader.chipVariant = 'filled';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default chip') }}\n </button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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>{{ tx('Image URL') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingSectionHeader.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingSectionHeader.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingSectionHeader\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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>{{ tx('Empty state') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingEmptyState.expr || tx('Default')\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>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingEmptyState.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of emptyStateTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingEmptyState.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'text';\n mappingEmptyState.expr = tx('No items available');\n onMappingChanged()\n \"\n >\n {{ tx('Default message') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'image';\n mappingEmptyState.imageUrl = '/list-empty-state.svg';\n mappingEmptyState.imageAlt = tx('No results');\n onMappingChanged()\n \"\n >\n {{ tx('Default image') }}\n </button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\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()\"\n ></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()\"\n ></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()\"\n ></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>{{ tx('Image URL') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingEmptyState.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingEmptyState\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></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()\">\n {{ tx('Apply mapping') }}\n </button>\n <button\n mat-button\n (click)=\"inferFromFields()\"\n [disabled]=\"!fields.length\"\n >\n {{ tx('Infer from schema') }}\n </button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Skeleton count') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"0\"\n [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\"\n />\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\">{{ tx('Theme preview') }}</span>\n <mat-button-toggle-group\n [(ngModel)]=\"skinPreviewTheme\"\n (change)=\"onSkinChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle [value]=\"'light'\">{{ tx('Light') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">{{ tx('Dark') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">{{ tx('Grid') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview\n [config]=\"working\"\n [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"\n ></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <div\n class=\"editor-content grid gap-3\"\n *ngIf=\"working?.a11y && working?.events\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default locale') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.locale\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"tx('e.g.: en-US')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default currency') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.currency\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"currencyPlaceholder()\"\n />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Accessibility') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabel\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-labelledby') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabelledBy\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.highContrast\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('High contrast') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.reduceMotion\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('Reduce motion') }}</mat-slide-toggle\n >\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Events') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('itemClick') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.itemClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.actionClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.selectionChange\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.loaded\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.mode\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No selection') }}</mat-option>\n <mat-option value=\"single\">{{ tx('Single') }}</mat-option>\n <mat-option value=\"multiple\">{{ tx('Multiple') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form name') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlName\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form path') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlPath\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Compare by (field)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.compareBy\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Return') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.return\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\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')\">\n {{ tx('Pill Soft') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">\n {{ tx('Gradient Tile') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('glass')\">\n {{ tx('Glass') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">\n {{ tx('Elevated') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('outline')\">\n {{ tx('Outline') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('flat')\">\n {{ tx('Flat') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">\n {{ tx('Neumorphism') }}\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Style') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.skin.type\"\n (ngModelChange)=\"onSkinTypeChanged($event)\"\n >\n <mat-option value=\"pill-soft\">{{ tx('Pill Soft') }}</mat-option>\n <mat-option value=\"gradient-tile\">{{ tx('Gradient Tile') }}</mat-option>\n <mat-option value=\"glass\">{{ tx('Glass') }}</mat-option>\n <mat-option value=\"elevated\">{{ tx('Elevated') }}</mat-option>\n <mat-option value=\"outline\">{{ tx('Outline') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Flat') }}</mat-option>\n <mat-option value=\"neumorphism\">{{ tx('Neumorphism') }}</mat-option>\n <mat-option value=\"custom\">{{ tx('Custom') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Radius') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.radius\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 1.25rem')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shadow') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.shadow\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: var(--md-sys-elevation-level2)')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.border\"\n (ngModelChange)=\"onSkinChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n *ngIf=\"working.skin.type === 'glass'\"\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Blur') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.backdropBlur\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 8px')\"\n />\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>{{ tx('Gradient from') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient to') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Angle') }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\"\n />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Extra CSS class (skin.class)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.class\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: my-list-skin')\"\n />\n </mat-form-field>\n\n <div\n *ngIf=\"working.skin.type === 'custom'\"\n class=\"g g-auto-220 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Inline style (skin.inlineStyle)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"working.skin.inlineStyle\"\n (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"\n ></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n {{ tx('CSS class example (add this to your global styles):') }}\n <pre class=\"code-block\">\n.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre\n >\n </div>\n </div>\n </div>\n </mat-tab>\n</mat-tab-group>\n\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"] }]
|
|
5016
5734
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5017
5735
|
type: Optional
|
|
5018
5736
|
}, {
|
|
@@ -5943,34 +6661,6 @@ class ListAiAdapter extends BaseAiAdapter {
|
|
|
5943
6661
|
}
|
|
5944
6662
|
}
|
|
5945
6663
|
|
|
5946
|
-
const PRAXIS_LIST_EN_US = {
|
|
5947
|
-
configEdit: 'Edit configuration',
|
|
5948
|
-
expandDetails: 'Expand details',
|
|
5949
|
-
collapseDetails: 'Collapse details',
|
|
5950
|
-
itemDetails: 'Item details',
|
|
5951
|
-
emptyState: 'No items available',
|
|
5952
|
-
};
|
|
5953
|
-
|
|
5954
|
-
const PRAXIS_LIST_PT_BR = {
|
|
5955
|
-
configEdit: 'Editar configurações',
|
|
5956
|
-
expandDetails: 'Expandir detalhes',
|
|
5957
|
-
collapseDetails: 'Recolher detalhes',
|
|
5958
|
-
itemDetails: 'Detalhes do item',
|
|
5959
|
-
emptyState: 'Nenhum item disponível',
|
|
5960
|
-
};
|
|
5961
|
-
|
|
5962
|
-
const PRAXIS_LIST_I18N_NAMESPACE = 'praxis-list';
|
|
5963
|
-
function providePraxisListI18n() {
|
|
5964
|
-
return providePraxisI18n({
|
|
5965
|
-
namespaces: {
|
|
5966
|
-
[PRAXIS_LIST_I18N_NAMESPACE]: {
|
|
5967
|
-
'pt-BR': PRAXIS_LIST_PT_BR,
|
|
5968
|
-
'en-US': PRAXIS_LIST_EN_US,
|
|
5969
|
-
},
|
|
5970
|
-
},
|
|
5971
|
-
});
|
|
5972
|
-
}
|
|
5973
|
-
|
|
5974
6664
|
class PraxisListMetricComponent {
|
|
5975
6665
|
metric;
|
|
5976
6666
|
metricClasses() {
|
|
@@ -6964,6 +7654,7 @@ const ROW_LAYOUT_SUPPORTED_SLOTS = [
|
|
|
6964
7654
|
class PraxisList {
|
|
6965
7655
|
static nextSkinScopeId = 0;
|
|
6966
7656
|
customizationEnabled = false;
|
|
7657
|
+
hostLocale = inject(LOCALE_ID, { optional: true }) ?? 'en-US';
|
|
6967
7658
|
config;
|
|
6968
7659
|
listId;
|
|
6969
7660
|
componentInstanceId;
|
|
@@ -7363,7 +8054,7 @@ class PraxisList {
|
|
|
7363
8054
|
.replace(/[^a-z0-9_-]+/g, '-');
|
|
7364
8055
|
}
|
|
7365
8056
|
featureLabel(item, expr) {
|
|
7366
|
-
const out = evaluateTemplate({ type: 'text', expr: expr || '' }, item);
|
|
8057
|
+
const out = evaluateTemplate({ type: 'text', expr: expr || '' }, item, this.templateEvaluatorOptions());
|
|
7367
8058
|
return out?.value ?? '';
|
|
7368
8059
|
}
|
|
7369
8060
|
plainText(value) {
|
|
@@ -8153,7 +8844,7 @@ class PraxisList {
|
|
|
8153
8844
|
};
|
|
8154
8845
|
}
|
|
8155
8846
|
if (def.type === 'icon') {
|
|
8156
|
-
const out = evaluateTemplate(def, item);
|
|
8847
|
+
const out = evaluateTemplate(def, item, this.templateEvaluatorOptions());
|
|
8157
8848
|
// Optional color mapping from config.templating.iconColorMap
|
|
8158
8849
|
try {
|
|
8159
8850
|
const map = this.config?.templating?.iconColorMap;
|
|
@@ -8178,7 +8869,7 @@ class PraxisList {
|
|
|
8178
8869
|
catch { }
|
|
8179
8870
|
return out;
|
|
8180
8871
|
}
|
|
8181
|
-
return evaluateTemplate(def, item);
|
|
8872
|
+
return evaluateTemplate(def, item, this.templateEvaluatorOptions());
|
|
8182
8873
|
}
|
|
8183
8874
|
buildUnsupportedComposeNode(def) {
|
|
8184
8875
|
return {
|
|
@@ -8366,9 +9057,15 @@ class PraxisList {
|
|
|
8366
9057
|
return [spec.slice(0, idx).trim(), spec.slice(idx + 1).trim()];
|
|
8367
9058
|
}
|
|
8368
9059
|
evalString(expr, ctx) {
|
|
8369
|
-
const res = evaluateTemplate({ type: 'text', expr }, ctx?.item ?? ctx);
|
|
9060
|
+
const res = evaluateTemplate({ type: 'text', expr }, ctx?.item ?? ctx, this.templateEvaluatorOptions());
|
|
8370
9061
|
return res?.value ?? '';
|
|
8371
9062
|
}
|
|
9063
|
+
templateEvaluatorOptions() {
|
|
9064
|
+
return {
|
|
9065
|
+
i18n: this.config?.i18n,
|
|
9066
|
+
appLocale: this.hostLocale,
|
|
9067
|
+
};
|
|
9068
|
+
}
|
|
8372
9069
|
parseTwoParams(param) {
|
|
8373
9070
|
if (!param)
|
|
8374
9071
|
return [undefined, undefined];
|
|
@@ -10207,4 +10904,3 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
10207
10904
|
*/
|
|
10208
10905
|
|
|
10209
10906
|
export { ExecutiveAlertsComponent, ExecutiveBadgeComponent, ExecutiveOwnerComponent, LIST_AI_CAPABILITIES, ListDataService, ListSkinService, PRAXIS_LIST_COMPONENT_METADATA, PRAXIS_LIST_EN_US, PRAXIS_LIST_I18N_NAMESPACE, PRAXIS_LIST_PT_BR, PraxisList, PraxisListConfigEditor, PraxisListDocPageComponent, PraxisListJsonConfigEditorComponent, adaptSelection, buildListApplyPlan, createListAuthoringDocument, evalExpr, evaluateTemplate, inferListAuthoringDocument, inferTemplatingFromSchema, normalizeListActionPayloads, normalizeListAuthoringDocument, normalizeListConfig, parseLegacyOrListDocument, projectListAuthoringDocument, providePraxisListI18n, providePraxisListMetadata, serializeListAuthoringDocument, toCanonicalListConfig, validateListAuthoringDocument };
|
|
10210
|
-
//# sourceMappingURL=praxisui-list.mjs.map
|