@praxisui/list 9.0.68-rc.3 → 10.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/ai/component-registry.json +2 -2
- package/fesm2022/praxisui-list.mjs +72 -19
- package/package.json +6 -6
- package/types/praxisui-list.d.ts +11 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Injectable, Input, ChangeDetectionStrategy, Component, LOCALE_ID, EventEmitter, output, CSP_NONCE, ElementRef, Renderer2, ChangeDetectorRef, effect, Injector, isDevMode, booleanAttribute, Output, Optional, SkipSelf, Inject, ViewChild, ENVIRONMENT_INITIALIZER,
|
|
2
|
+
import { inject, Injectable, Input, ChangeDetectionStrategy, Component, LOCALE_ID, EventEmitter, output, CSP_NONCE, ElementRef, Renderer2, DestroyRef, ChangeDetectorRef, effect, Injector, isDevMode, booleanAttribute, Output, signal, Optional, SkipSelf, Inject, ViewChild, ENVIRONMENT_INITIALIZER, computed } from '@angular/core';
|
|
3
3
|
import { ActivatedRoute } from '@angular/router';
|
|
4
4
|
import * as i1 from '@angular/common';
|
|
5
5
|
import { formatDate, CommonModule } from '@angular/common';
|
|
@@ -6785,6 +6785,7 @@ function warningDiagnostic(code, message, path) {
|
|
|
6785
6785
|
|
|
6786
6786
|
const PRAXIS_LIST_EN_US = {
|
|
6787
6787
|
loading: 'Loading items…',
|
|
6788
|
+
'editor.saveFailed': 'Unable to save the list settings. Your draft was kept. Try again.',
|
|
6788
6789
|
"configurationSource.label": "Configuration source",
|
|
6789
6790
|
"configurationSource.page": "Page configuration",
|
|
6790
6791
|
"configurationSource.individual": "Individual list preferences",
|
|
@@ -7169,6 +7170,7 @@ const PRAXIS_LIST_EN_US = {
|
|
|
7169
7170
|
|
|
7170
7171
|
const PRAXIS_LIST_PT_BR = {
|
|
7171
7172
|
loading: 'Carregando itens…',
|
|
7173
|
+
'editor.saveFailed': 'Não foi possível salvar as configurações da lista. O rascunho foi preservado. Tente novamente.',
|
|
7172
7174
|
"configurationSource.label": "Origem da configuração",
|
|
7173
7175
|
"configurationSource.page": "Configuração da página",
|
|
7174
7176
|
"configurationSource.individual": "Preferências individuais da lista",
|
|
@@ -8840,6 +8842,7 @@ class PraxisList {
|
|
|
8840
8842
|
appliedRuntimeConfig;
|
|
8841
8843
|
data = inject(ListDataService);
|
|
8842
8844
|
settings = inject(SettingsPanelService);
|
|
8845
|
+
destroyRef = inject(DestroyRef);
|
|
8843
8846
|
cdr = inject(ChangeDetectorRef);
|
|
8844
8847
|
componentKeys = inject(ComponentKeyService);
|
|
8845
8848
|
logger = inject(LoggerService);
|
|
@@ -9039,11 +9042,8 @@ class PraxisList {
|
|
|
9039
9042
|
this.data.setQuery(q);
|
|
9040
9043
|
});
|
|
9041
9044
|
}
|
|
9042
|
-
persistConfig(key, config) {
|
|
9043
|
-
this.storage
|
|
9044
|
-
.saveConfig(key, config)
|
|
9045
|
-
.pipe(take(1))
|
|
9046
|
-
.subscribe({ error: () => { } });
|
|
9045
|
+
async persistConfig(key, config) {
|
|
9046
|
+
await firstValueFrom(this.storage.saveConfig(key, config));
|
|
9047
9047
|
}
|
|
9048
9048
|
applySkins() {
|
|
9049
9049
|
this.skinClasses = this.skin.toHostClasses(this.config).join(' ');
|
|
@@ -10407,6 +10407,7 @@ class PraxisList {
|
|
|
10407
10407
|
const key = this.storageKey();
|
|
10408
10408
|
const ref = this.settings.open({
|
|
10409
10409
|
id: `praxis-list-editor:${key || this.listId || 'default'}`,
|
|
10410
|
+
owner: this.destroyRef,
|
|
10410
10411
|
title: 'Lista – Configurações',
|
|
10411
10412
|
content: {
|
|
10412
10413
|
component: PraxisListConfigEditor,
|
|
@@ -10414,6 +10415,21 @@ class PraxisList {
|
|
|
10414
10415
|
config: this.config,
|
|
10415
10416
|
document: createListAuthoringDocument({ config: this.config }),
|
|
10416
10417
|
listId: this.listId,
|
|
10418
|
+
persistSettings: async (raw) => {
|
|
10419
|
+
const document = parseLegacyOrListDocument(raw);
|
|
10420
|
+
const plan = buildListApplyPlan(document, this.buildListEditorRuntimeContext(document));
|
|
10421
|
+
if (plan.diagnostics.some((item) => item.level === 'error')) {
|
|
10422
|
+
throw new Error('Invalid list authoring document');
|
|
10423
|
+
}
|
|
10424
|
+
// Capture the destination of this session; an old response cannot
|
|
10425
|
+
// publish into a different list identity.
|
|
10426
|
+
if (this.destroyRef.destroyed || key !== this.storageKey())
|
|
10427
|
+
throw new Error('List editor target changed');
|
|
10428
|
+
if (key)
|
|
10429
|
+
await this.persistConfig(key, normalizeListConfig(plan.canonicalConfig));
|
|
10430
|
+
if (this.destroyRef.destroyed || key !== this.storageKey())
|
|
10431
|
+
throw new Error('List editor target changed');
|
|
10432
|
+
},
|
|
10417
10433
|
},
|
|
10418
10434
|
},
|
|
10419
10435
|
});
|
|
@@ -10421,7 +10437,7 @@ class PraxisList {
|
|
|
10421
10437
|
this.applyAuthoringPayload(applied, 'settings-applied');
|
|
10422
10438
|
});
|
|
10423
10439
|
ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((saved) => {
|
|
10424
|
-
this.applyAuthoringPayload(saved, 'settings-saved');
|
|
10440
|
+
this.applyAuthoringPayload(saved, 'settings-saved', false);
|
|
10425
10441
|
});
|
|
10426
10442
|
}
|
|
10427
10443
|
openAiAssistant() {
|
|
@@ -10821,10 +10837,10 @@ class PraxisList {
|
|
|
10821
10837
|
applyConfigFromAdapter(newCfg) {
|
|
10822
10838
|
this.applyAuthoringPayload(newCfg, 'adapter');
|
|
10823
10839
|
}
|
|
10824
|
-
applyAuthoringPayload(raw, trigger) {
|
|
10840
|
+
applyAuthoringPayload(raw, trigger, persist = trigger === 'settings-saved') {
|
|
10825
10841
|
const doc = parseLegacyOrListDocument(raw);
|
|
10826
10842
|
const plan = buildListApplyPlan(doc, this.buildListEditorRuntimeContext(doc), {
|
|
10827
|
-
saveConfig:
|
|
10843
|
+
saveConfig: persist,
|
|
10828
10844
|
});
|
|
10829
10845
|
this.executeListEditorApplyPlan(plan, doc, trigger);
|
|
10830
10846
|
}
|
|
@@ -10890,7 +10906,10 @@ class PraxisList {
|
|
|
10890
10906
|
if (plan.persistence?.saveConfig) {
|
|
10891
10907
|
const key = this.storageKey();
|
|
10892
10908
|
if (key) {
|
|
10893
|
-
this.persistConfig(key, this.config)
|
|
10909
|
+
void this.persistConfig(key, this.config).catch((error) => {
|
|
10910
|
+
this.logger.error('[PraxisList] Failed to save list settings.', this.buildLogOptions({ error }));
|
|
10911
|
+
this.snackBar.open(this.t('editor.saveFailed', ''), undefined, { duration: 5000 });
|
|
10912
|
+
});
|
|
10894
10913
|
}
|
|
10895
10914
|
}
|
|
10896
10915
|
if (plan.runtime?.schemaInference) {
|
|
@@ -12827,6 +12846,16 @@ class PraxisListConfigEditor {
|
|
|
12827
12846
|
isDirty$ = new BehaviorSubject(false);
|
|
12828
12847
|
isValid$ = new BehaviorSubject(true);
|
|
12829
12848
|
isBusy$ = new BehaviorSubject(false);
|
|
12849
|
+
saveError = signal('', ...(ngDevMode ? [{ debugName: "saveError" }] : /* istanbul ignore next */ []));
|
|
12850
|
+
destroyRef = inject(DestroyRef);
|
|
12851
|
+
saving = false;
|
|
12852
|
+
editorBusy = false;
|
|
12853
|
+
persistSettings;
|
|
12854
|
+
setEditorBusy(value) {
|
|
12855
|
+
this.editorBusy = value;
|
|
12856
|
+
this.isBusy$.next(value || this.saving);
|
|
12857
|
+
}
|
|
12858
|
+
onBeforeClose() { return !this.saving; }
|
|
12830
12859
|
initialJson = '';
|
|
12831
12860
|
lastJson = '';
|
|
12832
12861
|
fields = [];
|
|
@@ -12953,6 +12982,7 @@ class PraxisListConfigEditor {
|
|
|
12953
12982
|
constructor(injected) {
|
|
12954
12983
|
// Disable Immer auto-freeze to allow [(ngModel)] mutations on produced state
|
|
12955
12984
|
setAutoFreeze(false);
|
|
12985
|
+
this.persistSettings = injected?.persistSettings;
|
|
12956
12986
|
// optional inject
|
|
12957
12987
|
this.crud = inject(GenericCrudService, {
|
|
12958
12988
|
optional: true,
|
|
@@ -13063,10 +13093,33 @@ class PraxisListConfigEditor {
|
|
|
13063
13093
|
return { document, config: document.config, id: this.listId };
|
|
13064
13094
|
}
|
|
13065
13095
|
onSave() {
|
|
13066
|
-
if (this.hasInvalidSurfaceDraft || !this.isValid$.value)
|
|
13096
|
+
if (this.saving || this.destroyRef.destroyed || this.hasInvalidSurfaceDraft || !this.isValid$.value)
|
|
13067
13097
|
return;
|
|
13068
|
-
const
|
|
13069
|
-
|
|
13098
|
+
const value = this.getSettingsValue();
|
|
13099
|
+
if (!this.persistSettings)
|
|
13100
|
+
return value;
|
|
13101
|
+
return this.persistDraft(value, this.persistSettings);
|
|
13102
|
+
}
|
|
13103
|
+
async persistDraft(value, persist) {
|
|
13104
|
+
const snapshot = JSON.stringify(value);
|
|
13105
|
+
this.saving = true;
|
|
13106
|
+
this.isBusy$.next(true);
|
|
13107
|
+
this.saveError.set('');
|
|
13108
|
+
try {
|
|
13109
|
+
await persist(value);
|
|
13110
|
+
if (this.destroyRef.destroyed || !this.isValid$.value)
|
|
13111
|
+
return undefined;
|
|
13112
|
+
return snapshot === JSON.stringify(this.getSettingsValue()) ? value : undefined;
|
|
13113
|
+
}
|
|
13114
|
+
catch (error) {
|
|
13115
|
+
if (!this.destroyRef.destroyed)
|
|
13116
|
+
this.saveError.set(this.tx('editor.saveFailed'));
|
|
13117
|
+
throw error;
|
|
13118
|
+
}
|
|
13119
|
+
finally {
|
|
13120
|
+
this.saving = false;
|
|
13121
|
+
this.isBusy$.next(this.editorBusy);
|
|
13122
|
+
}
|
|
13070
13123
|
}
|
|
13071
13124
|
onJsonConfigChange(newValue) {
|
|
13072
13125
|
this.applyIncomingDocument(newValue);
|
|
@@ -13153,7 +13206,7 @@ class PraxisListConfigEditor {
|
|
|
13153
13206
|
// Debounced schema loader for resourcePath
|
|
13154
13207
|
setupSchemaWatcher() {
|
|
13155
13208
|
this.resourcePathChanges
|
|
13156
|
-
.pipe(debounceTime$1(300), distinctUntilChanged(), tap(() => this.
|
|
13209
|
+
.pipe(debounceTime$1(300), distinctUntilChanged(), tap(() => this.setEditorBusy(true)), switchMap((rp) => {
|
|
13157
13210
|
const path = (rp || '').trim();
|
|
13158
13211
|
if (!path || !this.crud) {
|
|
13159
13212
|
this.fields = [];
|
|
@@ -13171,7 +13224,7 @@ class PraxisListConfigEditor {
|
|
|
13171
13224
|
this.fields = (fields || [])
|
|
13172
13225
|
.map((f) => f?.name || f?.field || f?.key)
|
|
13173
13226
|
.filter((n) => !!n);
|
|
13174
|
-
this.
|
|
13227
|
+
this.setEditorBusy(false);
|
|
13175
13228
|
this.verify();
|
|
13176
13229
|
});
|
|
13177
13230
|
}
|
|
@@ -13811,7 +13864,7 @@ class PraxisListConfigEditor {
|
|
|
13811
13864
|
source.discardInvalidDrafts();
|
|
13812
13865
|
this.jsonEditorIsValid = true;
|
|
13813
13866
|
try {
|
|
13814
|
-
this.
|
|
13867
|
+
this.setEditorBusy(true);
|
|
13815
13868
|
const parsed = parseLegacyOrListDocument(JSON.parse(this.initialJson));
|
|
13816
13869
|
this.applyIncomingDocument(parsed);
|
|
13817
13870
|
this.lastJson = this.initialJson;
|
|
@@ -13821,7 +13874,7 @@ class PraxisListConfigEditor {
|
|
|
13821
13874
|
this.isValid$.next(true);
|
|
13822
13875
|
}
|
|
13823
13876
|
finally {
|
|
13824
|
-
this.
|
|
13877
|
+
this.setEditorBusy(false);
|
|
13825
13878
|
}
|
|
13826
13879
|
}
|
|
13827
13880
|
onLayoutChanged() {
|
|
@@ -15750,7 +15803,7 @@ class PraxisListConfigEditor {
|
|
|
15750
15803
|
},
|
|
15751
15804
|
providePraxisI18nConfig(SURFACE_OPEN_I18N_CONFIG),
|
|
15752
15805
|
providePraxisListI18n(),
|
|
15753
|
-
], ngImport: i0, template: "<mat-tab-group [preserveContent]=\"true\" 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 type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n [attr.aria-label]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n [attr.aria-label]=\"tx('Resource endpoint (resourcePath).')\"\n ></button>\n </mat-form-field>\n <div class=\"grid gap-2 mb-12\">\n <div class=\"muted\">{{ getLocalDataSummary() }}</div>\n @if (getLocalDataPrecedenceWarning(); as warning) {\n <div class=\"muted\">{{ warning }}</div>\n }\n @if (getLocalDataColumns().length) {\n <div class=\"muted\">\n {{ tx('Local columns') }}:\n {{ getLocalDataColumns().join(', ') }}\n </div>\n @for (row of getLocalDataPreviewRows(); track $index) {\n <div class=\"muted\">\n @for (column of getLocalDataColumns().slice(0, 6); track column) {\n <span>{{ column }}={{ row[column] }}</span>\n }\n </div>\n }\n }\n </div>\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n [attr.aria-label]=\"tx('Optional. Use valid JSON for initial filters.')\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n [attr.aria-label]=\"tx('Resource base field.')\"\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.id) {\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 $index; 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.id) {\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.value || 'default') {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n [attr.aria-label]=\"actionConditionTooltip()\"\n ></button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n @if ((a.kind || 'icon') === 'icon') {\n <span class=\"action-icon-preview\" aria-hidden=\"true\">\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </span>\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\" [disabled]=\"hasInvalidSurfaceDraft\" (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 #listSurfaceDraft\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n (validationChange)=\"onSurfaceDraftValidity(listSurfaceDraft, $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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalActionPayloadSchemaTooltip(a)\"\n [attr.aria-label]=\"globalActionPayloadSchemaTooltip(a)\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\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 [attr.aria-label]=\"tx('Advanced escape hatch. When set, payload is resolved from this expression and no default item payload is injected.')\"\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 $index;\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 type=\"button\"\n [praxisIconButton]=\"'arrow_back'\"\n size=\"dense\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move column left')\"\n (click)=\"moveRowLayoutColumn(i, -1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'arrow_forward'\"\n size=\"dense\"\n [disabled]=\"i === (working.layout.rowLayout.columns?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move column right')\"\n (click)=\"moveRowLayoutColumn(i, 1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [attr.aria-label]=\"tx('Remove column')\"\n (click)=\"removeRowLayoutColumn(i)\"\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=\"grid gap-2\">\n <div class=\"muted\">{{ getExpansionRemoteSummary() }}</div>\n @for (warning of getExpansionRemoteWarnings(); track warning) {\n <div class=\"muted\">{{ warning }}</div>\n }\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 type=\"button\"\n [praxisIconButton]=\"'arrow_upward'\"\n size=\"dense\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move section up')\"\n (click)=\"moveExpansionSection(i, -1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'arrow_downward'\"\n size=\"dense\"\n [disabled]=\"i === (working.expansion.sections?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move section down')\"\n (click)=\"moveExpansionSection(i, 1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [attr.aria-label]=\"tx('Remove section')\"\n (click)=\"removeExpansionSection(i)\"\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 <div class=\"grid gap-2 mt-12\">\n <div class=\"subtitle\">{{ tx('Export preview') }}</div>\n <div class=\"muted\">{{ getExportPreviewSummary() }}</div>\n @if (getExportPreviewColumns().length) {\n <div class=\"muted\">\n {{ tx('Columns') }}:\n {{ getExportPreviewColumns().join(', ') }}\n </div>\n @for (row of getExportPreviewRows(); track $index) {\n <div class=\"muted\">\n @for (column of getExportPreviewColumns(); track column) {\n <span>{{ column }}={{ row[column] }}</span>\n }\n </div>\n }\n } @else {\n <div class=\"muted\">\n {{ tx('No fields available for preview yet.') }}\n </div>\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 $index; 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.type) {\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.type) {\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.type) {\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.type) {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n [attr.aria-label]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\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.type) {\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\n matSuffix\n type=\"button\"\n [praxisIconButton]=\"'search'\"\n size=\"dense\"\n [matTooltip]=\"tx('Choose icon')\"\n [attr.aria-label]=\"tx('Choose icon')\"\n (click)=\"pickLeadingIcon()\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n [attr.aria-label]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\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 $index; let i = $index) {\n <div\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button\n type=\"button\"\n [praxisIconButton]=\"f.icon || 'search'\"\n size=\"dense\"\n [matTooltip]=\"tx('Choose feature icon')\"\n [attr.aria-label]=\"tx('Choose feature icon')\"\n (click)=\"pickFeatureIcon(i)\"\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\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [matTooltip]=\"tx('Remove feature')\"\n [attr.aria-label]=\"tx('Remove feature')\"\n (click)=\"removeFeature(i)\"\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.type) {\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.type) {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n aria-label=\"formControlName\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n aria-label=\"formControlPath\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n [attr.aria-label]=\"tx('Unique item key.')\"\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{min-width:0;grid-template-columns:minmax(0,1fr);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{vertical-align:middle}.editor-icon-action--danger{color:var(--md-sys-color-error)}.action-icon-preview{display:inline-grid;place-items:center;width:36px;height:36px;color:var(--md-sys-color-on-surface-variant)}.action-icon-preview mat-icon{width:18px;height:18px;font-size:18px;line-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}.preset-row{display:flex;flex-wrap:wrap}.preset-row button{white-space:nowrap}.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$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2$2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2$2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2$2.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i2$2.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$2.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$2.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8$1.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$1.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$1.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: i14.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i2$1.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: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { 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", "governedSelectionChange", "open", "close", "cancel", "activeViewChange", "activeColorClick", "focusEvent", "blurEvent"] }, { kind: "component", type: SurfaceOpenActionEditorComponent, selector: "praxis-surface-open-action-editor", inputs: ["value", "hostKind"], outputs: ["valueChange", "validationChange"] }] });
|
|
15806
|
+
], ngImport: i0, template: "@if (saveError()) {\n <p role=\"alert\">{{ saveError() }}</p>\n}\n<mat-tab-group [preserveContent]=\"true\" 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 type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n [attr.aria-label]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n [attr.aria-label]=\"tx('Resource endpoint (resourcePath).')\"\n ></button>\n </mat-form-field>\n <div class=\"grid gap-2 mb-12\">\n <div class=\"muted\">{{ getLocalDataSummary() }}</div>\n @if (getLocalDataPrecedenceWarning(); as warning) {\n <div class=\"muted\">{{ warning }}</div>\n }\n @if (getLocalDataColumns().length) {\n <div class=\"muted\">\n {{ tx('Local columns') }}:\n {{ getLocalDataColumns().join(', ') }}\n </div>\n @for (row of getLocalDataPreviewRows(); track $index) {\n <div class=\"muted\">\n @for (column of getLocalDataColumns().slice(0, 6); track column) {\n <span>{{ column }}={{ row[column] }}</span>\n }\n </div>\n }\n }\n </div>\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n [attr.aria-label]=\"tx('Optional. Use valid JSON for initial filters.')\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n [attr.aria-label]=\"tx('Resource base field.')\"\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.id) {\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 $index; 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.id) {\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.value || 'default') {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n [attr.aria-label]=\"actionConditionTooltip()\"\n ></button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n @if ((a.kind || 'icon') === 'icon') {\n <span class=\"action-icon-preview\" aria-hidden=\"true\">\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </span>\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\" [disabled]=\"hasInvalidSurfaceDraft\" (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 #listSurfaceDraft\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n (validationChange)=\"onSurfaceDraftValidity(listSurfaceDraft, $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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalActionPayloadSchemaTooltip(a)\"\n [attr.aria-label]=\"globalActionPayloadSchemaTooltip(a)\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\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 [attr.aria-label]=\"tx('Advanced escape hatch. When set, payload is resolved from this expression and no default item payload is injected.')\"\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 $index;\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 type=\"button\"\n [praxisIconButton]=\"'arrow_back'\"\n size=\"dense\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move column left')\"\n (click)=\"moveRowLayoutColumn(i, -1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'arrow_forward'\"\n size=\"dense\"\n [disabled]=\"i === (working.layout.rowLayout.columns?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move column right')\"\n (click)=\"moveRowLayoutColumn(i, 1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [attr.aria-label]=\"tx('Remove column')\"\n (click)=\"removeRowLayoutColumn(i)\"\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=\"grid gap-2\">\n <div class=\"muted\">{{ getExpansionRemoteSummary() }}</div>\n @for (warning of getExpansionRemoteWarnings(); track warning) {\n <div class=\"muted\">{{ warning }}</div>\n }\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 type=\"button\"\n [praxisIconButton]=\"'arrow_upward'\"\n size=\"dense\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move section up')\"\n (click)=\"moveExpansionSection(i, -1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'arrow_downward'\"\n size=\"dense\"\n [disabled]=\"i === (working.expansion.sections?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move section down')\"\n (click)=\"moveExpansionSection(i, 1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [attr.aria-label]=\"tx('Remove section')\"\n (click)=\"removeExpansionSection(i)\"\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 <div class=\"grid gap-2 mt-12\">\n <div class=\"subtitle\">{{ tx('Export preview') }}</div>\n <div class=\"muted\">{{ getExportPreviewSummary() }}</div>\n @if (getExportPreviewColumns().length) {\n <div class=\"muted\">\n {{ tx('Columns') }}:\n {{ getExportPreviewColumns().join(', ') }}\n </div>\n @for (row of getExportPreviewRows(); track $index) {\n <div class=\"muted\">\n @for (column of getExportPreviewColumns(); track column) {\n <span>{{ column }}={{ row[column] }}</span>\n }\n </div>\n }\n } @else {\n <div class=\"muted\">\n {{ tx('No fields available for preview yet.') }}\n </div>\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 $index; 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.type) {\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.type) {\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.type) {\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.type) {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n [attr.aria-label]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\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.type) {\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\n matSuffix\n type=\"button\"\n [praxisIconButton]=\"'search'\"\n size=\"dense\"\n [matTooltip]=\"tx('Choose icon')\"\n [attr.aria-label]=\"tx('Choose icon')\"\n (click)=\"pickLeadingIcon()\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n [attr.aria-label]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\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 $index; let i = $index) {\n <div\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button\n type=\"button\"\n [praxisIconButton]=\"f.icon || 'search'\"\n size=\"dense\"\n [matTooltip]=\"tx('Choose feature icon')\"\n [attr.aria-label]=\"tx('Choose feature icon')\"\n (click)=\"pickFeatureIcon(i)\"\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\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [matTooltip]=\"tx('Remove feature')\"\n [attr.aria-label]=\"tx('Remove feature')\"\n (click)=\"removeFeature(i)\"\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.type) {\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.type) {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n aria-label=\"formControlName\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n aria-label=\"formControlPath\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n [attr.aria-label]=\"tx('Unique item key.')\"\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{min-width:0;grid-template-columns:minmax(0,1fr);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{vertical-align:middle}.editor-icon-action--danger{color:var(--md-sys-color-error)}.action-icon-preview{display:inline-grid;place-items:center;width:36px;height:36px;color:var(--md-sys-color-on-surface-variant)}.action-icon-preview mat-icon{width:18px;height:18px;font-size:18px;line-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}.preset-row{display:flex;flex-wrap:wrap}.preset-row button{white-space:nowrap}.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$2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2$2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2$2.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2$2.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i2$2.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$2.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$2.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8$1.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$1.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$1.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: i14.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i2$1.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: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { 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", "governedSelectionChange", "open", "close", "cancel", "activeViewChange", "activeColorClick", "focusEvent", "blurEvent"] }, { kind: "component", type: SurfaceOpenActionEditorComponent, selector: "praxis-surface-open-action-editor", inputs: ["value", "hostKind"], outputs: ["valueChange", "validationChange"] }] });
|
|
15754
15807
|
}
|
|
15755
15808
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisListConfigEditor, decorators: [{
|
|
15756
15809
|
type: Component,
|
|
@@ -15795,7 +15848,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
15795
15848
|
PraxisListJsonConfigEditorComponent,
|
|
15796
15849
|
PdxColorPickerComponent,
|
|
15797
15850
|
SurfaceOpenActionEditorComponent,
|
|
15798
|
-
], template: "<mat-tab-group [preserveContent]=\"true\" 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 type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n [attr.aria-label]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n [attr.aria-label]=\"tx('Resource endpoint (resourcePath).')\"\n ></button>\n </mat-form-field>\n <div class=\"grid gap-2 mb-12\">\n <div class=\"muted\">{{ getLocalDataSummary() }}</div>\n @if (getLocalDataPrecedenceWarning(); as warning) {\n <div class=\"muted\">{{ warning }}</div>\n }\n @if (getLocalDataColumns().length) {\n <div class=\"muted\">\n {{ tx('Local columns') }}:\n {{ getLocalDataColumns().join(', ') }}\n </div>\n @for (row of getLocalDataPreviewRows(); track $index) {\n <div class=\"muted\">\n @for (column of getLocalDataColumns().slice(0, 6); track column) {\n <span>{{ column }}={{ row[column] }}</span>\n }\n </div>\n }\n }\n </div>\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n [attr.aria-label]=\"tx('Optional. Use valid JSON for initial filters.')\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n [attr.aria-label]=\"tx('Resource base field.')\"\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.id) {\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 $index; 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.id) {\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.value || 'default') {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n [attr.aria-label]=\"actionConditionTooltip()\"\n ></button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n @if ((a.kind || 'icon') === 'icon') {\n <span class=\"action-icon-preview\" aria-hidden=\"true\">\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </span>\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\" [disabled]=\"hasInvalidSurfaceDraft\" (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 #listSurfaceDraft\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n (validationChange)=\"onSurfaceDraftValidity(listSurfaceDraft, $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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalActionPayloadSchemaTooltip(a)\"\n [attr.aria-label]=\"globalActionPayloadSchemaTooltip(a)\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\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 [attr.aria-label]=\"tx('Advanced escape hatch. When set, payload is resolved from this expression and no default item payload is injected.')\"\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 $index;\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 type=\"button\"\n [praxisIconButton]=\"'arrow_back'\"\n size=\"dense\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move column left')\"\n (click)=\"moveRowLayoutColumn(i, -1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'arrow_forward'\"\n size=\"dense\"\n [disabled]=\"i === (working.layout.rowLayout.columns?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move column right')\"\n (click)=\"moveRowLayoutColumn(i, 1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [attr.aria-label]=\"tx('Remove column')\"\n (click)=\"removeRowLayoutColumn(i)\"\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=\"grid gap-2\">\n <div class=\"muted\">{{ getExpansionRemoteSummary() }}</div>\n @for (warning of getExpansionRemoteWarnings(); track warning) {\n <div class=\"muted\">{{ warning }}</div>\n }\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 type=\"button\"\n [praxisIconButton]=\"'arrow_upward'\"\n size=\"dense\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move section up')\"\n (click)=\"moveExpansionSection(i, -1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'arrow_downward'\"\n size=\"dense\"\n [disabled]=\"i === (working.expansion.sections?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move section down')\"\n (click)=\"moveExpansionSection(i, 1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [attr.aria-label]=\"tx('Remove section')\"\n (click)=\"removeExpansionSection(i)\"\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 <div class=\"grid gap-2 mt-12\">\n <div class=\"subtitle\">{{ tx('Export preview') }}</div>\n <div class=\"muted\">{{ getExportPreviewSummary() }}</div>\n @if (getExportPreviewColumns().length) {\n <div class=\"muted\">\n {{ tx('Columns') }}:\n {{ getExportPreviewColumns().join(', ') }}\n </div>\n @for (row of getExportPreviewRows(); track $index) {\n <div class=\"muted\">\n @for (column of getExportPreviewColumns(); track column) {\n <span>{{ column }}={{ row[column] }}</span>\n }\n </div>\n }\n } @else {\n <div class=\"muted\">\n {{ tx('No fields available for preview yet.') }}\n </div>\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 $index; 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.type) {\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.type) {\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.type) {\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.type) {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n [attr.aria-label]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\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.type) {\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\n matSuffix\n type=\"button\"\n [praxisIconButton]=\"'search'\"\n size=\"dense\"\n [matTooltip]=\"tx('Choose icon')\"\n [attr.aria-label]=\"tx('Choose icon')\"\n (click)=\"pickLeadingIcon()\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n [attr.aria-label]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\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 $index; let i = $index) {\n <div\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button\n type=\"button\"\n [praxisIconButton]=\"f.icon || 'search'\"\n size=\"dense\"\n [matTooltip]=\"tx('Choose feature icon')\"\n [attr.aria-label]=\"tx('Choose feature icon')\"\n (click)=\"pickFeatureIcon(i)\"\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\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [matTooltip]=\"tx('Remove feature')\"\n [attr.aria-label]=\"tx('Remove feature')\"\n (click)=\"removeFeature(i)\"\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.type) {\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.type) {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n aria-label=\"formControlName\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n aria-label=\"formControlPath\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n [attr.aria-label]=\"tx('Unique item key.')\"\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{min-width:0;grid-template-columns:minmax(0,1fr);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{vertical-align:middle}.editor-icon-action--danger{color:var(--md-sys-color-error)}.action-icon-preview{display:inline-grid;place-items:center;width:36px;height:36px;color:var(--md-sys-color-on-surface-variant)}.action-icon-preview mat-icon{width:18px;height:18px;font-size:18px;line-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}.preset-row{display:flex;flex-wrap:wrap}.preset-row button{white-space:nowrap}.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"] }]
|
|
15851
|
+
], template: "@if (saveError()) {\n <p role=\"alert\">{{ saveError() }}</p>\n}\n<mat-tab-group [preserveContent]=\"true\" 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 type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n [attr.aria-label]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n [attr.aria-label]=\"tx('Resource endpoint (resourcePath).')\"\n ></button>\n </mat-form-field>\n <div class=\"grid gap-2 mb-12\">\n <div class=\"muted\">{{ getLocalDataSummary() }}</div>\n @if (getLocalDataPrecedenceWarning(); as warning) {\n <div class=\"muted\">{{ warning }}</div>\n }\n @if (getLocalDataColumns().length) {\n <div class=\"muted\">\n {{ tx('Local columns') }}:\n {{ getLocalDataColumns().join(', ') }}\n </div>\n @for (row of getLocalDataPreviewRows(); track $index) {\n <div class=\"muted\">\n @for (column of getLocalDataColumns().slice(0, 6); track column) {\n <span>{{ column }}={{ row[column] }}</span>\n }\n </div>\n }\n }\n </div>\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n [attr.aria-label]=\"tx('Optional. Use valid JSON for initial filters.')\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n [attr.aria-label]=\"tx('Resource base field.')\"\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.id) {\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 $index; 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.id) {\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.value || 'default') {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n [attr.aria-label]=\"actionConditionTooltip()\"\n ></button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n @if ((a.kind || 'icon') === 'icon') {\n <span class=\"action-icon-preview\" aria-hidden=\"true\">\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </span>\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\" [disabled]=\"hasInvalidSurfaceDraft\" (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 #listSurfaceDraft\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n (validationChange)=\"onSurfaceDraftValidity(listSurfaceDraft, $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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalActionPayloadSchemaTooltip(a)\"\n [attr.aria-label]=\"globalActionPayloadSchemaTooltip(a)\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\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 [attr.aria-label]=\"tx('Advanced escape hatch. When set, payload is resolved from this expression and no default item payload is injected.')\"\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 $index;\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 type=\"button\"\n [praxisIconButton]=\"'arrow_back'\"\n size=\"dense\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move column left')\"\n (click)=\"moveRowLayoutColumn(i, -1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'arrow_forward'\"\n size=\"dense\"\n [disabled]=\"i === (working.layout.rowLayout.columns?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move column right')\"\n (click)=\"moveRowLayoutColumn(i, 1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [attr.aria-label]=\"tx('Remove column')\"\n (click)=\"removeRowLayoutColumn(i)\"\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=\"grid gap-2\">\n <div class=\"muted\">{{ getExpansionRemoteSummary() }}</div>\n @for (warning of getExpansionRemoteWarnings(); track warning) {\n <div class=\"muted\">{{ warning }}</div>\n }\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 type=\"button\"\n [praxisIconButton]=\"'arrow_upward'\"\n size=\"dense\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"tx('Move section up')\"\n (click)=\"moveExpansionSection(i, -1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'arrow_downward'\"\n size=\"dense\"\n [disabled]=\"i === (working.expansion.sections?.length || 0) - 1\"\n [attr.aria-label]=\"tx('Move section down')\"\n (click)=\"moveExpansionSection(i, 1)\"\n ></button>\n <button\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [attr.aria-label]=\"tx('Remove section')\"\n (click)=\"removeExpansionSection(i)\"\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 <div class=\"grid gap-2 mt-12\">\n <div class=\"subtitle\">{{ tx('Export preview') }}</div>\n <div class=\"muted\">{{ getExportPreviewSummary() }}</div>\n @if (getExportPreviewColumns().length) {\n <div class=\"muted\">\n {{ tx('Columns') }}:\n {{ getExportPreviewColumns().join(', ') }}\n </div>\n @for (row of getExportPreviewRows(); track $index) {\n <div class=\"muted\">\n @for (column of getExportPreviewColumns(); track column) {\n <span>{{ column }}={{ row[column] }}</span>\n }\n </div>\n }\n } @else {\n <div class=\"muted\">\n {{ tx('No fields available for preview yet.') }}\n </div>\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 $index; 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.type) {\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.type) {\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.type) {\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.type) {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n [attr.aria-label]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\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.type) {\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\n matSuffix\n type=\"button\"\n [praxisIconButton]=\"'search'\"\n size=\"dense\"\n [matTooltip]=\"tx('Choose icon')\"\n [attr.aria-label]=\"tx('Choose icon')\"\n (click)=\"pickLeadingIcon()\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n [attr.aria-label]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\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 $index; let i = $index) {\n <div\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button\n type=\"button\"\n [praxisIconButton]=\"f.icon || 'search'\"\n size=\"dense\"\n [matTooltip]=\"tx('Choose feature icon')\"\n [attr.aria-label]=\"tx('Choose feature icon')\"\n (click)=\"pickFeatureIcon(i)\"\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\n type=\"button\"\n [praxisIconButton]=\"'delete'\"\n size=\"dense\"\n class=\"editor-icon-action--danger\"\n [matTooltip]=\"tx('Remove feature')\"\n [attr.aria-label]=\"tx('Remove feature')\"\n (click)=\"removeFeature(i)\"\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.type) {\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.type) {\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n aria-label=\"formControlName\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n aria-label=\"formControlPath\"\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 matSuffix\n type=\"button\"\n [praxisIconButton]=\"'help_outline'\"\n size=\"dense\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n [attr.aria-label]=\"tx('Unique item key.')\"\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{min-width:0;grid-template-columns:minmax(0,1fr);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{vertical-align:middle}.editor-icon-action--danger{color:var(--md-sys-color-error)}.action-icon-preview{display:inline-grid;place-items:center;width:36px;height:36px;color:var(--md-sys-color-on-surface-variant)}.action-icon-preview mat-icon{width:18px;height:18px;font-size:18px;line-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}.preset-row{display:flex;flex-wrap:wrap}.preset-row button{white-space:nowrap}.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"] }]
|
|
15799
15852
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
15800
15853
|
type: Optional
|
|
15801
15854
|
}, {
|