@praxisui/list 9.0.0-beta.7 → 9.0.0-beta.8
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.
|
@@ -4201,6 +4201,10 @@ class PraxisListConfigEditor {
|
|
|
4201
4201
|
];
|
|
4202
4202
|
rowLayoutColumnAlignOptions = ['start', 'center', 'end'];
|
|
4203
4203
|
rowLayoutJustifyOptions = ['start', 'center', 'end', 'stretch'];
|
|
4204
|
+
rowLayoutStructuralSlots = new Set([
|
|
4205
|
+
'actions',
|
|
4206
|
+
'expand',
|
|
4207
|
+
]);
|
|
4204
4208
|
exportFormatOptions = [
|
|
4205
4209
|
'csv',
|
|
4206
4210
|
'json',
|
|
@@ -4229,6 +4233,33 @@ class PraxisListConfigEditor {
|
|
|
4229
4233
|
];
|
|
4230
4234
|
expansionShellOptions = ['attached', 'detached', 'modal'];
|
|
4231
4235
|
expansionColumnOptions = [1, 2, 3];
|
|
4236
|
+
ruleSlotOptions = [
|
|
4237
|
+
'leading',
|
|
4238
|
+
'primary',
|
|
4239
|
+
'secondary',
|
|
4240
|
+
'meta',
|
|
4241
|
+
'trailing',
|
|
4242
|
+
'identity',
|
|
4243
|
+
'balance',
|
|
4244
|
+
'limit',
|
|
4245
|
+
'risk',
|
|
4246
|
+
'alerts',
|
|
4247
|
+
'owner',
|
|
4248
|
+
];
|
|
4249
|
+
ruleTemplateTypeOptions = [
|
|
4250
|
+
'text',
|
|
4251
|
+
'chip',
|
|
4252
|
+
'icon',
|
|
4253
|
+
'image',
|
|
4254
|
+
'rating',
|
|
4255
|
+
'currency',
|
|
4256
|
+
'date',
|
|
4257
|
+
'html',
|
|
4258
|
+
'slot',
|
|
4259
|
+
'metric',
|
|
4260
|
+
'compose',
|
|
4261
|
+
'component',
|
|
4262
|
+
];
|
|
4232
4263
|
// Meta composition (multi-field)
|
|
4233
4264
|
mappingMetaFields = [];
|
|
4234
4265
|
mappingMetaSeparator = ' • ';
|
|
@@ -5141,6 +5172,35 @@ class PraxisListConfigEditor {
|
|
|
5141
5172
|
this.markDirty();
|
|
5142
5173
|
this.verify();
|
|
5143
5174
|
}
|
|
5175
|
+
rowLayoutColumnWarning(column) {
|
|
5176
|
+
const slot = column?.slot;
|
|
5177
|
+
if (!slot) {
|
|
5178
|
+
return this.tx('Select a slot for this column.');
|
|
5179
|
+
}
|
|
5180
|
+
if (!this.rowLayoutSlotOptions.includes(slot)) {
|
|
5181
|
+
return this.tx('This slot is not supported by the list runtime.');
|
|
5182
|
+
}
|
|
5183
|
+
if (this.rowLayoutStructuralSlots.has(slot)) {
|
|
5184
|
+
return '';
|
|
5185
|
+
}
|
|
5186
|
+
const config = this.buildCurrentDocument().config;
|
|
5187
|
+
if (this.rowLayoutSlotHasMaterialization(slot, config)) {
|
|
5188
|
+
return '';
|
|
5189
|
+
}
|
|
5190
|
+
return this.tx('This slot has no template or conditional override yet.');
|
|
5191
|
+
}
|
|
5192
|
+
rowLayoutSlotHasMaterialization(slot, config) {
|
|
5193
|
+
const templating = config?.templating;
|
|
5194
|
+
if (templating?.[slot])
|
|
5195
|
+
return true;
|
|
5196
|
+
return (config?.rules?.slotOverrides || []).some((rule) => {
|
|
5197
|
+
if (rule?.slot !== slot)
|
|
5198
|
+
return false;
|
|
5199
|
+
if (rule.template)
|
|
5200
|
+
return true;
|
|
5201
|
+
return Array.isArray(rule.effects) && rule.effects.some((effect) => !!effect?.template);
|
|
5202
|
+
});
|
|
5203
|
+
}
|
|
5144
5204
|
ensureRowLayoutDraft(draft) {
|
|
5145
5205
|
draft.layout ||= {};
|
|
5146
5206
|
draft.layout.variant = 'list';
|
|
@@ -5343,6 +5403,27 @@ class PraxisListConfigEditor {
|
|
|
5343
5403
|
const includeFields = this.working?.export?.general?.includeFields;
|
|
5344
5404
|
return Array.isArray(includeFields) ? includeFields.join(', ') : '';
|
|
5345
5405
|
}
|
|
5406
|
+
getExportIncludeFieldsWarning() {
|
|
5407
|
+
const unknown = this.getExportUnknownIncludeFields();
|
|
5408
|
+
if (!unknown.length)
|
|
5409
|
+
return '';
|
|
5410
|
+
return this.tx('Unknown export fields: {{fields}}', {
|
|
5411
|
+
fields: unknown.join(', '),
|
|
5412
|
+
});
|
|
5413
|
+
}
|
|
5414
|
+
getExportUnknownIncludeFields() {
|
|
5415
|
+
const includeFields = this.working?.export?.general?.includeFields;
|
|
5416
|
+
if (!Array.isArray(includeFields) || !includeFields.length)
|
|
5417
|
+
return [];
|
|
5418
|
+
const knownFields = new Set((this.fields || [])
|
|
5419
|
+
.map((field) => String(field || '').trim())
|
|
5420
|
+
.filter(Boolean));
|
|
5421
|
+
if (!knownFields.size)
|
|
5422
|
+
return [];
|
|
5423
|
+
return includeFields
|
|
5424
|
+
.map((field) => String(field || '').trim())
|
|
5425
|
+
.filter((field) => !!field && !knownFields.has(field));
|
|
5426
|
+
}
|
|
5346
5427
|
onExportIncludeFieldsTextChange(value) {
|
|
5347
5428
|
const fields = String(value || '')
|
|
5348
5429
|
.split(',')
|
|
@@ -5532,6 +5613,134 @@ class PraxisListConfigEditor {
|
|
|
5532
5613
|
emptyLabel: this.tx('No details available'),
|
|
5533
5614
|
};
|
|
5534
5615
|
}
|
|
5616
|
+
addItemStyleRule() {
|
|
5617
|
+
this.working = produce(this.working, (draft) => {
|
|
5618
|
+
draft.rules ||= {};
|
|
5619
|
+
draft.rules.itemStyles ||= [];
|
|
5620
|
+
draft.rules.itemStyles.push(this.defaultItemStyleRule(draft.rules.itemStyles.length));
|
|
5621
|
+
});
|
|
5622
|
+
this.markDirty();
|
|
5623
|
+
this.verify();
|
|
5624
|
+
}
|
|
5625
|
+
removeItemStyleRule(index) {
|
|
5626
|
+
this.working = produce(this.working, (draft) => {
|
|
5627
|
+
const rules = draft.rules?.itemStyles;
|
|
5628
|
+
if (!Array.isArray(rules))
|
|
5629
|
+
return;
|
|
5630
|
+
rules.splice(index, 1);
|
|
5631
|
+
});
|
|
5632
|
+
this.markDirty();
|
|
5633
|
+
this.verify();
|
|
5634
|
+
}
|
|
5635
|
+
addSlotOverrideRule() {
|
|
5636
|
+
this.working = produce(this.working, (draft) => {
|
|
5637
|
+
draft.rules ||= {};
|
|
5638
|
+
draft.rules.slotOverrides ||= [];
|
|
5639
|
+
draft.rules.slotOverrides.push(this.defaultSlotOverrideRule(draft.rules.slotOverrides.length));
|
|
5640
|
+
});
|
|
5641
|
+
this.markDirty();
|
|
5642
|
+
this.verify();
|
|
5643
|
+
}
|
|
5644
|
+
removeSlotOverrideRule(index) {
|
|
5645
|
+
this.working = produce(this.working, (draft) => {
|
|
5646
|
+
const rules = draft.rules?.slotOverrides;
|
|
5647
|
+
if (!Array.isArray(rules))
|
|
5648
|
+
return;
|
|
5649
|
+
rules.splice(index, 1);
|
|
5650
|
+
});
|
|
5651
|
+
this.markDirty();
|
|
5652
|
+
this.verify();
|
|
5653
|
+
}
|
|
5654
|
+
onRulesChanged() {
|
|
5655
|
+
this.markDirty();
|
|
5656
|
+
this.verify();
|
|
5657
|
+
}
|
|
5658
|
+
getRuleConditionText(rule) {
|
|
5659
|
+
return rule.condition ? JSON.stringify(rule.condition, null, 2) : '';
|
|
5660
|
+
}
|
|
5661
|
+
onRuleConditionChanged(rule, raw) {
|
|
5662
|
+
const parsed = this.parseJsonLogicEditorValue(raw);
|
|
5663
|
+
if (!parsed.valid)
|
|
5664
|
+
return;
|
|
5665
|
+
rule.condition = parsed.value;
|
|
5666
|
+
this.onRulesChanged();
|
|
5667
|
+
}
|
|
5668
|
+
getItemStyleEffect(rule) {
|
|
5669
|
+
return Array.isArray(rule.effects) ? rule.effects[0] : undefined;
|
|
5670
|
+
}
|
|
5671
|
+
ensureItemStyleEffect(rule) {
|
|
5672
|
+
rule.effects = [
|
|
5673
|
+
{
|
|
5674
|
+
kind: 'item-style',
|
|
5675
|
+
class: rule.class,
|
|
5676
|
+
style: rule.style,
|
|
5677
|
+
border: rule.border,
|
|
5678
|
+
background: rule.background,
|
|
5679
|
+
},
|
|
5680
|
+
];
|
|
5681
|
+
this.onRulesChanged();
|
|
5682
|
+
}
|
|
5683
|
+
clearItemStyleEffects(rule) {
|
|
5684
|
+
rule.effects = undefined;
|
|
5685
|
+
this.onRulesChanged();
|
|
5686
|
+
}
|
|
5687
|
+
getSlotOverrideEffect(rule) {
|
|
5688
|
+
return Array.isArray(rule.effects) ? rule.effects[0] : undefined;
|
|
5689
|
+
}
|
|
5690
|
+
ensureSlotOverrideEffect(rule) {
|
|
5691
|
+
rule.effects = [
|
|
5692
|
+
{
|
|
5693
|
+
kind: 'slot-override',
|
|
5694
|
+
template: rule.template,
|
|
5695
|
+
class: rule.class,
|
|
5696
|
+
style: rule.style,
|
|
5697
|
+
hide: rule.hide,
|
|
5698
|
+
},
|
|
5699
|
+
];
|
|
5700
|
+
this.onRulesChanged();
|
|
5701
|
+
}
|
|
5702
|
+
clearSlotOverrideEffects(rule) {
|
|
5703
|
+
rule.effects = undefined;
|
|
5704
|
+
this.onRulesChanged();
|
|
5705
|
+
}
|
|
5706
|
+
getSlotOverrideTemplateExpr(rule) {
|
|
5707
|
+
return rule.template?.expr || '';
|
|
5708
|
+
}
|
|
5709
|
+
onSlotOverrideTemplateExprChanged(rule, expr) {
|
|
5710
|
+
const value = String(expr || '').trim();
|
|
5711
|
+
rule.template = value ? { ...(rule.template || { type: 'text' }), expr: value } : undefined;
|
|
5712
|
+
this.onRulesChanged();
|
|
5713
|
+
}
|
|
5714
|
+
getSlotOverrideTemplateType(rule) {
|
|
5715
|
+
return (rule.template?.type || 'text');
|
|
5716
|
+
}
|
|
5717
|
+
onSlotOverrideTemplateTypeChanged(rule, type) {
|
|
5718
|
+
rule.template = {
|
|
5719
|
+
...(rule.template || {}),
|
|
5720
|
+
type: type || 'text',
|
|
5721
|
+
};
|
|
5722
|
+
this.onRulesChanged();
|
|
5723
|
+
}
|
|
5724
|
+
defaultItemStyleRule(index) {
|
|
5725
|
+
const suffix = index ? `-${index + 1}` : '';
|
|
5726
|
+
return {
|
|
5727
|
+
id: `item-style${suffix}`,
|
|
5728
|
+
condition: { '==': [{ var: 'item.status' }, 'warning'] },
|
|
5729
|
+
class: 'pdx-list-item--warning',
|
|
5730
|
+
};
|
|
5731
|
+
}
|
|
5732
|
+
defaultSlotOverrideRule(index) {
|
|
5733
|
+
const suffix = index ? `-${index + 1}` : '';
|
|
5734
|
+
return {
|
|
5735
|
+
id: `slot-override${suffix}`,
|
|
5736
|
+
slot: 'trailing',
|
|
5737
|
+
condition: { '==': [{ var: 'item.status' }, 'warning'] },
|
|
5738
|
+
template: {
|
|
5739
|
+
type: 'chip',
|
|
5740
|
+
expr: '${item.status}',
|
|
5741
|
+
},
|
|
5742
|
+
};
|
|
5743
|
+
}
|
|
5535
5744
|
onSkeletonChanged(value) {
|
|
5536
5745
|
const n = Number(value);
|
|
5537
5746
|
const fixed = !Number.isFinite(n) || n < 0 ? 0 : Math.floor(n);
|
|
@@ -6707,7 +6916,7 @@ class PraxisListConfigEditor {
|
|
|
6707
6916
|
},
|
|
6708
6917
|
providePraxisI18nConfig(SURFACE_OPEN_I18N_CONFIG),
|
|
6709
6918
|
providePraxisListI18n(),
|
|
6710
|
-
], ngImport: i0, template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <ng-template matTabContent>\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 @if (!queryError) {\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 >\n <mat-icon>help_outline</mat-icon>\n </button>\n }\n @if (queryError) {\n <mat-error>{{ queryError }}</mat-error>\n }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('JSON')\">\n <ng-template matTabContent>\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Actions')\">\n <ng-template matTabContent>\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 @for (ga of globalActionCatalog; track ga) {\n <mat-option [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || \"bolt\" }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n }\n </mat-select>\n @if (!globalActionCatalog.length) {\n <mat-hint\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n }\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a structured global action.') }}\n </div>\n </div>\n @for (a of working.actions || []; track a; let i = $index) {\n <div\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('Global action') }}</mat-label>\n <mat-select\n [ngModel]=\"getGlobalActionId(a)\"\n (ngModelChange)=\"onActionGlobalActionIdChange(a, $event)\"\n >\n <mat-option value=\"\">{{ tx('None') }}</mat-option>\n @for (action of globalActionCatalog; track action) {\n <mat-option [value]=\"action.id\">\n {{ action.label || action.id }}\n </mat-option>\n }\n </mat-select>\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 @if (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 }\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 @for (c of paletteOptions; track c) {\n <mat-option [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 }\n </mat-select>\n </mat-form-field>\n @if (isCustomColor(a.color)) {\n <div\n class=\"g gap-8\"\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 } @else {\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n }\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ actionVisibilityLabel() }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"actionShowIfModel(i)\"\n (ngModelChange)=\"onActionShowIfChanged(i, a, $event)\"\n ></textarea>\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 @if ((a.kind || 'icon') === 'icon') {\n <button\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 }\n @if (a.kind === 'button') {\n @if (a.buttonVariant === 'stroked') {\n <button\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 }\n @if (a.buttonVariant === 'raised') {\n <button\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 }\n @if (!a.buttonVariant || a.buttonVariant === 'flat') {\n <button\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 }\n }\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\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Placement') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.placement\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"trailing\">{{ tx('Trailing') }}</mat-option>\n <mat-option value=\"actions\">{{ tx('Actions column') }}</mat-option>\n </mat-select>\n </mat-form-field>\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 @if (!a.confirmation?.title && !a.confirmation?.message) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n }\n </div>\n </div>\n </mat-expansion-panel>\n @if (a.globalAction?.actionId) {\n @if (isSurfaceOpenCommand(a)) {\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n } @else {\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]=\"getGlobalActionPayloadText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadTextChange(a, $event)\"\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]=\"globalActionPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n @if (isGlobalActionPayloadInvalid(a)) {\n <mat-error\n >{{ tx('Invalid JSON') }}</mat-error\n >\n }\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload expression') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getGlobalActionPayloadExprText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadExprTextChange(a, $event)\"\n placeholder=\"item.warningMessage\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Advanced escape hatch. When set, payload is resolved from this expression and no default item payload is injected.')\"\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 mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalActionPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalActionPayloadExampleHint(a)\n }}</span>\n </div>\n }\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n }\n </div>\n </div>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Layout')\">\n <ng-template matTabContent>\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 @if (working.layout.variant === 'list') {\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 } @else {\n @if (working.layout.variant === 'tiles') {\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 } @else {\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 }\n }\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 @if (working.layout.variant !== 'tiles') {\n <mat-form-field\n appearance=\"outline\"\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 }\n @if (fields.length > 0) {\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\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 }\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 <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Row layout') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Stable columns for list rows')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"grid gap-3\">\n <div class=\"g g-1-auto ai-center gap-12\">\n <mat-slide-toggle\n [ngModel]=\"rowLayoutEnabled()\"\n (ngModelChange)=\"onRowLayoutEnabledChange($event)\"\n >{{ tx('Enable row layout') }}</mat-slide-toggle\n >\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyCorporateRowLayoutPreset()\"\n >\n {{ tx('Corporate row preset') }}\n </button>\n </div>\n @if (working.layout.rowLayout) {\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Layout type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.rowLayout.type\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n <mat-option value=\"grid\">{{ tx('Grid') }}</mat-option>\n <mat-option value=\"flex\">{{ tx('Flex') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Vertical alignment') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.rowLayout.itemAlignY\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (align of rowLayoutAlignOptions; track align) {\n <mat-option [value]=\"align\">{{ align }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gap') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.rowLayout.gap\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"16px\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Columns') }}\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addRowLayoutColumn()\"\n >\n {{ tx('Add column') }}\n </button>\n </div>\n @for (\n column of working.layout.rowLayout.columns || [];\n track column;\n let i = $index\n ) {\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Slot') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.slot\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (slot of rowLayoutSlotOptions; track slot) {\n <mat-option [value]=\"slot\">{{ slot }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.width\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"1fr\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Min width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.minWidth\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"120px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Max width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.maxWidth\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"320px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Align') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.align\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (align of rowLayoutColumnAlignOptions; track align) {\n <mat-option [value]=\"align\">{{ align }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Justify') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.justify\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (justify of rowLayoutJustifyOptions; track justify) {\n <mat-option [value]=\"justify\">{{ justify }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <div class=\"g row-flow gap-4 ai-center\">\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move column left')\"\n (click)=\"moveRowLayoutColumn(i, -1)\"\n >\n <mat-icon>arrow_back</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === (working.layout.rowLayout.columns?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move column right')\"\n (click)=\"moveRowLayoutColumn(i, 1)\"\n >\n <mat-icon>arrow_forward</mat-icon>\n </button>\n <button\n mat-icon-button\n color=\"warn\"\n type=\"button\"\n [attr.aria-label]=\"tx('Remove column')\"\n (click)=\"removeRowLayoutColumn(i)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n }\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Advanced row CSS') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.rowLayout.class\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <textarea\n matInput\n rows=\"2\"\n [(ngModel)]=\"working.layout.rowLayout.style\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n ></textarea>\n </mat-form-field>\n </div>\n </mat-expansion-panel>\n }\n </div>\n </mat-expansion-panel>\n <mat-divider class=\"my-8\"></mat-divider>\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Expansion') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Inline details for list rows')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"grid gap-3\">\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [ngModel]=\"expansionEnabled()\"\n (ngModelChange)=\"onExpansionEnabledChange($event)\"\n >{{ tx('Enable expansion') }}</mat-slide-toggle\n >\n @if (working.interaction?.expandable) {\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Trigger') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandTriggerModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandTrigger', $event)\"\n >\n @for (trigger of expandTriggerOptions; track trigger) {\n <mat-option [value]=\"trigger\">{{ trigger }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandModeModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandMode', $event)\"\n >\n @for (mode of expandModeOptions; track mode) {\n <mat-option [value]=\"mode\">{{ mode }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Control placement') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandPlacementModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandPlacement', $event)\"\n >\n @for (placement of expandPlacementOptions; track placement) {\n <mat-option [value]=\"placement\">{{ placement }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n @if (working.interaction?.expandable && working.expansion) {\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shell') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpansionShellModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('shell', $event)\"\n >\n @for (shell of expansionShellOptions; track shell) {\n <mat-option [value]=\"shell\">{{ shell }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Columns') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpansionColumnsModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('columns', $event)\"\n >\n @for (columns of expansionColumnOptions; track columns) {\n <mat-option [value]=\"columns\">{{ columns }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gap') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExpansionGapModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('gap', $event)\"\n placeholder=\"12px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Padding') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExpansionPaddingModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('padding', $event)\"\n placeholder=\"16px\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Sections') }}\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addExpansionSection()\"\n >\n {{ tx('Add section') }}\n </button>\n </div>\n @for (\n section of working.expansion.sections || [];\n track section.id;\n let i = $index\n ) {\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Id') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.id\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.title\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"section.type\"\n (ngModelChange)=\"onExpansionChanged()\"\n >\n @for (type of expansionSectionTypeOptions; track type) {\n <mat-option [value]=\"type\">{{ type }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Items expression') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.itemsExpr\"\n (ngModelChange)=\"onExpansionChanged()\"\n placeholder=\"${item.details}\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Empty label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.emptyLabel\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Show when (Json Logic)') }}</mat-label>\n <textarea\n matInput\n rows=\"2\"\n [ngModel]=\"getExpansionSectionShowIfText(section)\"\n (ngModelChange)=\"onExpansionSectionShowIfChanged(section, $event)\"\n ></textarea>\n </mat-form-field>\n <div class=\"g row-flow gap-4 ai-center\">\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move section up')\"\n (click)=\"moveExpansionSection(i, -1)\"\n >\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === (working.expansion.sections?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move section down')\"\n (click)=\"moveExpansionSection(i, 1)\"\n >\n <mat-icon>arrow_downward</mat-icon>\n </button>\n <button\n mat-icon-button\n color=\"warn\"\n type=\"button\"\n [attr.aria-label]=\"tx('Remove section')\"\n (click)=\"removeExpansionSection(i)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </mat-expansion-panel>\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 <mat-slide-toggle\n [ngModel]=\"exportEnabled()\"\n (ngModelChange)=\"onExportEnabledChange($event)\"\n >{{ tx('Enable export') }}</mat-slide-toggle\n >\n </div>\n @if (working.export?.enabled) {\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel mt-12\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Export') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Formats, scope and file options')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Formats') }}</mat-label>\n <mat-select\n multiple\n [ngModel]=\"getExportFormatsModel()\"\n (ngModelChange)=\"onExportFormatsChange($event)\"\n >\n @for (format of exportFormatOptions; track format) {\n <mat-option [value]=\"format\">{{\n format.toUpperCase()\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Scope') }}</mat-label>\n <mat-select\n [ngModel]=\"getExportScopeModel()\"\n (ngModelChange)=\"onExportScopeChange($event)\"\n >\n @for (scope of exportScopeOptions; track scope) {\n <mat-option [value]=\"scope\">{{ scope }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Max rows') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [ngModel]=\"getExportMaxRowsModel()\"\n (ngModelChange)=\"onExportMaxRowsChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default file name') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExportDefaultFileNameModel()\"\n (ngModelChange)=\"onExportDefaultFileNameChange($event)\"\n placeholder=\"praxis-list-export\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Included fields') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExportIncludeFieldsText()\"\n (ngModelChange)=\"onExportIncludeFieldsTextChange($event)\"\n [placeholder]=\"tx('Empty means all fields')\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [ngModel]=\"getExportIncludeHeadersModel()\"\n (ngModelChange)=\"onExportIncludeHeadersChange($event)\"\n >{{ tx('Include headers') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [ngModel]=\"getExportApplyFormattingModel()\"\n (ngModelChange)=\"onExportApplyFormattingChange($event)\"\n >{{ tx('Apply formatting') }}</mat-slide-toggle\n >\n </div>\n </mat-expansion-panel>\n }\n @if (working.ui?.showSearch) {\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 }\n @if (working.ui?.showSort) {\n <div class=\"mt-12\">\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 @for (r of uiSortRows; track r; let i = $index) {\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @if (isUiSortRowDuplicate(i)) {\n <div class=\"error\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n }\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Content')\">\n <ng-template matTabContent>\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of primaryTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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 @if (\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n ) {\n <mat-form-field\n appearance=\"outline\"\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 }\n @if (\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n ) {\n <mat-form-field\n appearance=\"outline\"\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 }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of secondaryTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (mappingMetaFields.length) {\n <div\n class=\"g g-1-1 ai-center gap-12\"\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 }\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n @if (!mappingMetaFields.length) {\n <div class=\"g g-1-1 gap-12\">\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of metaTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of trailingTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n @if (!mappingTrailing.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 @for (mt of leadingTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n @if (\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n ) {\n <mat-form-field\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\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 @if (mappingLeading.type === 'icon') {\n <div\n class=\"g g-1-auto gap-12 ai-center\"\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 }\n @if (mappingLeading.type === 'icon') {\n <div>\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\n <!-- Image Specific -->\n @if (mappingLeading.type === 'image') {\n <div\n class=\"g g-1-1 gap-12\"\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 @if (isImageUrlRequiredInvalid(mappingLeading.imageUrl)) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\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\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @for (f of features; track f; let i = $index) {\n <div\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 }\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 @for (mt of sectionHeaderTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n @if (!mappingSectionHeader.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 @for (mt of emptyStateTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n @if (!mappingEmptyState.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <ng-template matTabContent>\n @if (working?.a11y && working?.events) {\n <div\n class=\"editor-content grid gap-3\"\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>exportAction</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.exportAction\"\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 }\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <ng-template matTabContent>\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\n <ng-template matTabContent>\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 @if (working.skin.type === 'glass') {\n <mat-form-field\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 }\n @if (working.skin.type === 'gradient-tile') {\n <div 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\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 @if (working.skin.type === 'custom') {\n <div\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 }\n </div>\n </ng-template>\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: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.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: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i3$3.MatTabContent, selector: "[matTabContent]" }, { 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.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i2.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"] }] });
|
|
6919
|
+
], ngImport: i0, template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <ng-template matTabContent>\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 @if (!queryError) {\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 >\n <mat-icon>help_outline</mat-icon>\n </button>\n }\n @if (queryError) {\n <mat-error>{{ queryError }}</mat-error>\n }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('JSON')\">\n <ng-template matTabContent>\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Actions')\">\n <ng-template matTabContent>\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 @for (ga of globalActionCatalog; track ga) {\n <mat-option [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || \"bolt\" }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n }\n </mat-select>\n @if (!globalActionCatalog.length) {\n <mat-hint\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n }\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a structured global action.') }}\n </div>\n </div>\n @for (a of working.actions || []; track a; let i = $index) {\n <div\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('Global action') }}</mat-label>\n <mat-select\n [ngModel]=\"getGlobalActionId(a)\"\n (ngModelChange)=\"onActionGlobalActionIdChange(a, $event)\"\n >\n <mat-option value=\"\">{{ tx('None') }}</mat-option>\n @for (action of globalActionCatalog; track action) {\n <mat-option [value]=\"action.id\">\n {{ action.label || action.id }}\n </mat-option>\n }\n </mat-select>\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 @if (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 }\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 @for (c of paletteOptions; track c) {\n <mat-option [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 }\n </mat-select>\n </mat-form-field>\n @if (isCustomColor(a.color)) {\n <div\n class=\"g gap-8\"\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 } @else {\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n }\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ actionVisibilityLabel() }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"actionShowIfModel(i)\"\n (ngModelChange)=\"onActionShowIfChanged(i, a, $event)\"\n ></textarea>\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 @if ((a.kind || 'icon') === 'icon') {\n <button\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 }\n @if (a.kind === 'button') {\n @if (a.buttonVariant === 'stroked') {\n <button\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 }\n @if (a.buttonVariant === 'raised') {\n <button\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 }\n @if (!a.buttonVariant || a.buttonVariant === 'flat') {\n <button\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 }\n }\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\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Placement') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.placement\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"trailing\">{{ tx('Trailing') }}</mat-option>\n <mat-option value=\"actions\">{{ tx('Actions column') }}</mat-option>\n </mat-select>\n </mat-form-field>\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 @if (!a.confirmation?.title && !a.confirmation?.message) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n }\n </div>\n </div>\n </mat-expansion-panel>\n @if (a.globalAction?.actionId) {\n @if (isSurfaceOpenCommand(a)) {\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n } @else {\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]=\"getGlobalActionPayloadText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadTextChange(a, $event)\"\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]=\"globalActionPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n @if (isGlobalActionPayloadInvalid(a)) {\n <mat-error\n >{{ tx('Invalid JSON') }}</mat-error\n >\n }\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload expression') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getGlobalActionPayloadExprText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadExprTextChange(a, $event)\"\n placeholder=\"item.warningMessage\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Advanced escape hatch. When set, payload is resolved from this expression and no default item payload is injected.')\"\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 mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalActionPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalActionPayloadExampleHint(a)\n }}</span>\n </div>\n }\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n }\n </div>\n </div>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Layout')\">\n <ng-template matTabContent>\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 @if (working.layout.variant === 'list') {\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 } @else {\n @if (working.layout.variant === 'tiles') {\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 } @else {\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 }\n }\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 @if (working.layout.variant !== 'tiles') {\n <mat-form-field\n appearance=\"outline\"\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 }\n @if (fields.length > 0) {\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\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 }\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 <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Row layout') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Stable columns for list rows')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"grid gap-3\">\n <div class=\"g g-1-auto ai-center gap-12\">\n <mat-slide-toggle\n [ngModel]=\"rowLayoutEnabled()\"\n (ngModelChange)=\"onRowLayoutEnabledChange($event)\"\n >{{ tx('Enable row layout') }}</mat-slide-toggle\n >\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyCorporateRowLayoutPreset()\"\n >\n {{ tx('Corporate row preset') }}\n </button>\n </div>\n @if (working.layout.rowLayout) {\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Layout type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.rowLayout.type\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n <mat-option value=\"grid\">{{ tx('Grid') }}</mat-option>\n <mat-option value=\"flex\">{{ tx('Flex') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Vertical alignment') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.rowLayout.itemAlignY\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (align of rowLayoutAlignOptions; track align) {\n <mat-option [value]=\"align\">{{ align }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gap') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.rowLayout.gap\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"16px\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Columns') }}\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addRowLayoutColumn()\"\n >\n {{ tx('Add column') }}\n </button>\n </div>\n @for (\n column of working.layout.rowLayout.columns || [];\n track column;\n let i = $index\n ) {\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Slot') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.slot\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (slot of rowLayoutSlotOptions; track slot) {\n <mat-option [value]=\"slot\">{{ slot }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.width\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"1fr\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Min width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.minWidth\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"120px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Max width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.maxWidth\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"320px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Align') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.align\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (align of rowLayoutColumnAlignOptions; track align) {\n <mat-option [value]=\"align\">{{ align }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Justify') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.justify\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (justify of rowLayoutJustifyOptions; track justify) {\n <mat-option [value]=\"justify\">{{ justify }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <div class=\"g row-flow gap-4 ai-center\">\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move column left')\"\n (click)=\"moveRowLayoutColumn(i, -1)\"\n >\n <mat-icon>arrow_back</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === (working.layout.rowLayout.columns?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move column right')\"\n (click)=\"moveRowLayoutColumn(i, 1)\"\n >\n <mat-icon>arrow_forward</mat-icon>\n </button>\n <button\n mat-icon-button\n color=\"warn\"\n type=\"button\"\n [attr.aria-label]=\"tx('Remove column')\"\n (click)=\"removeRowLayoutColumn(i)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n @if (rowLayoutColumnWarning(column); as warning) {\n <div class=\"muted col-span-2\">\n {{ warning }}\n </div>\n }\n </div>\n }\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Advanced row CSS') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.rowLayout.class\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <textarea\n matInput\n rows=\"2\"\n [(ngModel)]=\"working.layout.rowLayout.style\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n ></textarea>\n </mat-form-field>\n </div>\n </mat-expansion-panel>\n }\n </div>\n </mat-expansion-panel>\n <mat-divider class=\"my-8\"></mat-divider>\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Expansion') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Inline details for list rows')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"grid gap-3\">\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [ngModel]=\"expansionEnabled()\"\n (ngModelChange)=\"onExpansionEnabledChange($event)\"\n >{{ tx('Enable expansion') }}</mat-slide-toggle\n >\n @if (working.interaction?.expandable) {\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Trigger') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandTriggerModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandTrigger', $event)\"\n >\n @for (trigger of expandTriggerOptions; track trigger) {\n <mat-option [value]=\"trigger\">{{ trigger }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandModeModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandMode', $event)\"\n >\n @for (mode of expandModeOptions; track mode) {\n <mat-option [value]=\"mode\">{{ mode }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Control placement') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandPlacementModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandPlacement', $event)\"\n >\n @for (placement of expandPlacementOptions; track placement) {\n <mat-option [value]=\"placement\">{{ placement }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n @if (working.interaction?.expandable && working.expansion) {\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shell') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpansionShellModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('shell', $event)\"\n >\n @for (shell of expansionShellOptions; track shell) {\n <mat-option [value]=\"shell\">{{ shell }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Columns') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpansionColumnsModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('columns', $event)\"\n >\n @for (columns of expansionColumnOptions; track columns) {\n <mat-option [value]=\"columns\">{{ columns }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gap') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExpansionGapModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('gap', $event)\"\n placeholder=\"12px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Padding') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExpansionPaddingModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('padding', $event)\"\n placeholder=\"16px\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Sections') }}\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addExpansionSection()\"\n >\n {{ tx('Add section') }}\n </button>\n </div>\n @for (\n section of working.expansion.sections || [];\n track section.id;\n let i = $index\n ) {\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Id') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.id\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.title\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"section.type\"\n (ngModelChange)=\"onExpansionChanged()\"\n >\n @for (type of expansionSectionTypeOptions; track type) {\n <mat-option [value]=\"type\">{{ type }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Items expression') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.itemsExpr\"\n (ngModelChange)=\"onExpansionChanged()\"\n placeholder=\"${item.details}\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Empty label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.emptyLabel\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Show when (Json Logic)') }}</mat-label>\n <textarea\n matInput\n rows=\"2\"\n [ngModel]=\"getExpansionSectionShowIfText(section)\"\n (ngModelChange)=\"onExpansionSectionShowIfChanged(section, $event)\"\n ></textarea>\n </mat-form-field>\n <div class=\"g row-flow gap-4 ai-center\">\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move section up')\"\n (click)=\"moveExpansionSection(i, -1)\"\n >\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === (working.expansion.sections?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move section down')\"\n (click)=\"moveExpansionSection(i, 1)\"\n >\n <mat-icon>arrow_downward</mat-icon>\n </button>\n <button\n mat-icon-button\n color=\"warn\"\n type=\"button\"\n [attr.aria-label]=\"tx('Remove section')\"\n (click)=\"removeExpansionSection(i)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </mat-expansion-panel>\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 <mat-slide-toggle\n [ngModel]=\"exportEnabled()\"\n (ngModelChange)=\"onExportEnabledChange($event)\"\n >{{ tx('Enable export') }}</mat-slide-toggle\n >\n </div>\n @if (working.export?.enabled) {\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel mt-12\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Export') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Formats, scope and file options')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Formats') }}</mat-label>\n <mat-select\n multiple\n [ngModel]=\"getExportFormatsModel()\"\n (ngModelChange)=\"onExportFormatsChange($event)\"\n >\n @for (format of exportFormatOptions; track format) {\n <mat-option [value]=\"format\">{{\n format.toUpperCase()\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Scope') }}</mat-label>\n <mat-select\n [ngModel]=\"getExportScopeModel()\"\n (ngModelChange)=\"onExportScopeChange($event)\"\n >\n @for (scope of exportScopeOptions; track scope) {\n <mat-option [value]=\"scope\">{{ scope }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Max rows') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [ngModel]=\"getExportMaxRowsModel()\"\n (ngModelChange)=\"onExportMaxRowsChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default file name') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExportDefaultFileNameModel()\"\n (ngModelChange)=\"onExportDefaultFileNameChange($event)\"\n placeholder=\"praxis-list-export\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Included fields') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExportIncludeFieldsText()\"\n (ngModelChange)=\"onExportIncludeFieldsTextChange($event)\"\n [placeholder]=\"tx('Empty means all fields')\"\n />\n @if (getExportIncludeFieldsWarning(); as warning) {\n <mat-hint>{{ warning }}</mat-hint>\n }\n </mat-form-field>\n <mat-slide-toggle\n [ngModel]=\"getExportIncludeHeadersModel()\"\n (ngModelChange)=\"onExportIncludeHeadersChange($event)\"\n >{{ tx('Include headers') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [ngModel]=\"getExportApplyFormattingModel()\"\n (ngModelChange)=\"onExportApplyFormattingChange($event)\"\n >{{ tx('Apply formatting') }}</mat-slide-toggle\n >\n </div>\n </mat-expansion-panel>\n }\n @if (working.ui?.showSearch) {\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 }\n @if (working.ui?.showSort) {\n <div class=\"mt-12\">\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 @for (r of uiSortRows; track r; let i = $index) {\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @if (isUiSortRowDuplicate(i)) {\n <div class=\"error\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n }\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Content')\">\n <ng-template matTabContent>\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of primaryTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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 @if (\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n ) {\n <mat-form-field\n appearance=\"outline\"\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 }\n @if (\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n ) {\n <mat-form-field\n appearance=\"outline\"\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 }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of secondaryTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (mappingMetaFields.length) {\n <div\n class=\"g g-1-1 ai-center gap-12\"\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 }\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n @if (!mappingMetaFields.length) {\n <div class=\"g g-1-1 gap-12\">\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of metaTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of trailingTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n @if (!mappingTrailing.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 @for (mt of leadingTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n @if (\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n ) {\n <mat-form-field\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\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 @if (mappingLeading.type === 'icon') {\n <div\n class=\"g g-1-auto gap-12 ai-center\"\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 }\n @if (mappingLeading.type === 'icon') {\n <div>\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\n <!-- Image Specific -->\n @if (mappingLeading.type === 'image') {\n <div\n class=\"g g-1-1 gap-12\"\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 @if (isImageUrlRequiredInvalid(mappingLeading.imageUrl)) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\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\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @for (f of features; track f; let i = $index) {\n <div\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 }\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 @for (mt of sectionHeaderTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n @if (!mappingSectionHeader.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 @for (mt of emptyStateTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n @if (!mappingEmptyState.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Rules')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div>\n <div class=\"subtitle\">{{ tx('Item style rules') }}</div>\n <div class=\"muted\">\n {{ tx('Apply class, style, border or background when Json Logic matches.') }}\n </div>\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addItemStyleRule()\"\n >\n {{ tx('Add item rule') }}\n </button>\n </div>\n @for (\n rule of working.rules?.itemStyles || [];\n track rule.id;\n let i = $index\n ) {\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\" [expanded]=\"i === 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ rule.id || tx('Item rule') }}</mat-panel-title>\n <mat-panel-description>{{ rule.class || rule.background || rule.border }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Id') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.id\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Condition (Json Logic)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"getRuleConditionText(rule)\"\n (ngModelChange)=\"onRuleConditionChanged(rule, $event)\"\n ></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.class\"\n (ngModelChange)=\"onRulesChanged()\"\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)]=\"rule.style\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.border\"\n (ngModelChange)=\"onRulesChanged()\"\n placeholder=\"1px solid var(--md-sys-color-outline)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Background') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.background\"\n (ngModelChange)=\"onRulesChanged()\"\n placeholder=\"var(--md-sys-color-surface-container)\"\n />\n </mat-form-field>\n <div class=\"flex-end\">\n <button\n mat-button\n color=\"warn\"\n type=\"button\"\n (click)=\"removeItemStyleRule(i)\"\n >\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"col-span-2\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div>\n <div class=\"subtitle\">{{ tx('Canonical effect') }}</div>\n <div class=\"muted\">\n {{ tx('Materialize the first effects[] entry used by the runtime.') }}\n </div>\n </div>\n @if (getItemStyleEffect(rule)) {\n <button\n mat-button\n type=\"button\"\n (click)=\"clearItemStyleEffects(rule)\"\n >\n {{ tx('Use direct fields') }}\n </button>\n } @else {\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"ensureItemStyleEffect(rule)\"\n >\n {{ tx('Use effects[]') }}\n </button>\n }\n </div>\n </div>\n @if (getItemStyleEffect(rule); as effect) {\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.class\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.style\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.border\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect background') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.background\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n }\n </div>\n </mat-expansion-panel>\n }\n\n <mat-divider class=\"my-8\"></mat-divider>\n\n <div class=\"g g-1-auto ai-center gap-8\">\n <div>\n <div class=\"subtitle\">{{ tx('Slot override rules') }}</div>\n <div class=\"muted\">\n {{ tx('Replace, decorate or hide a slot when Json Logic matches.') }}\n </div>\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addSlotOverrideRule()\"\n >\n {{ tx('Add slot rule') }}\n </button>\n </div>\n @for (\n rule of working.rules?.slotOverrides || [];\n track rule.id;\n let i = $index\n ) {\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\" [expanded]=\"i === 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ rule.id || tx('Slot rule') }}</mat-panel-title>\n <mat-panel-description>{{ rule.slot }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Id') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.id\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Slot') }}</mat-label>\n <mat-select\n [(ngModel)]=\"rule.slot\"\n (ngModelChange)=\"onRulesChanged()\"\n >\n @for (slot of ruleSlotOptions; track slot) {\n <mat-option [value]=\"slot\">{{ slot }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Condition (Json Logic)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"getRuleConditionText(rule)\"\n (ngModelChange)=\"onRuleConditionChanged(rule, $event)\"\n ></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Template type') }}</mat-label>\n <mat-select\n [ngModel]=\"getSlotOverrideTemplateType(rule)\"\n (ngModelChange)=\"onSlotOverrideTemplateTypeChanged(rule, $event)\"\n >\n @for (type of ruleTemplateTypeOptions; track type) {\n <mat-option [value]=\"type\">{{ tx(type) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Template expression') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getSlotOverrideTemplateExpr(rule)\"\n (ngModelChange)=\"onSlotOverrideTemplateExprChanged(rule, $event)\"\n placeholder=\"${item.status}\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.class\"\n (ngModelChange)=\"onRulesChanged()\"\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)]=\"rule.style\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"rule.hide\"\n (ngModelChange)=\"onRulesChanged()\"\n >{{ tx('Hide slot') }}</mat-slide-toggle>\n <div class=\"flex-end\">\n <button\n mat-button\n color=\"warn\"\n type=\"button\"\n (click)=\"removeSlotOverrideRule(i)\"\n >\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"col-span-2\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div>\n <div class=\"subtitle\">{{ tx('Canonical effect') }}</div>\n <div class=\"muted\">\n {{ tx('Materialize the first effects[] entry used by the runtime.') }}\n </div>\n </div>\n @if (getSlotOverrideEffect(rule)) {\n <button\n mat-button\n type=\"button\"\n (click)=\"clearSlotOverrideEffects(rule)\"\n >\n {{ tx('Use direct fields') }}\n </button>\n } @else {\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"ensureSlotOverrideEffect(rule)\"\n >\n {{ tx('Use effects[]') }}\n </button>\n }\n </div>\n </div>\n @if (getSlotOverrideEffect(rule); as effect) {\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect template type') }}</mat-label>\n <mat-select\n [ngModel]=\"getSlotOverrideTemplateType(effect)\"\n (ngModelChange)=\"onSlotOverrideTemplateTypeChanged(effect, $event)\"\n >\n @for (type of ruleTemplateTypeOptions; track type) {\n <mat-option [value]=\"type\">{{ tx(type) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect template expression') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getSlotOverrideTemplateExpr(effect)\"\n (ngModelChange)=\"onSlotOverrideTemplateExprChanged(effect, $event)\"\n placeholder=\"${item.status}\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.class\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.style\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"effect.hide\"\n (ngModelChange)=\"onRulesChanged()\"\n >{{ tx('Effect hides slot') }}</mat-slide-toggle>\n }\n </div>\n </mat-expansion-panel>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <ng-template matTabContent>\n @if (working?.a11y && working?.events) {\n <div\n class=\"editor-content grid gap-3\"\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>exportAction</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.exportAction\"\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 }\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <ng-template matTabContent>\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\n <ng-template matTabContent>\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 @if (working.skin.type === 'glass') {\n <mat-form-field\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 }\n @if (working.skin.type === 'gradient-tile') {\n <div 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\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 @if (working.skin.type === 'custom') {\n <div\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 }\n </div>\n </ng-template>\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: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.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: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i3$3.MatTabContent, selector: "[matTabContent]" }, { 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.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i2.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"] }] });
|
|
6711
6920
|
}
|
|
6712
6921
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisListConfigEditor, decorators: [{
|
|
6713
6922
|
type: Component,
|
|
@@ -6751,7 +6960,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6751
6960
|
PraxisListJsonConfigEditorComponent,
|
|
6752
6961
|
PdxColorPickerComponent,
|
|
6753
6962
|
SurfaceOpenActionEditorComponent,
|
|
6754
|
-
], template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <ng-template matTabContent>\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 @if (!queryError) {\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 >\n <mat-icon>help_outline</mat-icon>\n </button>\n }\n @if (queryError) {\n <mat-error>{{ queryError }}</mat-error>\n }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('JSON')\">\n <ng-template matTabContent>\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Actions')\">\n <ng-template matTabContent>\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 @for (ga of globalActionCatalog; track ga) {\n <mat-option [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || \"bolt\" }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n }\n </mat-select>\n @if (!globalActionCatalog.length) {\n <mat-hint\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n }\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a structured global action.') }}\n </div>\n </div>\n @for (a of working.actions || []; track a; let i = $index) {\n <div\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('Global action') }}</mat-label>\n <mat-select\n [ngModel]=\"getGlobalActionId(a)\"\n (ngModelChange)=\"onActionGlobalActionIdChange(a, $event)\"\n >\n <mat-option value=\"\">{{ tx('None') }}</mat-option>\n @for (action of globalActionCatalog; track action) {\n <mat-option [value]=\"action.id\">\n {{ action.label || action.id }}\n </mat-option>\n }\n </mat-select>\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 @if (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 }\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 @for (c of paletteOptions; track c) {\n <mat-option [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 }\n </mat-select>\n </mat-form-field>\n @if (isCustomColor(a.color)) {\n <div\n class=\"g gap-8\"\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 } @else {\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n }\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ actionVisibilityLabel() }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"actionShowIfModel(i)\"\n (ngModelChange)=\"onActionShowIfChanged(i, a, $event)\"\n ></textarea>\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 @if ((a.kind || 'icon') === 'icon') {\n <button\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 }\n @if (a.kind === 'button') {\n @if (a.buttonVariant === 'stroked') {\n <button\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 }\n @if (a.buttonVariant === 'raised') {\n <button\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 }\n @if (!a.buttonVariant || a.buttonVariant === 'flat') {\n <button\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 }\n }\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\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Placement') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.placement\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"trailing\">{{ tx('Trailing') }}</mat-option>\n <mat-option value=\"actions\">{{ tx('Actions column') }}</mat-option>\n </mat-select>\n </mat-form-field>\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 @if (!a.confirmation?.title && !a.confirmation?.message) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n }\n </div>\n </div>\n </mat-expansion-panel>\n @if (a.globalAction?.actionId) {\n @if (isSurfaceOpenCommand(a)) {\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n } @else {\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]=\"getGlobalActionPayloadText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadTextChange(a, $event)\"\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]=\"globalActionPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n @if (isGlobalActionPayloadInvalid(a)) {\n <mat-error\n >{{ tx('Invalid JSON') }}</mat-error\n >\n }\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload expression') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getGlobalActionPayloadExprText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadExprTextChange(a, $event)\"\n placeholder=\"item.warningMessage\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Advanced escape hatch. When set, payload is resolved from this expression and no default item payload is injected.')\"\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 mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalActionPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalActionPayloadExampleHint(a)\n }}</span>\n </div>\n }\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n }\n </div>\n </div>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Layout')\">\n <ng-template matTabContent>\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 @if (working.layout.variant === 'list') {\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 } @else {\n @if (working.layout.variant === 'tiles') {\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 } @else {\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 }\n }\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 @if (working.layout.variant !== 'tiles') {\n <mat-form-field\n appearance=\"outline\"\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 }\n @if (fields.length > 0) {\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\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 }\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 <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Row layout') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Stable columns for list rows')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"grid gap-3\">\n <div class=\"g g-1-auto ai-center gap-12\">\n <mat-slide-toggle\n [ngModel]=\"rowLayoutEnabled()\"\n (ngModelChange)=\"onRowLayoutEnabledChange($event)\"\n >{{ tx('Enable row layout') }}</mat-slide-toggle\n >\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyCorporateRowLayoutPreset()\"\n >\n {{ tx('Corporate row preset') }}\n </button>\n </div>\n @if (working.layout.rowLayout) {\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Layout type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.rowLayout.type\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n <mat-option value=\"grid\">{{ tx('Grid') }}</mat-option>\n <mat-option value=\"flex\">{{ tx('Flex') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Vertical alignment') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.rowLayout.itemAlignY\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (align of rowLayoutAlignOptions; track align) {\n <mat-option [value]=\"align\">{{ align }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gap') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.rowLayout.gap\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"16px\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Columns') }}\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addRowLayoutColumn()\"\n >\n {{ tx('Add column') }}\n </button>\n </div>\n @for (\n column of working.layout.rowLayout.columns || [];\n track column;\n let i = $index\n ) {\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Slot') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.slot\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (slot of rowLayoutSlotOptions; track slot) {\n <mat-option [value]=\"slot\">{{ slot }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.width\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"1fr\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Min width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.minWidth\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"120px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Max width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.maxWidth\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"320px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Align') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.align\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (align of rowLayoutColumnAlignOptions; track align) {\n <mat-option [value]=\"align\">{{ align }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Justify') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.justify\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (justify of rowLayoutJustifyOptions; track justify) {\n <mat-option [value]=\"justify\">{{ justify }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <div class=\"g row-flow gap-4 ai-center\">\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move column left')\"\n (click)=\"moveRowLayoutColumn(i, -1)\"\n >\n <mat-icon>arrow_back</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === (working.layout.rowLayout.columns?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move column right')\"\n (click)=\"moveRowLayoutColumn(i, 1)\"\n >\n <mat-icon>arrow_forward</mat-icon>\n </button>\n <button\n mat-icon-button\n color=\"warn\"\n type=\"button\"\n [attr.aria-label]=\"tx('Remove column')\"\n (click)=\"removeRowLayoutColumn(i)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n }\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Advanced row CSS') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.rowLayout.class\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <textarea\n matInput\n rows=\"2\"\n [(ngModel)]=\"working.layout.rowLayout.style\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n ></textarea>\n </mat-form-field>\n </div>\n </mat-expansion-panel>\n }\n </div>\n </mat-expansion-panel>\n <mat-divider class=\"my-8\"></mat-divider>\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Expansion') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Inline details for list rows')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"grid gap-3\">\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [ngModel]=\"expansionEnabled()\"\n (ngModelChange)=\"onExpansionEnabledChange($event)\"\n >{{ tx('Enable expansion') }}</mat-slide-toggle\n >\n @if (working.interaction?.expandable) {\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Trigger') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandTriggerModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandTrigger', $event)\"\n >\n @for (trigger of expandTriggerOptions; track trigger) {\n <mat-option [value]=\"trigger\">{{ trigger }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandModeModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandMode', $event)\"\n >\n @for (mode of expandModeOptions; track mode) {\n <mat-option [value]=\"mode\">{{ mode }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Control placement') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandPlacementModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandPlacement', $event)\"\n >\n @for (placement of expandPlacementOptions; track placement) {\n <mat-option [value]=\"placement\">{{ placement }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n @if (working.interaction?.expandable && working.expansion) {\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shell') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpansionShellModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('shell', $event)\"\n >\n @for (shell of expansionShellOptions; track shell) {\n <mat-option [value]=\"shell\">{{ shell }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Columns') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpansionColumnsModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('columns', $event)\"\n >\n @for (columns of expansionColumnOptions; track columns) {\n <mat-option [value]=\"columns\">{{ columns }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gap') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExpansionGapModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('gap', $event)\"\n placeholder=\"12px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Padding') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExpansionPaddingModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('padding', $event)\"\n placeholder=\"16px\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Sections') }}\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addExpansionSection()\"\n >\n {{ tx('Add section') }}\n </button>\n </div>\n @for (\n section of working.expansion.sections || [];\n track section.id;\n let i = $index\n ) {\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Id') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.id\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.title\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"section.type\"\n (ngModelChange)=\"onExpansionChanged()\"\n >\n @for (type of expansionSectionTypeOptions; track type) {\n <mat-option [value]=\"type\">{{ type }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Items expression') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.itemsExpr\"\n (ngModelChange)=\"onExpansionChanged()\"\n placeholder=\"${item.details}\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Empty label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.emptyLabel\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Show when (Json Logic)') }}</mat-label>\n <textarea\n matInput\n rows=\"2\"\n [ngModel]=\"getExpansionSectionShowIfText(section)\"\n (ngModelChange)=\"onExpansionSectionShowIfChanged(section, $event)\"\n ></textarea>\n </mat-form-field>\n <div class=\"g row-flow gap-4 ai-center\">\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move section up')\"\n (click)=\"moveExpansionSection(i, -1)\"\n >\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === (working.expansion.sections?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move section down')\"\n (click)=\"moveExpansionSection(i, 1)\"\n >\n <mat-icon>arrow_downward</mat-icon>\n </button>\n <button\n mat-icon-button\n color=\"warn\"\n type=\"button\"\n [attr.aria-label]=\"tx('Remove section')\"\n (click)=\"removeExpansionSection(i)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </mat-expansion-panel>\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 <mat-slide-toggle\n [ngModel]=\"exportEnabled()\"\n (ngModelChange)=\"onExportEnabledChange($event)\"\n >{{ tx('Enable export') }}</mat-slide-toggle\n >\n </div>\n @if (working.export?.enabled) {\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel mt-12\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Export') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Formats, scope and file options')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Formats') }}</mat-label>\n <mat-select\n multiple\n [ngModel]=\"getExportFormatsModel()\"\n (ngModelChange)=\"onExportFormatsChange($event)\"\n >\n @for (format of exportFormatOptions; track format) {\n <mat-option [value]=\"format\">{{\n format.toUpperCase()\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Scope') }}</mat-label>\n <mat-select\n [ngModel]=\"getExportScopeModel()\"\n (ngModelChange)=\"onExportScopeChange($event)\"\n >\n @for (scope of exportScopeOptions; track scope) {\n <mat-option [value]=\"scope\">{{ scope }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Max rows') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [ngModel]=\"getExportMaxRowsModel()\"\n (ngModelChange)=\"onExportMaxRowsChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default file name') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExportDefaultFileNameModel()\"\n (ngModelChange)=\"onExportDefaultFileNameChange($event)\"\n placeholder=\"praxis-list-export\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Included fields') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExportIncludeFieldsText()\"\n (ngModelChange)=\"onExportIncludeFieldsTextChange($event)\"\n [placeholder]=\"tx('Empty means all fields')\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [ngModel]=\"getExportIncludeHeadersModel()\"\n (ngModelChange)=\"onExportIncludeHeadersChange($event)\"\n >{{ tx('Include headers') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [ngModel]=\"getExportApplyFormattingModel()\"\n (ngModelChange)=\"onExportApplyFormattingChange($event)\"\n >{{ tx('Apply formatting') }}</mat-slide-toggle\n >\n </div>\n </mat-expansion-panel>\n }\n @if (working.ui?.showSearch) {\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 }\n @if (working.ui?.showSort) {\n <div class=\"mt-12\">\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 @for (r of uiSortRows; track r; let i = $index) {\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @if (isUiSortRowDuplicate(i)) {\n <div class=\"error\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n }\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Content')\">\n <ng-template matTabContent>\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of primaryTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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 @if (\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n ) {\n <mat-form-field\n appearance=\"outline\"\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 }\n @if (\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n ) {\n <mat-form-field\n appearance=\"outline\"\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 }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of secondaryTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (mappingMetaFields.length) {\n <div\n class=\"g g-1-1 ai-center gap-12\"\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 }\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n @if (!mappingMetaFields.length) {\n <div class=\"g g-1-1 gap-12\">\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of metaTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of trailingTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n @if (!mappingTrailing.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 @for (mt of leadingTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n @if (\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n ) {\n <mat-form-field\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\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 @if (mappingLeading.type === 'icon') {\n <div\n class=\"g g-1-auto gap-12 ai-center\"\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 }\n @if (mappingLeading.type === 'icon') {\n <div>\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\n <!-- Image Specific -->\n @if (mappingLeading.type === 'image') {\n <div\n class=\"g g-1-1 gap-12\"\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 @if (isImageUrlRequiredInvalid(mappingLeading.imageUrl)) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\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\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @for (f of features; track f; let i = $index) {\n <div\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 }\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 @for (mt of sectionHeaderTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n @if (!mappingSectionHeader.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 @for (mt of emptyStateTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n @if (!mappingEmptyState.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <ng-template matTabContent>\n @if (working?.a11y && working?.events) {\n <div\n class=\"editor-content grid gap-3\"\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>exportAction</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.exportAction\"\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 }\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <ng-template matTabContent>\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\n <ng-template matTabContent>\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 @if (working.skin.type === 'glass') {\n <mat-form-field\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 }\n @if (working.skin.type === 'gradient-tile') {\n <div 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\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 @if (working.skin.type === 'custom') {\n <div\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 }\n </div>\n </ng-template>\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"] }]
|
|
6963
|
+
], template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <ng-template matTabContent>\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 @if (!queryError) {\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 >\n <mat-icon>help_outline</mat-icon>\n </button>\n }\n @if (queryError) {\n <mat-error>{{ queryError }}</mat-error>\n }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('JSON')\">\n <ng-template matTabContent>\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Actions')\">\n <ng-template matTabContent>\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 @for (ga of globalActionCatalog; track ga) {\n <mat-option [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || \"bolt\" }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n }\n </mat-select>\n @if (!globalActionCatalog.length) {\n <mat-hint\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n }\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a structured global action.') }}\n </div>\n </div>\n @for (a of working.actions || []; track a; let i = $index) {\n <div\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('Global action') }}</mat-label>\n <mat-select\n [ngModel]=\"getGlobalActionId(a)\"\n (ngModelChange)=\"onActionGlobalActionIdChange(a, $event)\"\n >\n <mat-option value=\"\">{{ tx('None') }}</mat-option>\n @for (action of globalActionCatalog; track action) {\n <mat-option [value]=\"action.id\">\n {{ action.label || action.id }}\n </mat-option>\n }\n </mat-select>\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 @if (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 }\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 @for (c of paletteOptions; track c) {\n <mat-option [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 }\n </mat-select>\n </mat-form-field>\n @if (isCustomColor(a.color)) {\n <div\n class=\"g gap-8\"\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 } @else {\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n }\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ actionVisibilityLabel() }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"actionShowIfModel(i)\"\n (ngModelChange)=\"onActionShowIfChanged(i, a, $event)\"\n ></textarea>\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 @if ((a.kind || 'icon') === 'icon') {\n <button\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 }\n @if (a.kind === 'button') {\n @if (a.buttonVariant === 'stroked') {\n <button\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 }\n @if (a.buttonVariant === 'raised') {\n <button\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 }\n @if (!a.buttonVariant || a.buttonVariant === 'flat') {\n <button\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 }\n }\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\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Placement') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.placement\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"trailing\">{{ tx('Trailing') }}</mat-option>\n <mat-option value=\"actions\">{{ tx('Actions column') }}</mat-option>\n </mat-select>\n </mat-form-field>\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 @if (!a.confirmation?.title && !a.confirmation?.message) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n }\n </div>\n </div>\n </mat-expansion-panel>\n @if (a.globalAction?.actionId) {\n @if (isSurfaceOpenCommand(a)) {\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n } @else {\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]=\"getGlobalActionPayloadText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadTextChange(a, $event)\"\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]=\"globalActionPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n @if (isGlobalActionPayloadInvalid(a)) {\n <mat-error\n >{{ tx('Invalid JSON') }}</mat-error\n >\n }\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload expression') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getGlobalActionPayloadExprText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadExprTextChange(a, $event)\"\n placeholder=\"item.warningMessage\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Advanced escape hatch. When set, payload is resolved from this expression and no default item payload is injected.')\"\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 mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalActionPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalActionPayloadExampleHint(a)\n }}</span>\n </div>\n }\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n }\n </div>\n </div>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Layout')\">\n <ng-template matTabContent>\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 @if (working.layout.variant === 'list') {\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 } @else {\n @if (working.layout.variant === 'tiles') {\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 } @else {\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 }\n }\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 @if (working.layout.variant !== 'tiles') {\n <mat-form-field\n appearance=\"outline\"\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 }\n @if (fields.length > 0) {\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\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 }\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 <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Row layout') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Stable columns for list rows')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"grid gap-3\">\n <div class=\"g g-1-auto ai-center gap-12\">\n <mat-slide-toggle\n [ngModel]=\"rowLayoutEnabled()\"\n (ngModelChange)=\"onRowLayoutEnabledChange($event)\"\n >{{ tx('Enable row layout') }}</mat-slide-toggle\n >\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyCorporateRowLayoutPreset()\"\n >\n {{ tx('Corporate row preset') }}\n </button>\n </div>\n @if (working.layout.rowLayout) {\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Layout type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.rowLayout.type\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n <mat-option value=\"grid\">{{ tx('Grid') }}</mat-option>\n <mat-option value=\"flex\">{{ tx('Flex') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Vertical alignment') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.rowLayout.itemAlignY\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (align of rowLayoutAlignOptions; track align) {\n <mat-option [value]=\"align\">{{ align }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gap') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.rowLayout.gap\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"16px\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Columns') }}\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addRowLayoutColumn()\"\n >\n {{ tx('Add column') }}\n </button>\n </div>\n @for (\n column of working.layout.rowLayout.columns || [];\n track column;\n let i = $index\n ) {\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Slot') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.slot\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (slot of rowLayoutSlotOptions; track slot) {\n <mat-option [value]=\"slot\">{{ slot }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.width\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"1fr\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Min width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.minWidth\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"120px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Max width') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"column.maxWidth\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n placeholder=\"320px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Align') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.align\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (align of rowLayoutColumnAlignOptions; track align) {\n <mat-option [value]=\"align\">{{ align }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Justify') }}</mat-label>\n <mat-select\n [(ngModel)]=\"column.justify\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n >\n @for (justify of rowLayoutJustifyOptions; track justify) {\n <mat-option [value]=\"justify\">{{ justify }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <div class=\"g row-flow gap-4 ai-center\">\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move column left')\"\n (click)=\"moveRowLayoutColumn(i, -1)\"\n >\n <mat-icon>arrow_back</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === (working.layout.rowLayout.columns?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move column right')\"\n (click)=\"moveRowLayoutColumn(i, 1)\"\n >\n <mat-icon>arrow_forward</mat-icon>\n </button>\n <button\n mat-icon-button\n color=\"warn\"\n type=\"button\"\n [attr.aria-label]=\"tx('Remove column')\"\n (click)=\"removeRowLayoutColumn(i)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n @if (rowLayoutColumnWarning(column); as warning) {\n <div class=\"muted col-span-2\">\n {{ warning }}\n </div>\n }\n </div>\n }\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Advanced row CSS') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.rowLayout.class\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <textarea\n matInput\n rows=\"2\"\n [(ngModel)]=\"working.layout.rowLayout.style\"\n (ngModelChange)=\"onRowLayoutChanged()\"\n ></textarea>\n </mat-form-field>\n </div>\n </mat-expansion-panel>\n }\n </div>\n </mat-expansion-panel>\n <mat-divider class=\"my-8\"></mat-divider>\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Expansion') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Inline details for list rows')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"grid gap-3\">\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [ngModel]=\"expansionEnabled()\"\n (ngModelChange)=\"onExpansionEnabledChange($event)\"\n >{{ tx('Enable expansion') }}</mat-slide-toggle\n >\n @if (working.interaction?.expandable) {\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Trigger') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandTriggerModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandTrigger', $event)\"\n >\n @for (trigger of expandTriggerOptions; track trigger) {\n <mat-option [value]=\"trigger\">{{ trigger }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandModeModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandMode', $event)\"\n >\n @for (mode of expandModeOptions; track mode) {\n <mat-option [value]=\"mode\">{{ mode }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Control placement') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpandPlacementModel()\"\n (ngModelChange)=\"onExpansionInteractionChanged('expandPlacement', $event)\"\n >\n @for (placement of expandPlacementOptions; track placement) {\n <mat-option [value]=\"placement\">{{ placement }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n </div>\n @if (working.interaction?.expandable && working.expansion) {\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shell') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpansionShellModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('shell', $event)\"\n >\n @for (shell of expansionShellOptions; track shell) {\n <mat-option [value]=\"shell\">{{ shell }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Columns') }}</mat-label>\n <mat-select\n [ngModel]=\"getExpansionColumnsModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('columns', $event)\"\n >\n @for (columns of expansionColumnOptions; track columns) {\n <mat-option [value]=\"columns\">{{ columns }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gap') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExpansionGapModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('gap', $event)\"\n placeholder=\"12px\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Padding') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExpansionPaddingModel()\"\n (ngModelChange)=\"onExpansionRenderingChanged('padding', $event)\"\n placeholder=\"16px\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Sections') }}\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addExpansionSection()\"\n >\n {{ tx('Add section') }}\n </button>\n </div>\n @for (\n section of working.expansion.sections || [];\n track section.id;\n let i = $index\n ) {\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Id') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.id\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.title\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"section.type\"\n (ngModelChange)=\"onExpansionChanged()\"\n >\n @for (type of expansionSectionTypeOptions; track type) {\n <mat-option [value]=\"type\">{{ type }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Items expression') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.itemsExpr\"\n (ngModelChange)=\"onExpansionChanged()\"\n placeholder=\"${item.details}\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Empty label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"section.emptyLabel\"\n (ngModelChange)=\"onExpansionChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Show when (Json Logic)') }}</mat-label>\n <textarea\n matInput\n rows=\"2\"\n [ngModel]=\"getExpansionSectionShowIfText(section)\"\n (ngModelChange)=\"onExpansionSectionShowIfChanged(section, $event)\"\n ></textarea>\n </mat-form-field>\n <div class=\"g row-flow gap-4 ai-center\">\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move section up')\"\n (click)=\"moveExpansionSection(i, -1)\"\n >\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [disabled]=\"i === (working.expansion.sections?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move section down')\"\n (click)=\"moveExpansionSection(i, 1)\"\n >\n <mat-icon>arrow_downward</mat-icon>\n </button>\n <button\n mat-icon-button\n color=\"warn\"\n type=\"button\"\n [attr.aria-label]=\"tx('Remove section')\"\n (click)=\"removeExpansionSection(i)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </mat-expansion-panel>\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 <mat-slide-toggle\n [ngModel]=\"exportEnabled()\"\n (ngModelChange)=\"onExportEnabledChange($event)\"\n >{{ tx('Enable export') }}</mat-slide-toggle\n >\n </div>\n @if (working.export?.enabled) {\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel mt-12\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Export') }}</mat-panel-title>\n <mat-panel-description>{{\n tx('Formats, scope and file options')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Formats') }}</mat-label>\n <mat-select\n multiple\n [ngModel]=\"getExportFormatsModel()\"\n (ngModelChange)=\"onExportFormatsChange($event)\"\n >\n @for (format of exportFormatOptions; track format) {\n <mat-option [value]=\"format\">{{\n format.toUpperCase()\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Scope') }}</mat-label>\n <mat-select\n [ngModel]=\"getExportScopeModel()\"\n (ngModelChange)=\"onExportScopeChange($event)\"\n >\n @for (scope of exportScopeOptions; track scope) {\n <mat-option [value]=\"scope\">{{ scope }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Max rows') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [ngModel]=\"getExportMaxRowsModel()\"\n (ngModelChange)=\"onExportMaxRowsChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default file name') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExportDefaultFileNameModel()\"\n (ngModelChange)=\"onExportDefaultFileNameChange($event)\"\n placeholder=\"praxis-list-export\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Included fields') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getExportIncludeFieldsText()\"\n (ngModelChange)=\"onExportIncludeFieldsTextChange($event)\"\n [placeholder]=\"tx('Empty means all fields')\"\n />\n @if (getExportIncludeFieldsWarning(); as warning) {\n <mat-hint>{{ warning }}</mat-hint>\n }\n </mat-form-field>\n <mat-slide-toggle\n [ngModel]=\"getExportIncludeHeadersModel()\"\n (ngModelChange)=\"onExportIncludeHeadersChange($event)\"\n >{{ tx('Include headers') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [ngModel]=\"getExportApplyFormattingModel()\"\n (ngModelChange)=\"onExportApplyFormattingChange($event)\"\n >{{ tx('Apply formatting') }}</mat-slide-toggle\n >\n </div>\n </mat-expansion-panel>\n }\n @if (working.ui?.showSearch) {\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 }\n @if (working.ui?.showSort) {\n <div class=\"mt-12\">\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 @for (r of uiSortRows; track r; let i = $index) {\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @if (isUiSortRowDuplicate(i)) {\n <div class=\"error\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n }\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n }\n </div>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Content')\">\n <ng-template matTabContent>\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of primaryTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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 @if (\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n ) {\n <mat-form-field\n appearance=\"outline\"\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 }\n @if (\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n ) {\n <mat-form-field\n appearance=\"outline\"\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 }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of secondaryTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (mappingMetaFields.length) {\n <div\n class=\"g g-1-1 ai-center gap-12\"\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 }\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n @if (!mappingMetaFields.length) {\n <div class=\"g g-1-1 gap-12\">\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of metaTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\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 @for (mt of trailingTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n @if (!mappingTrailing.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 @for (mt of leadingTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n @if (\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n ) {\n <mat-form-field\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n @for (f of fields; track f) {\n <mat-option [value]=\"f\">{{\n f\n }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\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 @if (mappingLeading.type === 'icon') {\n <div\n class=\"g g-1-auto gap-12 ai-center\"\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 }\n @if (mappingLeading.type === 'icon') {\n <div>\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\n <!-- Image Specific -->\n @if (mappingLeading.type === 'image') {\n <div\n class=\"g g-1-1 gap-12\"\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 @if (isImageUrlRequiredInvalid(mappingLeading.imageUrl)) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\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\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @for (f of features; track f; let i = $index) {\n <div\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 }\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 @for (mt of sectionHeaderTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n @if (!mappingSectionHeader.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 @for (mt of emptyStateTypeConfigs; track mt) {\n <mat-option\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n }\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.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\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 @if (\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n ) {\n <mat-error\n >{{ tx('URL/expr is required') }}</mat-error\n >\n }\n </mat-form-field>\n </div>\n @if (!mappingEmptyState.imageUrl) {\n <div\n class=\"text-caption muted\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Rules')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div>\n <div class=\"subtitle\">{{ tx('Item style rules') }}</div>\n <div class=\"muted\">\n {{ tx('Apply class, style, border or background when Json Logic matches.') }}\n </div>\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addItemStyleRule()\"\n >\n {{ tx('Add item rule') }}\n </button>\n </div>\n @for (\n rule of working.rules?.itemStyles || [];\n track rule.id;\n let i = $index\n ) {\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\" [expanded]=\"i === 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ rule.id || tx('Item rule') }}</mat-panel-title>\n <mat-panel-description>{{ rule.class || rule.background || rule.border }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Id') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.id\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Condition (Json Logic)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"getRuleConditionText(rule)\"\n (ngModelChange)=\"onRuleConditionChanged(rule, $event)\"\n ></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.class\"\n (ngModelChange)=\"onRulesChanged()\"\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)]=\"rule.style\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.border\"\n (ngModelChange)=\"onRulesChanged()\"\n placeholder=\"1px solid var(--md-sys-color-outline)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Background') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.background\"\n (ngModelChange)=\"onRulesChanged()\"\n placeholder=\"var(--md-sys-color-surface-container)\"\n />\n </mat-form-field>\n <div class=\"flex-end\">\n <button\n mat-button\n color=\"warn\"\n type=\"button\"\n (click)=\"removeItemStyleRule(i)\"\n >\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"col-span-2\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div>\n <div class=\"subtitle\">{{ tx('Canonical effect') }}</div>\n <div class=\"muted\">\n {{ tx('Materialize the first effects[] entry used by the runtime.') }}\n </div>\n </div>\n @if (getItemStyleEffect(rule)) {\n <button\n mat-button\n type=\"button\"\n (click)=\"clearItemStyleEffects(rule)\"\n >\n {{ tx('Use direct fields') }}\n </button>\n } @else {\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"ensureItemStyleEffect(rule)\"\n >\n {{ tx('Use effects[]') }}\n </button>\n }\n </div>\n </div>\n @if (getItemStyleEffect(rule); as effect) {\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.class\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.style\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.border\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect background') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.background\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n }\n </div>\n </mat-expansion-panel>\n }\n\n <mat-divider class=\"my-8\"></mat-divider>\n\n <div class=\"g g-1-auto ai-center gap-8\">\n <div>\n <div class=\"subtitle\">{{ tx('Slot override rules') }}</div>\n <div class=\"muted\">\n {{ tx('Replace, decorate or hide a slot when Json Logic matches.') }}\n </div>\n </div>\n <button\n mat-flat-button\n color=\"primary\"\n type=\"button\"\n (click)=\"addSlotOverrideRule()\"\n >\n {{ tx('Add slot rule') }}\n </button>\n </div>\n @for (\n rule of working.rules?.slotOverrides || [];\n track rule.id;\n let i = $index\n ) {\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\" [expanded]=\"i === 0\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ rule.id || tx('Slot rule') }}</mat-panel-title>\n <mat-panel-description>{{ rule.slot }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Id') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.id\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Slot') }}</mat-label>\n <mat-select\n [(ngModel)]=\"rule.slot\"\n (ngModelChange)=\"onRulesChanged()\"\n >\n @for (slot of ruleSlotOptions; track slot) {\n <mat-option [value]=\"slot\">{{ slot }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Condition (Json Logic)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"getRuleConditionText(rule)\"\n (ngModelChange)=\"onRuleConditionChanged(rule, $event)\"\n ></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Template type') }}</mat-label>\n <mat-select\n [ngModel]=\"getSlotOverrideTemplateType(rule)\"\n (ngModelChange)=\"onSlotOverrideTemplateTypeChanged(rule, $event)\"\n >\n @for (type of ruleTemplateTypeOptions; track type) {\n <mat-option [value]=\"type\">{{ tx(type) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Template expression') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getSlotOverrideTemplateExpr(rule)\"\n (ngModelChange)=\"onSlotOverrideTemplateExprChanged(rule, $event)\"\n placeholder=\"${item.status}\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"rule.class\"\n (ngModelChange)=\"onRulesChanged()\"\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)]=\"rule.style\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"rule.hide\"\n (ngModelChange)=\"onRulesChanged()\"\n >{{ tx('Hide slot') }}</mat-slide-toggle>\n <div class=\"flex-end\">\n <button\n mat-button\n color=\"warn\"\n type=\"button\"\n (click)=\"removeSlotOverrideRule(i)\"\n >\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"col-span-2\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div>\n <div class=\"subtitle\">{{ tx('Canonical effect') }}</div>\n <div class=\"muted\">\n {{ tx('Materialize the first effects[] entry used by the runtime.') }}\n </div>\n </div>\n @if (getSlotOverrideEffect(rule)) {\n <button\n mat-button\n type=\"button\"\n (click)=\"clearSlotOverrideEffects(rule)\"\n >\n {{ tx('Use direct fields') }}\n </button>\n } @else {\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"ensureSlotOverrideEffect(rule)\"\n >\n {{ tx('Use effects[]') }}\n </button>\n }\n </div>\n </div>\n @if (getSlotOverrideEffect(rule); as effect) {\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect template type') }}</mat-label>\n <mat-select\n [ngModel]=\"getSlotOverrideTemplateType(effect)\"\n (ngModelChange)=\"onSlotOverrideTemplateTypeChanged(effect, $event)\"\n >\n @for (type of ruleTemplateTypeOptions; track type) {\n <mat-option [value]=\"type\">{{ tx(type) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect template expression') }}</mat-label>\n <input\n matInput\n [ngModel]=\"getSlotOverrideTemplateExpr(effect)\"\n (ngModelChange)=\"onSlotOverrideTemplateExprChanged(effect, $event)\"\n placeholder=\"${item.status}\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.class\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Effect style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"effect.style\"\n (ngModelChange)=\"onRulesChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"effect.hide\"\n (ngModelChange)=\"onRulesChanged()\"\n >{{ tx('Effect hides slot') }}</mat-slide-toggle>\n }\n </div>\n </mat-expansion-panel>\n }\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <ng-template matTabContent>\n @if (working?.a11y && working?.events) {\n <div\n class=\"editor-content grid gap-3\"\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>exportAction</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.exportAction\"\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 }\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <ng-template matTabContent>\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 </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\n <ng-template matTabContent>\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 @if (working.skin.type === 'glass') {\n <mat-form-field\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 }\n @if (working.skin.type === 'gradient-tile') {\n <div 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\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 @if (working.skin.type === 'custom') {\n <div\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 }\n </div>\n </ng-template>\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"] }]
|
|
6755
6964
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
6756
6965
|
type: Optional
|
|
6757
6966
|
}, {
|