@praxisui/list 8.0.0-beta.1 → 8.0.0-beta.11
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 +22 -6
- package/fesm2022/praxisui-list.mjs +2337 -145
- package/index.d.ts +35 -23
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Injectable, Input, ChangeDetectionStrategy, Component, LOCALE_ID, CSP_NONCE, EventEmitter, Output, Optional, Inject, ChangeDetectorRef, isDevMode, booleanAttribute, ENVIRONMENT_INITIALIZER, signal, computed } from '@angular/core';
|
|
2
|
+
import { inject, Injectable, Input, ChangeDetectionStrategy, Component, LOCALE_ID, CSP_NONCE, EventEmitter, Output, Optional, SkipSelf, Inject, ChangeDetectorRef, isDevMode, booleanAttribute, ENVIRONMENT_INITIALIZER, signal, computed } from '@angular/core';
|
|
3
3
|
import { ActivatedRoute } from '@angular/router';
|
|
4
4
|
import * as i1 from '@angular/common';
|
|
5
5
|
import { formatDate, CommonModule } from '@angular/common';
|
|
@@ -7,7 +7,7 @@ import * as i3 from '@angular/material/list';
|
|
|
7
7
|
import { MatListModule } from '@angular/material/list';
|
|
8
8
|
import * as i4 from '@angular/material/icon';
|
|
9
9
|
import { MatIconModule } from '@angular/material/icon';
|
|
10
|
-
import { resolveValuePresentation, LoggerService, GenericCrudService, toTitleCase, PraxisIconDirective, providePraxisI18n, PraxisI18nService, IconPickerService, GLOBAL_ACTION_CATALOG, PRAXIS_GLOBAL_ACTION_CATALOG, getGlobalActionCatalog, SURFACE_OPEN_I18N_NAMESPACE, getGlobalActionUiSchema, SurfaceOpenActionEditorComponent, providePraxisI18nConfig, SURFACE_OPEN_I18N_CONFIG, deepMerge, ASYNC_CONFIG_STORAGE, ComponentKeyService, PraxisJsonLogicService, GlobalActionService, GLOBAL_DIALOG_SERVICE, ComponentMetadataRegistry, API_URL, LocalStorageAsyncAdapter, LocalStorageConfigService } from '@praxisui/core';
|
|
10
|
+
import { resolveValuePresentation, LoggerService, GenericCrudService, toTitleCase, PraxisIconDirective, providePraxisI18n, PraxisI18nService, IconPickerService, GLOBAL_ACTION_CATALOG, PRAXIS_GLOBAL_ACTION_CATALOG, getGlobalActionCatalog, SURFACE_OPEN_I18N_NAMESPACE, getGlobalActionUiSchema, SurfaceOpenActionEditorComponent, PRAXIS_I18N_CONFIG, providePraxisI18nConfig, SURFACE_OPEN_I18N_CONFIG, deepMerge, ASYNC_CONFIG_STORAGE, ComponentKeyService, PraxisJsonLogicService, GlobalActionService, GLOBAL_DIALOG_SERVICE, ComponentMetadataRegistry, API_URL, LocalStorageAsyncAdapter, LocalStorageConfigService } from '@praxisui/core';
|
|
11
11
|
import * as i5 from '@angular/material/chips';
|
|
12
12
|
import { MatChipsModule } from '@angular/material/chips';
|
|
13
13
|
import { MatDividerModule } from '@angular/material/divider';
|
|
@@ -469,10 +469,9 @@ class ListDataService {
|
|
|
469
469
|
}
|
|
470
470
|
this.loading$.next(true);
|
|
471
471
|
const obs = this.crud.filter(query || {}, req).pipe(map((page) => {
|
|
472
|
-
const
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
: content.length;
|
|
472
|
+
const resolvedPage = unwrapApiEnvelope(page);
|
|
473
|
+
const content = extractPageContent(resolvedPage);
|
|
474
|
+
const total = extractPageTotal(resolvedPage, content.length);
|
|
476
475
|
this.total$.next(total);
|
|
477
476
|
return content;
|
|
478
477
|
}),
|
|
@@ -480,7 +479,7 @@ class ListDataService {
|
|
|
480
479
|
catchError((err) => {
|
|
481
480
|
this.warnFilterFallbackOnce(resourcePath, err);
|
|
482
481
|
return this.crud.getAll().pipe(map((arr) => {
|
|
483
|
-
const content = arr
|
|
482
|
+
const content = extractPageContent(unwrapApiEnvelope(arr));
|
|
484
483
|
this.total$.next(content.length);
|
|
485
484
|
return content;
|
|
486
485
|
}));
|
|
@@ -601,6 +600,16 @@ class ListDataService {
|
|
|
601
600
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ListDataService, decorators: [{
|
|
602
601
|
type: Injectable
|
|
603
602
|
}] });
|
|
603
|
+
function unwrapApiEnvelope(page) {
|
|
604
|
+
return page?.data ?? page;
|
|
605
|
+
}
|
|
606
|
+
function extractPageContent(page) {
|
|
607
|
+
const content = page?.content ?? page ?? [];
|
|
608
|
+
return Array.isArray(content) ? content : [];
|
|
609
|
+
}
|
|
610
|
+
function extractPageTotal(page, fallback) {
|
|
611
|
+
return typeof page?.totalElements === 'number' ? page.totalElements : fallback;
|
|
612
|
+
}
|
|
604
613
|
function stableSerialize$1(value) {
|
|
605
614
|
if (value === null || value === undefined)
|
|
606
615
|
return String(value);
|
|
@@ -2037,21 +2046,22 @@ function normalizeListActionPayloads(actions) {
|
|
|
2037
2046
|
if (!action || typeof action !== 'object')
|
|
2038
2047
|
return action;
|
|
2039
2048
|
const next = cloneJson(action);
|
|
2040
|
-
|
|
2041
|
-
|
|
2049
|
+
const globalAction = next.globalAction;
|
|
2050
|
+
if (globalAction && typeof globalAction.payload === 'string') {
|
|
2051
|
+
const trimmed = globalAction.payload.trim();
|
|
2042
2052
|
if (!trimmed) {
|
|
2043
|
-
|
|
2053
|
+
globalAction.payload = undefined;
|
|
2044
2054
|
}
|
|
2045
2055
|
else if (looksLikeJsonPayload(trimmed)) {
|
|
2046
2056
|
try {
|
|
2047
|
-
|
|
2057
|
+
globalAction.payload = JSON.parse(trimmed);
|
|
2048
2058
|
}
|
|
2049
2059
|
catch {
|
|
2050
|
-
|
|
2060
|
+
globalAction.payload = trimmed;
|
|
2051
2061
|
}
|
|
2052
2062
|
}
|
|
2053
2063
|
else {
|
|
2054
|
-
|
|
2064
|
+
globalAction.payload = trimmed;
|
|
2055
2065
|
}
|
|
2056
2066
|
}
|
|
2057
2067
|
next.placement = oneOf(next.placement, ACTION_PLACEMENTS, undefined);
|
|
@@ -2978,10 +2988,7 @@ function readBoolean(value) {
|
|
|
2978
2988
|
return undefined;
|
|
2979
2989
|
}
|
|
2980
2990
|
function looksLikeJsonPayload(value) {
|
|
2981
|
-
return
|
|
2982
|
-
value.startsWith('[') ||
|
|
2983
|
-
value.endsWith('}') ||
|
|
2984
|
-
value.endsWith(']'));
|
|
2991
|
+
return value.startsWith('{') || value.startsWith('[');
|
|
2985
2992
|
}
|
|
2986
2993
|
function isAcceptedImageUrl(value) {
|
|
2987
2994
|
return ((value.startsWith('${') && value.endsWith('}')) ||
|
|
@@ -3366,7 +3373,7 @@ const PRAXIS_LIST_EN_US = {
|
|
|
3366
3373
|
'Global action (Praxis)': 'Global action (Praxis)',
|
|
3367
3374
|
'-- Select --': '-- Select --',
|
|
3368
3375
|
'No global action registered.': 'No global action registered.',
|
|
3369
|
-
'Select to add with a global
|
|
3376
|
+
'Select to add with a structured global action.': 'Select to add with a structured global action.',
|
|
3370
3377
|
'Action type': 'Action type',
|
|
3371
3378
|
Icon: 'Icon',
|
|
3372
3379
|
Button: 'Button',
|
|
@@ -3400,7 +3407,7 @@ const PRAXIS_LIST_EN_US = {
|
|
|
3400
3407
|
'Emit local event too': 'Emit local event too',
|
|
3401
3408
|
JSON: 'JSON',
|
|
3402
3409
|
ID: 'ID',
|
|
3403
|
-
'
|
|
3410
|
+
'Global action': 'Global action',
|
|
3404
3411
|
Catalog: 'Catalog',
|
|
3405
3412
|
Danger: 'Danger',
|
|
3406
3413
|
Warning: 'Warning',
|
|
@@ -3591,7 +3598,7 @@ const PRAXIS_LIST_PT_BR = {
|
|
|
3591
3598
|
'Global action (Praxis)': 'Ação global (Praxis)',
|
|
3592
3599
|
'-- Select --': '-- Selecionar --',
|
|
3593
3600
|
'No global action registered.': 'Nenhuma ação global registrada.',
|
|
3594
|
-
'Select to add with a global
|
|
3601
|
+
'Select to add with a structured global action.': 'Selecione para adicionar com uma ação global estruturada.',
|
|
3595
3602
|
'Action type': 'Tipo de ação',
|
|
3596
3603
|
Icon: 'Ícone',
|
|
3597
3604
|
Button: 'Botão',
|
|
@@ -3625,7 +3632,7 @@ const PRAXIS_LIST_PT_BR = {
|
|
|
3625
3632
|
'Emit local event too': 'Emitir evento local também',
|
|
3626
3633
|
JSON: 'JSON',
|
|
3627
3634
|
ID: 'ID',
|
|
3628
|
-
'
|
|
3635
|
+
'Global action': 'Ação global',
|
|
3629
3636
|
Catalog: 'Catálogo',
|
|
3630
3637
|
Danger: 'Perigo',
|
|
3631
3638
|
Warning: 'Aviso',
|
|
@@ -4502,45 +4509,31 @@ class PraxisListConfigEditor {
|
|
|
4502
4509
|
}
|
|
4503
4510
|
this.onActionsChanged();
|
|
4504
4511
|
}
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
const trimmed = payload.trim();
|
|
4509
|
-
if (!trimmed)
|
|
4510
|
-
return false;
|
|
4511
|
-
// Only validate if it looks like JSON object/array
|
|
4512
|
-
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
4513
|
-
try {
|
|
4514
|
-
JSON.parse(trimmed);
|
|
4515
|
-
return false;
|
|
4516
|
-
}
|
|
4517
|
-
catch {
|
|
4518
|
-
return true;
|
|
4519
|
-
}
|
|
4520
|
-
}
|
|
4521
|
-
return false;
|
|
4522
|
-
}
|
|
4523
|
-
applyGlobalPayloadExample(action) {
|
|
4524
|
-
if (!action?.command)
|
|
4512
|
+
applyGlobalActionPayloadExample(action) {
|
|
4513
|
+
const cmd = this.getGlobalActionId(action);
|
|
4514
|
+
if (!cmd)
|
|
4525
4515
|
return;
|
|
4526
|
-
const cmd = action.command.replace(/^global[:.]/, '').trim();
|
|
4527
4516
|
const entry = this.globalActionCatalog.find((e) => e.id === cmd);
|
|
4528
4517
|
if (entry?.payloadSchema?.example) {
|
|
4529
|
-
action.
|
|
4518
|
+
action.globalAction = {
|
|
4519
|
+
...(action.globalAction || {}),
|
|
4520
|
+
actionId: cmd,
|
|
4521
|
+
payload: entry.payloadSchema.example,
|
|
4522
|
+
};
|
|
4530
4523
|
this.onActionsChanged();
|
|
4531
4524
|
}
|
|
4532
4525
|
}
|
|
4533
|
-
|
|
4534
|
-
|
|
4526
|
+
globalActionPayloadExampleHint(action) {
|
|
4527
|
+
const cmd = this.getGlobalActionId(action);
|
|
4528
|
+
if (!cmd)
|
|
4535
4529
|
return '';
|
|
4536
|
-
const cmd = action.command.replace(/^global[:.]/, '').trim();
|
|
4537
4530
|
const entry = this.globalActionCatalog.find((e) => e.id === cmd);
|
|
4538
4531
|
return entry?.payloadSchema?.example ? this.tx('Example available') : '';
|
|
4539
4532
|
}
|
|
4540
|
-
|
|
4541
|
-
|
|
4533
|
+
globalActionPayloadSchemaTooltip(action) {
|
|
4534
|
+
const cmd = this.getGlobalActionId(action);
|
|
4535
|
+
if (!cmd)
|
|
4542
4536
|
return this.tx('No schema available.');
|
|
4543
|
-
const cmd = action.command.replace(/^global[:.]/, '').trim();
|
|
4544
4537
|
const entry = this.globalActionCatalog.find((e) => e.id === cmd);
|
|
4545
4538
|
const schema = entry?.payloadSchema;
|
|
4546
4539
|
if (!schema)
|
|
@@ -4570,27 +4563,79 @@ class PraxisListConfigEditor {
|
|
|
4570
4563
|
}
|
|
4571
4564
|
return lines.join('\n');
|
|
4572
4565
|
}
|
|
4566
|
+
onActionGlobalActionIdChange(action, actionId) {
|
|
4567
|
+
const id = String(actionId || '').trim();
|
|
4568
|
+
if (!id) {
|
|
4569
|
+
delete action.globalAction;
|
|
4570
|
+
this.onActionsChanged();
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
action.globalAction = {
|
|
4574
|
+
...(action.globalAction || {}),
|
|
4575
|
+
actionId: id,
|
|
4576
|
+
};
|
|
4577
|
+
this.onActionsChanged();
|
|
4578
|
+
}
|
|
4579
|
+
getGlobalActionPayloadText(action) {
|
|
4580
|
+
const payload = action?.globalAction?.payload;
|
|
4581
|
+
if (payload === undefined || payload === null)
|
|
4582
|
+
return '';
|
|
4583
|
+
if (typeof payload === 'string')
|
|
4584
|
+
return payload;
|
|
4585
|
+
try {
|
|
4586
|
+
return JSON.stringify(payload, null, 2);
|
|
4587
|
+
}
|
|
4588
|
+
catch {
|
|
4589
|
+
return '';
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
onGlobalActionPayloadTextChange(action, value) {
|
|
4593
|
+
const text = String(value || '').trim();
|
|
4594
|
+
if (!action.globalAction?.actionId)
|
|
4595
|
+
return;
|
|
4596
|
+
if (!text) {
|
|
4597
|
+
action.globalAction = { ...action.globalAction, payload: undefined };
|
|
4598
|
+
this.onActionsChanged();
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4601
|
+
action.globalAction = {
|
|
4602
|
+
...action.globalAction,
|
|
4603
|
+
payload: this.parseGlobalActionPayloadText(text),
|
|
4604
|
+
};
|
|
4605
|
+
this.onActionsChanged();
|
|
4606
|
+
}
|
|
4607
|
+
isGlobalActionPayloadInvalid(action) {
|
|
4608
|
+
const payload = action?.globalAction?.payload;
|
|
4609
|
+
if (typeof payload !== 'string')
|
|
4610
|
+
return false;
|
|
4611
|
+
const trimmed = payload.trim();
|
|
4612
|
+
if (!trimmed)
|
|
4613
|
+
return false;
|
|
4614
|
+
if (!this.looksLikeJsonPayload(trimmed))
|
|
4615
|
+
return false;
|
|
4616
|
+
try {
|
|
4617
|
+
JSON.parse(trimmed);
|
|
4618
|
+
return false;
|
|
4619
|
+
}
|
|
4620
|
+
catch {
|
|
4621
|
+
return true;
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4573
4624
|
isSurfaceOpenCommand(action) {
|
|
4574
4625
|
return this.getGlobalActionSchema(action)?.editorMode === 'surface-open';
|
|
4575
4626
|
}
|
|
4576
|
-
|
|
4577
|
-
const
|
|
4578
|
-
if (
|
|
4579
|
-
|
|
4580
|
-
const parsed = JSON.parse(raw);
|
|
4581
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
4582
|
-
return this.normalizeSurfaceOpenPayload(parsed);
|
|
4583
|
-
}
|
|
4584
|
-
}
|
|
4585
|
-
catch {
|
|
4586
|
-
// keep default payload while authoring invalid JSON elsewhere
|
|
4587
|
-
}
|
|
4627
|
+
getSurfaceOpenGlobalActionPayload(action) {
|
|
4628
|
+
const payload = action?.globalAction?.payload;
|
|
4629
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
4630
|
+
return this.normalizeSurfaceOpenPayload(payload);
|
|
4588
4631
|
}
|
|
4589
4632
|
return this.normalizeSurfaceOpenPayload(undefined);
|
|
4590
4633
|
}
|
|
4591
|
-
|
|
4592
|
-
action.
|
|
4593
|
-
|
|
4634
|
+
onSurfaceOpenGlobalActionPayloadChange(action, payload) {
|
|
4635
|
+
action.globalAction = {
|
|
4636
|
+
actionId: 'surface.open',
|
|
4637
|
+
payload: this.normalizeSurfaceOpenPayload(payload),
|
|
4638
|
+
};
|
|
4594
4639
|
this.onActionsChanged();
|
|
4595
4640
|
}
|
|
4596
4641
|
onGlobalActionSelected(id) {
|
|
@@ -4602,9 +4647,6 @@ class PraxisListConfigEditor {
|
|
|
4602
4647
|
}
|
|
4603
4648
|
addGlobalActionFromCatalog(action) {
|
|
4604
4649
|
const payload = action.payloadSchema?.example;
|
|
4605
|
-
const payloadText = payload != null && typeof payload === 'object'
|
|
4606
|
-
? JSON.stringify(payload, null, 2)
|
|
4607
|
-
: payload;
|
|
4608
4650
|
this.working = produce(this.working, (draft) => {
|
|
4609
4651
|
draft.actions = draft.actions || [];
|
|
4610
4652
|
draft.actions.push({
|
|
@@ -4613,8 +4655,9 @@ class PraxisListConfigEditor {
|
|
|
4613
4655
|
label: action.label || action.id,
|
|
4614
4656
|
color: undefined,
|
|
4615
4657
|
kind: 'icon',
|
|
4616
|
-
|
|
4617
|
-
|
|
4658
|
+
globalAction: payload !== undefined
|
|
4659
|
+
? { actionId: action.id, payload }
|
|
4660
|
+
: { actionId: action.id },
|
|
4618
4661
|
});
|
|
4619
4662
|
});
|
|
4620
4663
|
this.syncActionShowIfDrafts();
|
|
@@ -5679,8 +5722,25 @@ class PraxisListConfigEditor {
|
|
|
5679
5722
|
return entry?.description || '';
|
|
5680
5723
|
}
|
|
5681
5724
|
getGlobalActionSchema(action) {
|
|
5682
|
-
|
|
5683
|
-
|
|
5725
|
+
return getGlobalActionUiSchema(this.getGlobalActionId(action));
|
|
5726
|
+
}
|
|
5727
|
+
getGlobalActionId(action) {
|
|
5728
|
+
return String(action?.globalAction?.actionId || '').trim();
|
|
5729
|
+
}
|
|
5730
|
+
parseGlobalActionPayloadText(text) {
|
|
5731
|
+
if (!this.looksLikeJsonPayload(text))
|
|
5732
|
+
return text;
|
|
5733
|
+
try {
|
|
5734
|
+
return JSON.parse(text);
|
|
5735
|
+
}
|
|
5736
|
+
catch {
|
|
5737
|
+
return text;
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
looksLikeJsonPayload(text) {
|
|
5741
|
+
const value = String(text || '').trim();
|
|
5742
|
+
return ((value.startsWith('{') && value.endsWith('}')) ||
|
|
5743
|
+
(value.startsWith('[') && value.endsWith(']')));
|
|
5684
5744
|
}
|
|
5685
5745
|
normalizeSurfaceOpenPayload(payload) {
|
|
5686
5746
|
return {
|
|
@@ -5878,13 +5938,31 @@ class PraxisListConfigEditor {
|
|
|
5878
5938
|
}
|
|
5879
5939
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisListConfigEditor, deps: [{ token: SETTINGS_PANEL_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
5880
5940
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisListConfigEditor, isStandalone: true, selector: "praxis-list-config-editor", inputs: { config: "config", listId: "listId" }, providers: [
|
|
5941
|
+
{
|
|
5942
|
+
provide: PRAXIS_I18N_CONFIG,
|
|
5943
|
+
multi: true,
|
|
5944
|
+
deps: [[new Optional(), new SkipSelf(), PraxisI18nService]],
|
|
5945
|
+
useFactory: (parent) => ({
|
|
5946
|
+
locale: parent?.getLocale(),
|
|
5947
|
+
fallbackLocale: parent?.getFallbackLocale(),
|
|
5948
|
+
}),
|
|
5949
|
+
},
|
|
5881
5950
|
providePraxisI18nConfig(SURFACE_OPEN_I18N_CONFIG),
|
|
5882
5951
|
providePraxisListI18n(),
|
|
5883
|
-
], ngImport: i0, template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <ng-template matTabContent>\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">\n {{ tx('Assistant-applied adjustments replace the entire configuration object.') }}\n </div>\n <button\n mat-icon-button\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Resource (API)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.dataSource.resourcePath\"\n (ngModelChange)=\"onResourcePathChange($event)\"\n [placeholder]=\"tx('e.g.: users')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Query (JSON)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [(ngModel)]=\"queryJson\"\n (ngModelChange)=\"onQueryChanged($event)\"\n [placeholder]=\"queryPlaceholder()\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n *ngIf=\"!queryError\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"queryError\">{{ queryError }}</mat-error>\n </mat-form-field>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Sort by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortField\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortDir\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </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 <mat-option *ngFor=\"let ga of globalActionCatalog\" [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || \"bolt\" }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n </mat-select>\n <mat-hint\n *ngIf=\"!globalActionCatalog.length\"\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a global `command`.') }}\n </div>\n </div>\n <div\n *ngFor=\"let a of working.actions || []; let i = index\"\n class=\"g g-auto-200 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('ID') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.id\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action type') }}</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">{{ tx('Icon') }}</mat-option>\n <mat-option value=\"button\">{{ tx('Button') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.icon\"\n (ngModelChange)=\"onActionsChanged()\"\n [placeholder]=\"tx('e.g.: edit, delete')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Command (global)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.command\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder=\"global:toast.success\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.label\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Variant') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.buttonVariant\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option value=\"stroked\">{{ tx('Outlined') }}</mat-option>\n <mat-option value=\"raised\">{{ tx('Raised') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Filled') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action color') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span\n class=\"color-dot\"\n [style.background]=\"colorDotBackground(c.value)\"\n ></span\n >{{ tx(c.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g gap-8\"\n *ngIf=\"isCustomColor(a.color); else actionCustomBtn\"\n >\n <pdx-color-picker\n [label]=\"tx('Custom color')\"\n [format]=\"'hex'\"\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n ></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action payload') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.emitPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Payload emitted by the action.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ actionVisibilityLabel() }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"actionShowIfModel(i)\"\n (ngModelChange)=\"onActionShowIfChanged(i, a, $event)\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n *ngIf=\"(a.kind || 'icon') === 'icon'\"\n mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n >\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button\n *ngIf=\"a.buttonVariant === 'stroked'\"\n mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'stroked')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"a.buttonVariant === 'raised'\"\n mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'raised')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\"\n mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'flat')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n </ng-container>\n <span class=\"muted\">{{ tx('Preview') }}</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle\n [(ngModel)]=\"a.showLoading\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Show loading') }}</mat-slide-toggle\n >\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >{{ tx('Confirmation') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">{{ tx('Type') }}</span>\n <mat-button-toggle-group\n [value]=\"a.confirmation?.type || ''\"\n (change)=\"applyConfirmationPreset(a, $event.value)\"\n >\n <mat-button-toggle value=\"\">{{ tx('Default') }}</mat-button-toggle>\n <mat-button-toggle value=\"danger\">{{ tx('Danger') }}</mat-button-toggle>\n <mat-button-toggle value=\"warning\">{{ tx('Warning') }}</mat-button-toggle>\n <mat-button-toggle value=\"info\">{{ tx('Info') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.title\"\n (ngModelChange)=\"setConfirmationField(a, 'title', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.message\"\n (ngModelChange)=\"setConfirmationField(a, 'message', $event)\"\n />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">{{ tx('Preview') }}</div>\n <div class=\"text-caption\">\n <strong>{{\n a.confirmation?.title || tx('Confirm action')\n }}</strong>\n </div>\n <div class=\"text-caption muted\">\n {{\n a.confirmation?.message ||\n tx('Are you sure you want to continue?')\n }}\n </div>\n <div class=\"text-caption\">\n <span\n class=\"confirm-type\"\n [ngClass]=\"a.confirmation?.type || 'default'\"\n >{{ tx('Type') }}:\n {{ a.confirmation?.type || tx('Default').toLowerCase() }}</span\n >\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <ng-container *ngIf=\"isSurfaceOpenCommand(a); else defaultGlobalPayloadEditor\">\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n </ng-container>\n <ng-template #defaultGlobalPayloadEditor>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload (JSON/Template)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"a.globalPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\"\n >{{ tx('Invalid JSON') }}</mat-error\n >\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalPayloadExampleHint(a)\n }}</span>\n </div>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n </div>\n </div>\n </div>\n </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 <ng-container\n *ngIf=\"working.layout.variant === 'list'; else cardModels\"\n >\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Media on the left') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel (large media)') }}</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container\n *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\"\n >\n <mat-option value=\"standard\">{{ tx('Standard tile') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Tile with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel tile') }}</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Card with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel') }}</mat-option>\n </ng-template>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Lines') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.lines\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"1\">1</mat-option>\n <mat-option [value]=\"2\">2</mat-option>\n <mat-option [value]=\"3\">3</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Items per page') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Density') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.density\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"default\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"comfortable\">{{ tx('Comfortable') }}</mat-option>\n <mat-option value=\"compact\">{{ tx('Compact') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Spacing between items') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.itemSpacing\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No extra space') }}</mat-option>\n <mat-option value=\"tight\">{{ tx('Tight') }}</mat-option>\n <mat-option value=\"default\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"relaxed\">{{ tx('Relaxed') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"working.layout.variant !== 'tiles'\"\n >\n <mat-label>{{ tx('Dividers') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.dividers\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('None') }}</mat-option>\n <mat-option value=\"between\">{{ tx('Between groups') }}</mat-option>\n <mat-option value=\"all\">{{ tx('All') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <ng-container *ngIf=\"fields.length > 0; else groupByText\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Group by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"\">{{ tx('None') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <ng-template #groupByText>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Group by') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n [placeholder]=\"tx('e.g.: department')\"\n />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.stickySectionHeader\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Sticky section header') }}\n </mat-slide-toggle>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.virtualScroll\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Virtual scroll') }}\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('List tools') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSearch\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show search') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSort\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show sorting') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showRange\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show total X-Y range') }}</mat-slide-toggle\n >\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngIf=\"working.ui?.showSearch\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field to search') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.ui.searchField\"\n (ngModelChange)=\"onUiChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Search placeholder') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.ui.searchPlaceholder\"\n (ngModelChange)=\"onUiChanged()\"\n [placeholder]=\"tx('e.g.: Search by title')\"\n />\n </mat-form-field>\n </div>\n <div class=\"mt-12\" *ngIf=\"working.ui?.showSort\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Sorting options (label \u2192 field+direction)') }}\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">\n {{ tx('Add option') }}\n </button>\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngFor=\"let r of uiSortRows; let i = index\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"r.label\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n [placeholder]=\"tx('e.g.: Most recent')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.field\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.dir\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n </div>\n </div>\n </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 <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of primaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Secondary -->\n <mat-expansion-panel [expanded]=\"!!mappingSecondary.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSecondary.type) }}</mat-icon>\n <span>{{ tx('Secondary (Summary)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSecondary.field || tx('Not mapped')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'subtitle';\n onMappingChanged()\n \"\n >\n {{ tx('Subtitle') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'date';\n mappingSecondary.field = 'hireDate';\n mappingSecondary.dateStyle = 'short';\n onMappingChanged()\n \"\n >\n {{ tx('Short date') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applySalaryPreset()\"\n >\n {{ tx('Salary') }}\n </button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of secondaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('CSS class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Inline style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel\n [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingMeta.type || \"text\")\n }}</mat-icon>\n <span>{{ tx('Meta (Detail/Side)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingMetaFields.length\n ? tx('Composed field ({{count}})', { count: mappingMetaFields.length })\n : mappingMeta.field || tx('Not mapped')\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <!-- Composition Mode Toggle -->\n <div class=\"g g-1-1 gap-12 p-12 bg-subtle rounded\">\n <div class=\"text-caption muted\">{{ tx('Composition mode') }}</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Fields to compose (multi-select)') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMetaFields\"\n multiple\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g g-1-1 ai-center gap-12\"\n *ngIf=\"mappingMetaFields.length\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Separator') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMetaSeparator\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"mappingMetaWrapSecondInParens\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n {{ tx('(Second) in parentheses') }}\n </mat-slide-toggle>\n </div>\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"!mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Single field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- None --') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of metaTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Type configuration (pluggable editors) -->\n @switch (mappingMeta.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of trailingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'chip';\n mappingTrailing.chipColor = 'primary';\n mappingTrailing.chipVariant = 'filled';\n mappingTrailing.field = 'status';\n onMappingChanged()\n \"\n >\n {{ tx('Status chip') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'icon';\n mappingTrailing.field = 'status';\n mappingTrailing.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Status icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyPricePreset()\"\n >\n {{ tx('Price') }}\n </button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-chip>\n }\n @case (\"rating\") {\n <praxis-meta-editor-rating\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-rating>\n }\n @case (\"icon\") {\n <praxis-meta-editor-icon\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-icon>\n }\n @case (\"image\") {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('URL / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingTrailing.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingTrailing.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Leading -->\n <mat-expansion-panel\n [expanded]=\"\n !!mappingLeading.field ||\n (mappingLeading.type === 'icon' && !!mappingLeading.icon) ||\n (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\n \"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingLeading.type) }}</mat-icon>\n <span>{{ tx('Leading (left)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingLeading.type === \"icon\"\n ? mappingLeading.icon || tx('Static icon')\n : mappingLeading.field ||\n (mappingLeading.imageUrl\n ? tx('Static image')\n : tx('Not mapped'))\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of leadingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n \"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'icon';\n mappingLeading.icon = 'person';\n mappingLeading.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'image';\n mappingLeading.imageUrl = 'https://placehold.co/64x64';\n mappingLeading.imageAlt = 'Avatar';\n mappingLeading.badgeText = '${item.status}';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar image + badge') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'chip';\n mappingLeading.field = 'tag';\n mappingLeading.chipColor = 'accent';\n mappingLeading.chipVariant = 'filled';\n onMappingChanged()\n \"\n >\n {{ tx('Tag chip') }}\n </button>\n </div>\n\n <!-- Icon Specific -->\n <div\n class=\"g g-1-auto gap-12 ai-center\"\n *ngIf=\"mappingLeading.type === 'icon'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.icon\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\">\n <mat-icon>search</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"text-caption muted\">\n {{ tx('Use the `|iconMap` pipe in the extra pipe for dynamic rendering.') }}\n </div>\n </div>\n <div *ngIf=\"mappingLeading.type === 'icon'\">\n <praxis-meta-editor-icon\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div\n class=\"g g-1-1 gap-12\"\n *ngIf=\"mappingLeading.type === 'image'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Image URL') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Alt text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageAlt\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Badge text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.badgeText\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <div\n *ngFor=\"let f of features; let i = index\"\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\">\n <mat-icon>{{ f.icon || \"search\" }}</mat-icon>\n </button>\n <mat-form-field\n appearance=\"outline\"\n class=\"dense-form-field no-sub\"\n >\n <input\n matInput\n [(ngModel)]=\"f.expr\"\n (ngModelChange)=\"onFeaturesChanged()\"\n [placeholder]=\"tx('Expression/Text')\"\n />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\">\n <mat-icon>add</mat-icon> {{ tx('Add feature') }}\n </button>\n </div>\n </mat-expansion-panel>\n <!-- Section Header -->\n <mat-expansion-panel [expanded]=\"!!mappingSectionHeader.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingSectionHeader.type)\n }}</mat-icon>\n <span>{{ tx('Section header') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSectionHeader.expr || tx('Not configured')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSectionHeader.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of sectionHeaderTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Expression (item.key)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingSectionHeader.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'text';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default text') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'chip';\n mappingSectionHeader.chipColor = 'primary';\n mappingSectionHeader.chipVariant = 'filled';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default chip') }}\n </button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingSectionHeader.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingSectionHeader\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Empty State -->\n <mat-expansion-panel [expanded]=\"!!mappingEmptyState.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>inbox</mat-icon>\n <span>{{ tx('Empty state') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingEmptyState.expr || tx('Default')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingEmptyState.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of emptyStateTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingEmptyState.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'text';\n mappingEmptyState.expr = tx('No items available');\n onMappingChanged()\n \"\n >\n {{ tx('Default message') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'image';\n mappingEmptyState.imageUrl = '/list-empty-state.svg';\n mappingEmptyState.imageAlt = tx('No results');\n onMappingChanged()\n \"\n >\n {{ tx('Default image') }}\n </button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingEmptyState.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingEmptyState\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n </mat-accordion>\n\n <button mat-flat-button color=\"primary\" (click)=\"applyTemplate()\">\n {{ tx('Apply mapping') }}\n </button>\n <button\n mat-button\n (click)=\"inferFromFields()\"\n [disabled]=\"!fields.length\"\n >\n {{ tx('Infer from schema') }}\n </button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Skeleton count') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"0\"\n [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\"\n />\n </mat-form-field>\n </div>\n\n <div class=\"g gap-12 mt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"section-title mat-subtitle-1\">{{ tx('Theme preview') }}</span>\n <mat-button-toggle-group\n [(ngModel)]=\"skinPreviewTheme\"\n (change)=\"onSkinChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle [value]=\"'light'\">{{ tx('Light') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">{{ tx('Dark') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">{{ tx('Grid') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview\n [config]=\"working\"\n [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"\n ></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <ng-template matTabContent>\n <div\n class=\"editor-content grid gap-3\"\n *ngIf=\"working?.a11y && working?.events\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default locale') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.locale\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"tx('e.g.: en-US')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default currency') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.currency\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"currencyPlaceholder()\"\n />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Accessibility') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabel\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-labelledby') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabelledBy\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.highContrast\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('High contrast') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.reduceMotion\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('Reduce motion') }}</mat-slide-toggle\n >\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Events') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('itemClick') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.itemClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.actionClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.selectionChange\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.loaded\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.mode\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No selection') }}</mat-option>\n <mat-option value=\"single\">{{ tx('Single') }}</mat-option>\n <mat-option value=\"multiple\">{{ tx('Multiple') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form name') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlName\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form path') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlPath\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Compare by (field)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.compareBy\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Return') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.return\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-button (click)=\"applySkinPreset('pill-soft')\">\n {{ tx('Pill Soft') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">\n {{ tx('Gradient Tile') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('glass')\">\n {{ tx('Glass') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">\n {{ tx('Elevated') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('outline')\">\n {{ tx('Outline') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('flat')\">\n {{ tx('Flat') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">\n {{ tx('Neumorphism') }}\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Style') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.skin.type\"\n (ngModelChange)=\"onSkinTypeChanged($event)\"\n >\n <mat-option value=\"pill-soft\">{{ tx('Pill Soft') }}</mat-option>\n <mat-option value=\"gradient-tile\">{{ tx('Gradient Tile') }}</mat-option>\n <mat-option value=\"glass\">{{ tx('Glass') }}</mat-option>\n <mat-option value=\"elevated\">{{ tx('Elevated') }}</mat-option>\n <mat-option value=\"outline\">{{ tx('Outline') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Flat') }}</mat-option>\n <mat-option value=\"neumorphism\">{{ tx('Neumorphism') }}</mat-option>\n <mat-option value=\"custom\">{{ tx('Custom') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Radius') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.radius\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 1.25rem')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shadow') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.shadow\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: var(--md-sys-elevation-level2)')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.border\"\n (ngModelChange)=\"onSkinChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n *ngIf=\"working.skin.type === 'glass'\"\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Blur') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.backdropBlur\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 8px')\"\n />\n </mat-form-field>\n <div *ngIf=\"working.skin.type === 'gradient-tile'\" class=\"g gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient from') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient to') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Angle') }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\"\n />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Extra CSS class (skin.class)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.class\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: my-list-skin')\"\n />\n </mat-form-field>\n\n <div\n *ngIf=\"working.skin.type === 'custom'\"\n class=\"g g-auto-220 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Inline style (skin.inlineStyle)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"working.skin.inlineStyle\"\n (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"\n ></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n {{ tx('CSS class example (add this to your global styles):') }}\n <pre class=\"code-block\">\n.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre\n >\n </div>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n</mat-tab-group>\n\n", styles: [".confirm-type{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;line-height:16px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.confirm-type.danger{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.confirm-type.warning{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.confirm-type.info{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}:host{display:block;color:var(--md-sys-color-on-surface)}.list-editor-tabs{--editor-surface: var(--md-sys-color-surface-container-lowest);--editor-border: 1px solid var(--md-sys-color-outline-variant);--editor-radius: var(--md-sys-shape-corner-large, 16px);--editor-muted: var(--md-sys-color-on-surface-variant);--editor-accent: var(--md-sys-color-primary)}.editor-content{padding:16px;background:var(--editor-surface);border:var(--editor-border);border-radius:var(--editor-radius);display:grid;gap:12px}.editor-content .mat-mdc-form-field{width:100%;max-width:none;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var( --md-sys-color-outline-variant );--mdc-outlined-text-field-hover-outline-color: var( --md-sys-color-outline );--mdc-outlined-text-field-focus-outline-color: var( --md-sys-color-primary );--mdc-outlined-text-field-error-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-error-focus-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-error-hover-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-label-text-color: var( --md-sys-color-on-surface-variant );--mdc-outlined-text-field-input-text-color: var( --md-sys-color-on-surface );--mdc-outlined-text-field-supporting-text-color: var( --md-sys-color-on-surface-variant )}.editor-content .mat-mdc-form-field.w-full{max-width:none}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.editor-split{grid-template-columns:minmax(0,1fr);align-items:start}.editor-main,.editor-aside{display:grid;gap:12px}.skin-preview-wrap{border-radius:calc(var(--editor-radius) - 4px);border:var(--editor-border);background:var(--md-sys-color-surface-container);padding:12px}.g{display:grid}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.g-auto-200{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.g-1-auto{grid-template-columns:1fr auto}.row-flow{grid-auto-flow:column}.gap-6{gap:6px}.gap-8{gap:8px}.gap-12{gap:12px}.ai-center{align-items:center}.ai-end{align-items:end}.mt-12{margin-top:12px}.mb-8{margin-bottom:8px}.mb-6{margin-bottom:6px}.my-8{margin:8px 0}.subtitle{margin:8px 0 4px;color:var(--editor-muted);font-weight:500}.section-title{color:var(--editor-muted);font-weight:600}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--md-sys-color-error);font-size:.85rem}.muted{color:var(--editor-muted)}.text-caption{color:var(--editor-muted);font-size:.8rem}:host ::ng-deep .mat-mdc-select-panel .option-icon{font-size:18px;margin-right:6px;vertical-align:middle}:host ::ng-deep .mat-mdc-select-panel .color-dot{width:10px;height:10px;border-radius:999px;display:inline-block;margin-right:6px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-outline)}:host ::ng-deep .mat-mdc-select-panel .color-primary{background:var(--md-sys-color-primary)}:host ::ng-deep .mat-mdc-select-panel .color-accent{background:var(--md-sys-color-tertiary)}:host ::ng-deep .mat-mdc-select-panel .color-warn{background:var(--md-sys-color-error)}:host ::ng-deep .mat-mdc-select-panel .color-default{background:var(--md-sys-color-outline)}@media(max-width:1024px){.editor-split{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "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$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i2$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i10.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i10.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i10.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i10.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i10.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i11.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i11.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i12.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "ngmodule", type: MatMenuModule }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisListSkinPreviewComponent, selector: "praxis-list-skin-preview", inputs: ["config", "items", "theme"] }, { kind: "component", type: PraxisMetaEditorTextComponent, selector: "praxis-meta-editor-text", inputs: ["model", "setPipe"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorChipComponent, selector: "praxis-meta-editor-chip", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorRatingComponent, selector: "praxis-meta-editor-rating", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorCurrencyComponent, selector: "praxis-meta-editor-currency", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorDateComponent, selector: "praxis-meta-editor-date", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorIconComponent, selector: "praxis-meta-editor-icon", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorImageComponent, selector: "praxis-meta-editor-image", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisListJsonConfigEditorComponent, selector: "praxis-list-json-config-editor", inputs: ["document"], outputs: ["documentChange", "validationChange", "editorEvent"] }, { kind: "component", type: PdxColorPickerComponent, selector: "pdx-color-picker", inputs: ["actionsLayout", "activeView", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "clearButton", "disabledMode", "readonlyMode", "visible", "presentationMode", "fillMode", "format", "gradientSettings", "icon", "iconClass", "svgIcon", "paletteSettings", "popupSettings", "preview", "rounded", "size", "tabindex", "views", "maxRecent", "showRecent"], outputs: ["valueChange", "open", "close", "cancel", "activeViewChange", "activeColorClick", "focusEvent", "blurEvent"] }, { kind: "component", type: SurfaceOpenActionEditorComponent, selector: "praxis-surface-open-action-editor", inputs: ["value", "hostKind"], outputs: ["valueChange"] }] });
|
|
5952
|
+
], ngImport: i0, template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <ng-template matTabContent>\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">\n {{ tx('Assistant-applied adjustments replace the entire configuration object.') }}\n </div>\n <button\n mat-icon-button\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Resource (API)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.dataSource.resourcePath\"\n (ngModelChange)=\"onResourcePathChange($event)\"\n [placeholder]=\"tx('e.g.: users')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Query (JSON)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [(ngModel)]=\"queryJson\"\n (ngModelChange)=\"onQueryChanged($event)\"\n [placeholder]=\"queryPlaceholder()\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n *ngIf=\"!queryError\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"queryError\">{{ queryError }}</mat-error>\n </mat-form-field>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Sort by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortField\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortDir\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </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 <mat-option *ngFor=\"let ga of globalActionCatalog\" [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || \"bolt\" }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n </mat-select>\n <mat-hint\n *ngIf=\"!globalActionCatalog.length\"\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a structured global action.') }}\n </div>\n </div>\n <div\n *ngFor=\"let a of working.actions || []; let i = index\"\n class=\"g g-auto-200 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('ID') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.id\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action type') }}</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">{{ tx('Icon') }}</mat-option>\n <mat-option value=\"button\">{{ tx('Button') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.icon\"\n (ngModelChange)=\"onActionsChanged()\"\n [placeholder]=\"tx('e.g.: edit, delete')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('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 <mat-option *ngFor=\"let action of globalActionCatalog\" [value]=\"action.id\">\n {{ action.label || action.id }}\n </mat-option>\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 <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Variant') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.buttonVariant\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option value=\"stroked\">{{ tx('Outlined') }}</mat-option>\n <mat-option value=\"raised\">{{ tx('Raised') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Filled') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action color') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span\n class=\"color-dot\"\n [style.background]=\"colorDotBackground(c.value)\"\n ></span\n >{{ tx(c.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g gap-8\"\n *ngIf=\"isCustomColor(a.color); else actionCustomBtn\"\n >\n <pdx-color-picker\n [label]=\"tx('Custom color')\"\n [format]=\"'hex'\"\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n ></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action payload') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.emitPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Payload emitted by the action.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ actionVisibilityLabel() }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"actionShowIfModel(i)\"\n (ngModelChange)=\"onActionShowIfChanged(i, a, $event)\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n *ngIf=\"(a.kind || 'icon') === 'icon'\"\n mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n >\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button\n *ngIf=\"a.buttonVariant === 'stroked'\"\n mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'stroked')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"a.buttonVariant === 'raised'\"\n mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'raised')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\"\n mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'flat')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n </ng-container>\n <span class=\"muted\">{{ tx('Preview') }}</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.globalAction?.actionId\">\n <mat-slide-toggle\n [(ngModel)]=\"a.showLoading\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Show loading') }}</mat-slide-toggle\n >\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >{{ tx('Confirmation') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">{{ tx('Type') }}</span>\n <mat-button-toggle-group\n [value]=\"a.confirmation?.type || ''\"\n (change)=\"applyConfirmationPreset(a, $event.value)\"\n >\n <mat-button-toggle value=\"\">{{ tx('Default') }}</mat-button-toggle>\n <mat-button-toggle value=\"danger\">{{ tx('Danger') }}</mat-button-toggle>\n <mat-button-toggle value=\"warning\">{{ tx('Warning') }}</mat-button-toggle>\n <mat-button-toggle value=\"info\">{{ tx('Info') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.title\"\n (ngModelChange)=\"setConfirmationField(a, 'title', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.message\"\n (ngModelChange)=\"setConfirmationField(a, 'message', $event)\"\n />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">{{ tx('Preview') }}</div>\n <div class=\"text-caption\">\n <strong>{{\n a.confirmation?.title || tx('Confirm action')\n }}</strong>\n </div>\n <div class=\"text-caption muted\">\n {{\n a.confirmation?.message ||\n tx('Are you sure you want to continue?')\n }}\n </div>\n <div class=\"text-caption\">\n <span\n class=\"confirm-type\"\n [ngClass]=\"a.confirmation?.type || 'default'\"\n >{{ tx('Type') }}:\n {{ a.confirmation?.type || tx('Default').toLowerCase() }}</span\n >\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <ng-container *ngIf=\"isSurfaceOpenCommand(a); else defaultGlobalActionPayloadEditor\">\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n </ng-container>\n <ng-template #defaultGlobalActionPayloadEditor>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload (JSON/Template)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"getGlobalActionPayloadText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadTextChange(a, $event)\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalActionPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalActionPayloadInvalid(a)\"\n >{{ tx('Invalid JSON') }}</mat-error\n >\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalActionPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalActionPayloadExampleHint(a)\n }}</span>\n </div>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n </div>\n </div>\n </div>\n </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 <ng-container\n *ngIf=\"working.layout.variant === 'list'; else cardModels\"\n >\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Media on the left') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel (large media)') }}</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container\n *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\"\n >\n <mat-option value=\"standard\">{{ tx('Standard tile') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Tile with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel tile') }}</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Card with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel') }}</mat-option>\n </ng-template>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Lines') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.lines\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"1\">1</mat-option>\n <mat-option [value]=\"2\">2</mat-option>\n <mat-option [value]=\"3\">3</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Items per page') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Density') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.density\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"default\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"comfortable\">{{ tx('Comfortable') }}</mat-option>\n <mat-option value=\"compact\">{{ tx('Compact') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Spacing between items') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.itemSpacing\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No extra space') }}</mat-option>\n <mat-option value=\"tight\">{{ tx('Tight') }}</mat-option>\n <mat-option value=\"default\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"relaxed\">{{ tx('Relaxed') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"working.layout.variant !== 'tiles'\"\n >\n <mat-label>{{ tx('Dividers') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.dividers\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('None') }}</mat-option>\n <mat-option value=\"between\">{{ tx('Between groups') }}</mat-option>\n <mat-option value=\"all\">{{ tx('All') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <ng-container *ngIf=\"fields.length > 0; else groupByText\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Group by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"\">{{ tx('None') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <ng-template #groupByText>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Group by') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n [placeholder]=\"tx('e.g.: department')\"\n />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.stickySectionHeader\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Sticky section header') }}\n </mat-slide-toggle>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.virtualScroll\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Virtual scroll') }}\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('List tools') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSearch\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show search') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSort\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show sorting') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showRange\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show total X-Y range') }}</mat-slide-toggle\n >\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngIf=\"working.ui?.showSearch\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field to search') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.ui.searchField\"\n (ngModelChange)=\"onUiChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Search placeholder') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.ui.searchPlaceholder\"\n (ngModelChange)=\"onUiChanged()\"\n [placeholder]=\"tx('e.g.: Search by title')\"\n />\n </mat-form-field>\n </div>\n <div class=\"mt-12\" *ngIf=\"working.ui?.showSort\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Sorting options (label \u2192 field+direction)') }}\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">\n {{ tx('Add option') }}\n </button>\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngFor=\"let r of uiSortRows; let i = index\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"r.label\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n [placeholder]=\"tx('e.g.: Most recent')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.field\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.dir\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n </div>\n </div>\n </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 <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of primaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Secondary -->\n <mat-expansion-panel [expanded]=\"!!mappingSecondary.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSecondary.type) }}</mat-icon>\n <span>{{ tx('Secondary (Summary)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSecondary.field || tx('Not mapped')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'subtitle';\n onMappingChanged()\n \"\n >\n {{ tx('Subtitle') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'date';\n mappingSecondary.field = 'hireDate';\n mappingSecondary.dateStyle = 'short';\n onMappingChanged()\n \"\n >\n {{ tx('Short date') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applySalaryPreset()\"\n >\n {{ tx('Salary') }}\n </button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of secondaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('CSS class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Inline style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel\n [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingMeta.type || \"text\")\n }}</mat-icon>\n <span>{{ tx('Meta (Detail/Side)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingMetaFields.length\n ? tx('Composed field ({{count}})', { count: mappingMetaFields.length })\n : mappingMeta.field || tx('Not mapped')\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <!-- Composition Mode Toggle -->\n <div class=\"g g-1-1 gap-12 p-12 bg-subtle rounded\">\n <div class=\"text-caption muted\">{{ tx('Composition mode') }}</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Fields to compose (multi-select)') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMetaFields\"\n multiple\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g g-1-1 ai-center gap-12\"\n *ngIf=\"mappingMetaFields.length\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Separator') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMetaSeparator\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"mappingMetaWrapSecondInParens\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n {{ tx('(Second) in parentheses') }}\n </mat-slide-toggle>\n </div>\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"!mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Single field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- None --') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of metaTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Type configuration (pluggable editors) -->\n @switch (mappingMeta.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of trailingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'chip';\n mappingTrailing.chipColor = 'primary';\n mappingTrailing.chipVariant = 'filled';\n mappingTrailing.field = 'status';\n onMappingChanged()\n \"\n >\n {{ tx('Status chip') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'icon';\n mappingTrailing.field = 'status';\n mappingTrailing.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Status icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyPricePreset()\"\n >\n {{ tx('Price') }}\n </button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-chip>\n }\n @case (\"rating\") {\n <praxis-meta-editor-rating\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-rating>\n }\n @case (\"icon\") {\n <praxis-meta-editor-icon\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-icon>\n }\n @case (\"image\") {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('URL / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingTrailing.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingTrailing.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Leading -->\n <mat-expansion-panel\n [expanded]=\"\n !!mappingLeading.field ||\n (mappingLeading.type === 'icon' && !!mappingLeading.icon) ||\n (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\n \"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingLeading.type) }}</mat-icon>\n <span>{{ tx('Leading (left)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingLeading.type === \"icon\"\n ? mappingLeading.icon || tx('Static icon')\n : mappingLeading.field ||\n (mappingLeading.imageUrl\n ? tx('Static image')\n : tx('Not mapped'))\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of leadingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n \"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'icon';\n mappingLeading.icon = 'person';\n mappingLeading.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'image';\n mappingLeading.imageUrl = 'https://placehold.co/64x64';\n mappingLeading.imageAlt = 'Avatar';\n mappingLeading.badgeText = '${item.status}';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar image + badge') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'chip';\n mappingLeading.field = 'tag';\n mappingLeading.chipColor = 'accent';\n mappingLeading.chipVariant = 'filled';\n onMappingChanged()\n \"\n >\n {{ tx('Tag chip') }}\n </button>\n </div>\n\n <!-- Icon Specific -->\n <div\n class=\"g g-1-auto gap-12 ai-center\"\n *ngIf=\"mappingLeading.type === 'icon'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.icon\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\">\n <mat-icon>search</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"text-caption muted\">\n {{ tx('Use the `|iconMap` pipe in the extra pipe for dynamic rendering.') }}\n </div>\n </div>\n <div *ngIf=\"mappingLeading.type === 'icon'\">\n <praxis-meta-editor-icon\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div\n class=\"g g-1-1 gap-12\"\n *ngIf=\"mappingLeading.type === 'image'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Image URL') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Alt text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageAlt\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Badge text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.badgeText\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <div\n *ngFor=\"let f of features; let i = index\"\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\">\n <mat-icon>{{ f.icon || \"search\" }}</mat-icon>\n </button>\n <mat-form-field\n appearance=\"outline\"\n class=\"dense-form-field no-sub\"\n >\n <input\n matInput\n [(ngModel)]=\"f.expr\"\n (ngModelChange)=\"onFeaturesChanged()\"\n [placeholder]=\"tx('Expression/Text')\"\n />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\">\n <mat-icon>add</mat-icon> {{ tx('Add feature') }}\n </button>\n </div>\n </mat-expansion-panel>\n <!-- Section Header -->\n <mat-expansion-panel [expanded]=\"!!mappingSectionHeader.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingSectionHeader.type)\n }}</mat-icon>\n <span>{{ tx('Section header') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSectionHeader.expr || tx('Not configured')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSectionHeader.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of sectionHeaderTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Expression (item.key)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingSectionHeader.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'text';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default text') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'chip';\n mappingSectionHeader.chipColor = 'primary';\n mappingSectionHeader.chipVariant = 'filled';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default chip') }}\n </button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingSectionHeader.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingSectionHeader\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Empty State -->\n <mat-expansion-panel [expanded]=\"!!mappingEmptyState.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>inbox</mat-icon>\n <span>{{ tx('Empty state') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingEmptyState.expr || tx('Default')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingEmptyState.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of emptyStateTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingEmptyState.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'text';\n mappingEmptyState.expr = tx('No items available');\n onMappingChanged()\n \"\n >\n {{ tx('Default message') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'image';\n mappingEmptyState.imageUrl = '/list-empty-state.svg';\n mappingEmptyState.imageAlt = tx('No results');\n onMappingChanged()\n \"\n >\n {{ tx('Default image') }}\n </button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingEmptyState.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingEmptyState\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n </mat-accordion>\n\n <button mat-flat-button color=\"primary\" (click)=\"applyTemplate()\">\n {{ tx('Apply mapping') }}\n </button>\n <button\n mat-button\n (click)=\"inferFromFields()\"\n [disabled]=\"!fields.length\"\n >\n {{ tx('Infer from schema') }}\n </button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Skeleton count') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"0\"\n [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\"\n />\n </mat-form-field>\n </div>\n\n <div class=\"g gap-12 mt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"section-title mat-subtitle-1\">{{ tx('Theme preview') }}</span>\n <mat-button-toggle-group\n [(ngModel)]=\"skinPreviewTheme\"\n (change)=\"onSkinChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle [value]=\"'light'\">{{ tx('Light') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">{{ tx('Dark') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">{{ tx('Grid') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview\n [config]=\"working\"\n [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"\n ></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <ng-template matTabContent>\n <div\n class=\"editor-content grid gap-3\"\n *ngIf=\"working?.a11y && working?.events\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default locale') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.locale\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"tx('e.g.: en-US')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default currency') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.currency\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"currencyPlaceholder()\"\n />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Accessibility') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabel\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-labelledby') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabelledBy\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.highContrast\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('High contrast') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.reduceMotion\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('Reduce motion') }}</mat-slide-toggle\n >\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Events') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('itemClick') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.itemClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.actionClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.selectionChange\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.loaded\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.mode\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No selection') }}</mat-option>\n <mat-option value=\"single\">{{ tx('Single') }}</mat-option>\n <mat-option value=\"multiple\">{{ tx('Multiple') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form name') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlName\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form path') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlPath\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Compare by (field)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.compareBy\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Return') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.return\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-button (click)=\"applySkinPreset('pill-soft')\">\n {{ tx('Pill Soft') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">\n {{ tx('Gradient Tile') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('glass')\">\n {{ tx('Glass') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">\n {{ tx('Elevated') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('outline')\">\n {{ tx('Outline') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('flat')\">\n {{ tx('Flat') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">\n {{ tx('Neumorphism') }}\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Style') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.skin.type\"\n (ngModelChange)=\"onSkinTypeChanged($event)\"\n >\n <mat-option value=\"pill-soft\">{{ tx('Pill Soft') }}</mat-option>\n <mat-option value=\"gradient-tile\">{{ tx('Gradient Tile') }}</mat-option>\n <mat-option value=\"glass\">{{ tx('Glass') }}</mat-option>\n <mat-option value=\"elevated\">{{ tx('Elevated') }}</mat-option>\n <mat-option value=\"outline\">{{ tx('Outline') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Flat') }}</mat-option>\n <mat-option value=\"neumorphism\">{{ tx('Neumorphism') }}</mat-option>\n <mat-option value=\"custom\">{{ tx('Custom') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Radius') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.radius\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 1.25rem')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shadow') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.shadow\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: var(--md-sys-elevation-level2)')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.border\"\n (ngModelChange)=\"onSkinChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n *ngIf=\"working.skin.type === 'glass'\"\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Blur') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.backdropBlur\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 8px')\"\n />\n </mat-form-field>\n <div *ngIf=\"working.skin.type === 'gradient-tile'\" class=\"g gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient from') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient to') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Angle') }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\"\n />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Extra CSS class (skin.class)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.class\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: my-list-skin')\"\n />\n </mat-form-field>\n\n <div\n *ngIf=\"working.skin.type === 'custom'\"\n class=\"g g-auto-220 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Inline style (skin.inlineStyle)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"working.skin.inlineStyle\"\n (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"\n ></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n {{ tx('CSS class example (add this to your global styles):') }}\n <pre class=\"code-block\">\n.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre\n >\n </div>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n</mat-tab-group>\n", styles: [".confirm-type{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;line-height:16px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.confirm-type.danger{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.confirm-type.warning{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.confirm-type.info{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}:host{display:block;color:var(--md-sys-color-on-surface)}.list-editor-tabs{--editor-surface: var(--md-sys-color-surface-container-lowest);--editor-border: 1px solid var(--md-sys-color-outline-variant);--editor-radius: var(--md-sys-shape-corner-large, 16px);--editor-muted: var(--md-sys-color-on-surface-variant);--editor-accent: var(--md-sys-color-primary)}.editor-content{padding:16px;background:var(--editor-surface);border:var(--editor-border);border-radius:var(--editor-radius);display:grid;gap:12px}.editor-content .mat-mdc-form-field{width:100%;max-width:none;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var( --md-sys-color-outline-variant );--mdc-outlined-text-field-hover-outline-color: var( --md-sys-color-outline );--mdc-outlined-text-field-focus-outline-color: var( --md-sys-color-primary );--mdc-outlined-text-field-error-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-error-focus-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-error-hover-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-label-text-color: var( --md-sys-color-on-surface-variant );--mdc-outlined-text-field-input-text-color: var( --md-sys-color-on-surface );--mdc-outlined-text-field-supporting-text-color: var( --md-sys-color-on-surface-variant )}.editor-content .mat-mdc-form-field.w-full{max-width:none}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.editor-split{grid-template-columns:minmax(0,1fr);align-items:start}.editor-main,.editor-aside{display:grid;gap:12px}.skin-preview-wrap{border-radius:calc(var(--editor-radius) - 4px);border:var(--editor-border);background:var(--md-sys-color-surface-container);padding:12px}.g{display:grid}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.g-auto-200{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.g-1-auto{grid-template-columns:1fr auto}.row-flow{grid-auto-flow:column}.gap-6{gap:6px}.gap-8{gap:8px}.gap-12{gap:12px}.ai-center{align-items:center}.ai-end{align-items:end}.mt-12{margin-top:12px}.mb-8{margin-bottom:8px}.mb-6{margin-bottom:6px}.my-8{margin:8px 0}.subtitle{margin:8px 0 4px;color:var(--editor-muted);font-weight:500}.section-title{color:var(--editor-muted);font-weight:600}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--md-sys-color-error);font-size:.85rem}.muted{color:var(--editor-muted)}.text-caption{color:var(--editor-muted);font-size:.8rem}:host ::ng-deep .mat-mdc-select-panel .option-icon{font-size:18px;margin-right:6px;vertical-align:middle}:host ::ng-deep .mat-mdc-select-panel .color-dot{width:10px;height:10px;border-radius:999px;display:inline-block;margin-right:6px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-outline)}:host ::ng-deep .mat-mdc-select-panel .color-primary{background:var(--md-sys-color-primary)}:host ::ng-deep .mat-mdc-select-panel .color-accent{background:var(--md-sys-color-tertiary)}:host ::ng-deep .mat-mdc-select-panel .color-warn{background:var(--md-sys-color-error)}:host ::ng-deep .mat-mdc-select-panel .color-default{background:var(--md-sys-color-outline)}@media(max-width:1024px){.editor-split{grid-template-columns:minmax(0,1fr)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "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$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2$1.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i2$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i2$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i3$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i10.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i10.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i10.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i10.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i10.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i11.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i11.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i12.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "ngmodule", type: MatMenuModule }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisListSkinPreviewComponent, selector: "praxis-list-skin-preview", inputs: ["config", "items", "theme"] }, { kind: "component", type: PraxisMetaEditorTextComponent, selector: "praxis-meta-editor-text", inputs: ["model", "setPipe"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorChipComponent, selector: "praxis-meta-editor-chip", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorRatingComponent, selector: "praxis-meta-editor-rating", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorCurrencyComponent, selector: "praxis-meta-editor-currency", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorDateComponent, selector: "praxis-meta-editor-date", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorIconComponent, selector: "praxis-meta-editor-icon", inputs: ["model", "paletteOptions", "colorDotBackground", "isCustomColor", "enableCustomColor"], outputs: ["change"] }, { kind: "component", type: PraxisMetaEditorImageComponent, selector: "praxis-meta-editor-image", inputs: ["model"], outputs: ["change"] }, { kind: "component", type: PraxisListJsonConfigEditorComponent, selector: "praxis-list-json-config-editor", inputs: ["document"], outputs: ["documentChange", "validationChange", "editorEvent"] }, { kind: "component", type: PdxColorPickerComponent, selector: "pdx-color-picker", inputs: ["actionsLayout", "activeView", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "clearButton", "disabledMode", "readonlyMode", "visible", "presentationMode", "fillMode", "format", "gradientSettings", "icon", "iconClass", "svgIcon", "paletteSettings", "popupSettings", "preview", "rounded", "size", "tabindex", "views", "maxRecent", "showRecent"], outputs: ["valueChange", "open", "close", "cancel", "activeViewChange", "activeColorClick", "focusEvent", "blurEvent"] }, { kind: "component", type: SurfaceOpenActionEditorComponent, selector: "praxis-surface-open-action-editor", inputs: ["value", "hostKind"], outputs: ["valueChange"] }] });
|
|
5884
5953
|
}
|
|
5885
5954
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisListConfigEditor, decorators: [{
|
|
5886
5955
|
type: Component,
|
|
5887
5956
|
args: [{ selector: 'praxis-list-config-editor', standalone: true, providers: [
|
|
5957
|
+
{
|
|
5958
|
+
provide: PRAXIS_I18N_CONFIG,
|
|
5959
|
+
multi: true,
|
|
5960
|
+
deps: [[new Optional(), new SkipSelf(), PraxisI18nService]],
|
|
5961
|
+
useFactory: (parent) => ({
|
|
5962
|
+
locale: parent?.getLocale(),
|
|
5963
|
+
fallbackLocale: parent?.getFallbackLocale(),
|
|
5964
|
+
}),
|
|
5965
|
+
},
|
|
5888
5966
|
providePraxisI18nConfig(SURFACE_OPEN_I18N_CONFIG),
|
|
5889
5967
|
providePraxisListI18n(),
|
|
5890
5968
|
], imports: [
|
|
@@ -5915,7 +5993,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
5915
5993
|
PraxisListJsonConfigEditorComponent,
|
|
5916
5994
|
PdxColorPickerComponent,
|
|
5917
5995
|
SurfaceOpenActionEditorComponent,
|
|
5918
|
-
], template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <ng-template matTabContent>\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">\n {{ tx('Assistant-applied adjustments replace the entire configuration object.') }}\n </div>\n <button\n mat-icon-button\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Resource (API)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.dataSource.resourcePath\"\n (ngModelChange)=\"onResourcePathChange($event)\"\n [placeholder]=\"tx('e.g.: users')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Query (JSON)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [(ngModel)]=\"queryJson\"\n (ngModelChange)=\"onQueryChanged($event)\"\n [placeholder]=\"queryPlaceholder()\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n *ngIf=\"!queryError\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"queryError\">{{ queryError }}</mat-error>\n </mat-form-field>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Sort by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortField\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortDir\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </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 <mat-option *ngFor=\"let ga of globalActionCatalog\" [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || \"bolt\" }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n </mat-select>\n <mat-hint\n *ngIf=\"!globalActionCatalog.length\"\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a global `command`.') }}\n </div>\n </div>\n <div\n *ngFor=\"let a of working.actions || []; let i = index\"\n class=\"g g-auto-200 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('ID') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.id\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action type') }}</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">{{ tx('Icon') }}</mat-option>\n <mat-option value=\"button\">{{ tx('Button') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.icon\"\n (ngModelChange)=\"onActionsChanged()\"\n [placeholder]=\"tx('e.g.: edit, delete')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Command (global)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.command\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder=\"global:toast.success\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.label\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Variant') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.buttonVariant\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option value=\"stroked\">{{ tx('Outlined') }}</mat-option>\n <mat-option value=\"raised\">{{ tx('Raised') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Filled') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action color') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span\n class=\"color-dot\"\n [style.background]=\"colorDotBackground(c.value)\"\n ></span\n >{{ tx(c.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g gap-8\"\n *ngIf=\"isCustomColor(a.color); else actionCustomBtn\"\n >\n <pdx-color-picker\n [label]=\"tx('Custom color')\"\n [format]=\"'hex'\"\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n ></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action payload') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.emitPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Payload emitted by the action.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ actionVisibilityLabel() }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"actionShowIfModel(i)\"\n (ngModelChange)=\"onActionShowIfChanged(i, a, $event)\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n *ngIf=\"(a.kind || 'icon') === 'icon'\"\n mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n >\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button\n *ngIf=\"a.buttonVariant === 'stroked'\"\n mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'stroked')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"a.buttonVariant === 'raised'\"\n mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'raised')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\"\n mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'flat')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n </ng-container>\n <span class=\"muted\">{{ tx('Preview') }}</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.command\">\n <mat-slide-toggle\n [(ngModel)]=\"a.showLoading\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Show loading') }}</mat-slide-toggle\n >\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >{{ tx('Confirmation') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">{{ tx('Type') }}</span>\n <mat-button-toggle-group\n [value]=\"a.confirmation?.type || ''\"\n (change)=\"applyConfirmationPreset(a, $event.value)\"\n >\n <mat-button-toggle value=\"\">{{ tx('Default') }}</mat-button-toggle>\n <mat-button-toggle value=\"danger\">{{ tx('Danger') }}</mat-button-toggle>\n <mat-button-toggle value=\"warning\">{{ tx('Warning') }}</mat-button-toggle>\n <mat-button-toggle value=\"info\">{{ tx('Info') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.title\"\n (ngModelChange)=\"setConfirmationField(a, 'title', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.message\"\n (ngModelChange)=\"setConfirmationField(a, 'message', $event)\"\n />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">{{ tx('Preview') }}</div>\n <div class=\"text-caption\">\n <strong>{{\n a.confirmation?.title || tx('Confirm action')\n }}</strong>\n </div>\n <div class=\"text-caption muted\">\n {{\n a.confirmation?.message ||\n tx('Are you sure you want to continue?')\n }}\n </div>\n <div class=\"text-caption\">\n <span\n class=\"confirm-type\"\n [ngClass]=\"a.confirmation?.type || 'default'\"\n >{{ tx('Type') }}:\n {{ a.confirmation?.type || tx('Default').toLowerCase() }}</span\n >\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <ng-container *ngIf=\"isSurfaceOpenCommand(a); else defaultGlobalPayloadEditor\">\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n </ng-container>\n <ng-template #defaultGlobalPayloadEditor>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload (JSON/Template)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"a.globalPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalPayloadInvalid(a.globalPayload)\"\n >{{ tx('Invalid JSON') }}</mat-error\n >\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalPayloadExampleHint(a)\n }}</span>\n </div>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n </div>\n </div>\n </div>\n </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 <ng-container\n *ngIf=\"working.layout.variant === 'list'; else cardModels\"\n >\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Media on the left') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel (large media)') }}</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container\n *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\"\n >\n <mat-option value=\"standard\">{{ tx('Standard tile') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Tile with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel tile') }}</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Card with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel') }}</mat-option>\n </ng-template>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Lines') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.lines\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"1\">1</mat-option>\n <mat-option [value]=\"2\">2</mat-option>\n <mat-option [value]=\"3\">3</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Items per page') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Density') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.density\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"default\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"comfortable\">{{ tx('Comfortable') }}</mat-option>\n <mat-option value=\"compact\">{{ tx('Compact') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Spacing between items') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.itemSpacing\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No extra space') }}</mat-option>\n <mat-option value=\"tight\">{{ tx('Tight') }}</mat-option>\n <mat-option value=\"default\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"relaxed\">{{ tx('Relaxed') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"working.layout.variant !== 'tiles'\"\n >\n <mat-label>{{ tx('Dividers') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.dividers\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('None') }}</mat-option>\n <mat-option value=\"between\">{{ tx('Between groups') }}</mat-option>\n <mat-option value=\"all\">{{ tx('All') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <ng-container *ngIf=\"fields.length > 0; else groupByText\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Group by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"\">{{ tx('None') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <ng-template #groupByText>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Group by') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n [placeholder]=\"tx('e.g.: department')\"\n />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.stickySectionHeader\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Sticky section header') }}\n </mat-slide-toggle>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.virtualScroll\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Virtual scroll') }}\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('List tools') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSearch\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show search') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSort\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show sorting') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showRange\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show total X-Y range') }}</mat-slide-toggle\n >\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngIf=\"working.ui?.showSearch\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field to search') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.ui.searchField\"\n (ngModelChange)=\"onUiChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Search placeholder') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.ui.searchPlaceholder\"\n (ngModelChange)=\"onUiChanged()\"\n [placeholder]=\"tx('e.g.: Search by title')\"\n />\n </mat-form-field>\n </div>\n <div class=\"mt-12\" *ngIf=\"working.ui?.showSort\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Sorting options (label \u2192 field+direction)') }}\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">\n {{ tx('Add option') }}\n </button>\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngFor=\"let r of uiSortRows; let i = index\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"r.label\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n [placeholder]=\"tx('e.g.: Most recent')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.field\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.dir\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n </div>\n </div>\n </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 <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of primaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Secondary -->\n <mat-expansion-panel [expanded]=\"!!mappingSecondary.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSecondary.type) }}</mat-icon>\n <span>{{ tx('Secondary (Summary)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSecondary.field || tx('Not mapped')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'subtitle';\n onMappingChanged()\n \"\n >\n {{ tx('Subtitle') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'date';\n mappingSecondary.field = 'hireDate';\n mappingSecondary.dateStyle = 'short';\n onMappingChanged()\n \"\n >\n {{ tx('Short date') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applySalaryPreset()\"\n >\n {{ tx('Salary') }}\n </button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of secondaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('CSS class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Inline style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel\n [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingMeta.type || \"text\")\n }}</mat-icon>\n <span>{{ tx('Meta (Detail/Side)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingMetaFields.length\n ? tx('Composed field ({{count}})', { count: mappingMetaFields.length })\n : mappingMeta.field || tx('Not mapped')\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <!-- Composition Mode Toggle -->\n <div class=\"g g-1-1 gap-12 p-12 bg-subtle rounded\">\n <div class=\"text-caption muted\">{{ tx('Composition mode') }}</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Fields to compose (multi-select)') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMetaFields\"\n multiple\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g g-1-1 ai-center gap-12\"\n *ngIf=\"mappingMetaFields.length\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Separator') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMetaSeparator\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"mappingMetaWrapSecondInParens\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n {{ tx('(Second) in parentheses') }}\n </mat-slide-toggle>\n </div>\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"!mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Single field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- None --') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of metaTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Type configuration (pluggable editors) -->\n @switch (mappingMeta.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of trailingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'chip';\n mappingTrailing.chipColor = 'primary';\n mappingTrailing.chipVariant = 'filled';\n mappingTrailing.field = 'status';\n onMappingChanged()\n \"\n >\n {{ tx('Status chip') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'icon';\n mappingTrailing.field = 'status';\n mappingTrailing.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Status icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyPricePreset()\"\n >\n {{ tx('Price') }}\n </button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-chip>\n }\n @case (\"rating\") {\n <praxis-meta-editor-rating\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-rating>\n }\n @case (\"icon\") {\n <praxis-meta-editor-icon\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-icon>\n }\n @case (\"image\") {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('URL / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingTrailing.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingTrailing.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Leading -->\n <mat-expansion-panel\n [expanded]=\"\n !!mappingLeading.field ||\n (mappingLeading.type === 'icon' && !!mappingLeading.icon) ||\n (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\n \"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingLeading.type) }}</mat-icon>\n <span>{{ tx('Leading (left)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingLeading.type === \"icon\"\n ? mappingLeading.icon || tx('Static icon')\n : mappingLeading.field ||\n (mappingLeading.imageUrl\n ? tx('Static image')\n : tx('Not mapped'))\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of leadingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n \"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'icon';\n mappingLeading.icon = 'person';\n mappingLeading.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'image';\n mappingLeading.imageUrl = 'https://placehold.co/64x64';\n mappingLeading.imageAlt = 'Avatar';\n mappingLeading.badgeText = '${item.status}';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar image + badge') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'chip';\n mappingLeading.field = 'tag';\n mappingLeading.chipColor = 'accent';\n mappingLeading.chipVariant = 'filled';\n onMappingChanged()\n \"\n >\n {{ tx('Tag chip') }}\n </button>\n </div>\n\n <!-- Icon Specific -->\n <div\n class=\"g g-1-auto gap-12 ai-center\"\n *ngIf=\"mappingLeading.type === 'icon'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.icon\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\">\n <mat-icon>search</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"text-caption muted\">\n {{ tx('Use the `|iconMap` pipe in the extra pipe for dynamic rendering.') }}\n </div>\n </div>\n <div *ngIf=\"mappingLeading.type === 'icon'\">\n <praxis-meta-editor-icon\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div\n class=\"g g-1-1 gap-12\"\n *ngIf=\"mappingLeading.type === 'image'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Image URL') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Alt text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageAlt\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Badge text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.badgeText\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <div\n *ngFor=\"let f of features; let i = index\"\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\">\n <mat-icon>{{ f.icon || \"search\" }}</mat-icon>\n </button>\n <mat-form-field\n appearance=\"outline\"\n class=\"dense-form-field no-sub\"\n >\n <input\n matInput\n [(ngModel)]=\"f.expr\"\n (ngModelChange)=\"onFeaturesChanged()\"\n [placeholder]=\"tx('Expression/Text')\"\n />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\">\n <mat-icon>add</mat-icon> {{ tx('Add feature') }}\n </button>\n </div>\n </mat-expansion-panel>\n <!-- Section Header -->\n <mat-expansion-panel [expanded]=\"!!mappingSectionHeader.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingSectionHeader.type)\n }}</mat-icon>\n <span>{{ tx('Section header') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSectionHeader.expr || tx('Not configured')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSectionHeader.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of sectionHeaderTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Expression (item.key)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingSectionHeader.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'text';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default text') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'chip';\n mappingSectionHeader.chipColor = 'primary';\n mappingSectionHeader.chipVariant = 'filled';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default chip') }}\n </button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingSectionHeader.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingSectionHeader\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Empty State -->\n <mat-expansion-panel [expanded]=\"!!mappingEmptyState.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>inbox</mat-icon>\n <span>{{ tx('Empty state') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingEmptyState.expr || tx('Default')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingEmptyState.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of emptyStateTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingEmptyState.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'text';\n mappingEmptyState.expr = tx('No items available');\n onMappingChanged()\n \"\n >\n {{ tx('Default message') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'image';\n mappingEmptyState.imageUrl = '/list-empty-state.svg';\n mappingEmptyState.imageAlt = tx('No results');\n onMappingChanged()\n \"\n >\n {{ tx('Default image') }}\n </button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingEmptyState.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingEmptyState\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n </mat-accordion>\n\n <button mat-flat-button color=\"primary\" (click)=\"applyTemplate()\">\n {{ tx('Apply mapping') }}\n </button>\n <button\n mat-button\n (click)=\"inferFromFields()\"\n [disabled]=\"!fields.length\"\n >\n {{ tx('Infer from schema') }}\n </button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Skeleton count') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"0\"\n [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\"\n />\n </mat-form-field>\n </div>\n\n <div class=\"g gap-12 mt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"section-title mat-subtitle-1\">{{ tx('Theme preview') }}</span>\n <mat-button-toggle-group\n [(ngModel)]=\"skinPreviewTheme\"\n (change)=\"onSkinChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle [value]=\"'light'\">{{ tx('Light') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">{{ tx('Dark') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">{{ tx('Grid') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview\n [config]=\"working\"\n [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"\n ></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <ng-template matTabContent>\n <div\n class=\"editor-content grid gap-3\"\n *ngIf=\"working?.a11y && working?.events\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default locale') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.locale\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"tx('e.g.: en-US')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default currency') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.currency\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"currencyPlaceholder()\"\n />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Accessibility') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabel\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-labelledby') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabelledBy\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.highContrast\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('High contrast') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.reduceMotion\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('Reduce motion') }}</mat-slide-toggle\n >\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Events') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('itemClick') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.itemClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.actionClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.selectionChange\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.loaded\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.mode\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No selection') }}</mat-option>\n <mat-option value=\"single\">{{ tx('Single') }}</mat-option>\n <mat-option value=\"multiple\">{{ tx('Multiple') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form name') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlName\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form path') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlPath\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Compare by (field)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.compareBy\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Return') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.return\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-button (click)=\"applySkinPreset('pill-soft')\">\n {{ tx('Pill Soft') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">\n {{ tx('Gradient Tile') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('glass')\">\n {{ tx('Glass') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">\n {{ tx('Elevated') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('outline')\">\n {{ tx('Outline') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('flat')\">\n {{ tx('Flat') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">\n {{ tx('Neumorphism') }}\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Style') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.skin.type\"\n (ngModelChange)=\"onSkinTypeChanged($event)\"\n >\n <mat-option value=\"pill-soft\">{{ tx('Pill Soft') }}</mat-option>\n <mat-option value=\"gradient-tile\">{{ tx('Gradient Tile') }}</mat-option>\n <mat-option value=\"glass\">{{ tx('Glass') }}</mat-option>\n <mat-option value=\"elevated\">{{ tx('Elevated') }}</mat-option>\n <mat-option value=\"outline\">{{ tx('Outline') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Flat') }}</mat-option>\n <mat-option value=\"neumorphism\">{{ tx('Neumorphism') }}</mat-option>\n <mat-option value=\"custom\">{{ tx('Custom') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Radius') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.radius\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 1.25rem')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shadow') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.shadow\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: var(--md-sys-elevation-level2)')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.border\"\n (ngModelChange)=\"onSkinChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n *ngIf=\"working.skin.type === 'glass'\"\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Blur') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.backdropBlur\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 8px')\"\n />\n </mat-form-field>\n <div *ngIf=\"working.skin.type === 'gradient-tile'\" class=\"g gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient from') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient to') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Angle') }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\"\n />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Extra CSS class (skin.class)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.class\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: my-list-skin')\"\n />\n </mat-form-field>\n\n <div\n *ngIf=\"working.skin.type === 'custom'\"\n class=\"g g-auto-220 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Inline style (skin.inlineStyle)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"working.skin.inlineStyle\"\n (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"\n ></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n {{ tx('CSS class example (add this to your global styles):') }}\n <pre class=\"code-block\">\n.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre\n >\n </div>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n</mat-tab-group>\n\n", styles: [".confirm-type{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;line-height:16px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.confirm-type.danger{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.confirm-type.warning{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.confirm-type.info{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}:host{display:block;color:var(--md-sys-color-on-surface)}.list-editor-tabs{--editor-surface: var(--md-sys-color-surface-container-lowest);--editor-border: 1px solid var(--md-sys-color-outline-variant);--editor-radius: var(--md-sys-shape-corner-large, 16px);--editor-muted: var(--md-sys-color-on-surface-variant);--editor-accent: var(--md-sys-color-primary)}.editor-content{padding:16px;background:var(--editor-surface);border:var(--editor-border);border-radius:var(--editor-radius);display:grid;gap:12px}.editor-content .mat-mdc-form-field{width:100%;max-width:none;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var( --md-sys-color-outline-variant );--mdc-outlined-text-field-hover-outline-color: var( --md-sys-color-outline );--mdc-outlined-text-field-focus-outline-color: var( --md-sys-color-primary );--mdc-outlined-text-field-error-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-error-focus-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-error-hover-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-label-text-color: var( --md-sys-color-on-surface-variant );--mdc-outlined-text-field-input-text-color: var( --md-sys-color-on-surface );--mdc-outlined-text-field-supporting-text-color: var( --md-sys-color-on-surface-variant )}.editor-content .mat-mdc-form-field.w-full{max-width:none}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.editor-split{grid-template-columns:minmax(0,1fr);align-items:start}.editor-main,.editor-aside{display:grid;gap:12px}.skin-preview-wrap{border-radius:calc(var(--editor-radius) - 4px);border:var(--editor-border);background:var(--md-sys-color-surface-container);padding:12px}.g{display:grid}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.g-auto-200{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.g-1-auto{grid-template-columns:1fr auto}.row-flow{grid-auto-flow:column}.gap-6{gap:6px}.gap-8{gap:8px}.gap-12{gap:12px}.ai-center{align-items:center}.ai-end{align-items:end}.mt-12{margin-top:12px}.mb-8{margin-bottom:8px}.mb-6{margin-bottom:6px}.my-8{margin:8px 0}.subtitle{margin:8px 0 4px;color:var(--editor-muted);font-weight:500}.section-title{color:var(--editor-muted);font-weight:600}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--md-sys-color-error);font-size:.85rem}.muted{color:var(--editor-muted)}.text-caption{color:var(--editor-muted);font-size:.8rem}:host ::ng-deep .mat-mdc-select-panel .option-icon{font-size:18px;margin-right:6px;vertical-align:middle}:host ::ng-deep .mat-mdc-select-panel .color-dot{width:10px;height:10px;border-radius:999px;display:inline-block;margin-right:6px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-outline)}:host ::ng-deep .mat-mdc-select-panel .color-primary{background:var(--md-sys-color-primary)}:host ::ng-deep .mat-mdc-select-panel .color-accent{background:var(--md-sys-color-tertiary)}:host ::ng-deep .mat-mdc-select-panel .color-warn{background:var(--md-sys-color-error)}:host ::ng-deep .mat-mdc-select-panel .color-default{background:var(--md-sys-color-outline)}@media(max-width:1024px){.editor-split{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
5996
|
+
], template: "<mat-tab-group class=\"list-editor-tabs\">\n <mat-tab [label]=\"tx('Data')\">\n <ng-template matTabContent>\n <div class=\"editor-content\">\n <div class=\"g g-1-auto gap-8 ai-center\">\n <div class=\"muted\">\n {{ tx('Assistant-applied adjustments replace the entire configuration object.') }}\n </div>\n <button\n mat-icon-button\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('applyConfigFromAdapter does not perform a deep merge. Make sure the adapter sends the full config.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Resource (API)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.dataSource.resourcePath\"\n (ngModelChange)=\"onResourcePathChange($event)\"\n [placeholder]=\"tx('e.g.: users')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource endpoint (resourcePath).')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Query (JSON)') }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [(ngModel)]=\"queryJson\"\n (ngModelChange)=\"onQueryChanged($event)\"\n [placeholder]=\"queryPlaceholder()\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Optional. Use valid JSON for initial filters.')\"\n *ngIf=\"!queryError\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"queryError\">{{ queryError }}</mat-error>\n </mat-form-field>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Sort by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortField\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Resource base field.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"sortDir\"\n (ngModelChange)=\"updateSortConfig()\"\n >\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </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 <mat-option *ngFor=\"let ga of globalActionCatalog\" [value]=\"ga.id\">\n <mat-icon class=\"option-icon\">{{ ga.icon || \"bolt\" }}</mat-icon>\n {{ ga.label }}\n </mat-option>\n </mat-select>\n <mat-hint\n *ngIf=\"!globalActionCatalog.length\"\n class=\"text-caption muted\"\n >{{ tx('No global action registered.') }}</mat-hint\n >\n </mat-form-field>\n <div class=\"muted text-caption\">\n {{ tx('Select to add with a structured global action.') }}\n </div>\n </div>\n <div\n *ngFor=\"let a of working.actions || []; let i = index\"\n class=\"g g-auto-200 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('ID') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.id\"\n (ngModelChange)=\"onActionsChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action type') }}</mat-label>\n <mat-select [(ngModel)]=\"a.kind\" (ngModelChange)=\"onActionsChanged()\">\n <mat-option value=\"icon\">{{ tx('Icon') }}</mat-option>\n <mat-option value=\"button\">{{ tx('Button') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"a.icon\"\n (ngModelChange)=\"onActionsChanged()\"\n [placeholder]=\"tx('e.g.: edit, delete')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('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 <mat-option *ngFor=\"let action of globalActionCatalog\" [value]=\"action.id\">\n {{ action.label || action.id }}\n </mat-option>\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 <ng-container *ngIf=\"a.kind === 'button'\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Variant') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.buttonVariant\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option value=\"stroked\">{{ tx('Outlined') }}</mat-option>\n <mat-option value=\"raised\">{{ tx('Raised') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Filled') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action color') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option *ngFor=\"let c of paletteOptions\" [value]=\"c.value\">\n <span\n class=\"color-dot\"\n [style.background]=\"colorDotBackground(c.value)\"\n ></span\n >{{ tx(c.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g gap-8\"\n *ngIf=\"isCustomColor(a.color); else actionCustomBtn\"\n >\n <pdx-color-picker\n [label]=\"tx('Custom color')\"\n [format]=\"'hex'\"\n [(ngModel)]=\"a.color\"\n (ngModelChange)=\"onActionsChanged()\"\n ></pdx-color-picker>\n </div>\n <ng-template #actionCustomBtn>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"enableCustomActionColor(a)\"\n >\n {{ tx('Use custom color') }}\n </button>\n </ng-template>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Action payload') }}</mat-label>\n <mat-select\n [(ngModel)]=\"a.emitPayload\"\n (ngModelChange)=\"onActionsChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n </mat-select>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Payload emitted by the action.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ actionVisibilityLabel() }}</mat-label>\n <textarea\n matInput\n rows=\"3\"\n [ngModel]=\"actionShowIfModel(i)\"\n (ngModelChange)=\"onActionShowIfChanged(i, a, $event)\"\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"actionConditionTooltip()\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n *ngIf=\"(a.kind || 'icon') === 'icon'\"\n mat-icon-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n >\n <mat-icon\n [praxisIcon]=\"a.icon || 'bolt'\"\n [style.cssText]=\"iconStyle(a.color)\"\n ></mat-icon>\n </button>\n <ng-container *ngIf=\"a.kind === 'button'\">\n <button\n *ngIf=\"a.buttonVariant === 'stroked'\"\n mat-stroked-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'stroked')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"a.buttonVariant === 'raised'\"\n mat-raised-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'raised')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n <button\n *ngIf=\"!a.buttonVariant || a.buttonVariant === 'flat'\"\n mat-flat-button\n [color]=\"isThemeColor(a.color) ? a.color : undefined\"\n [style.cssText]=\"buttonStyle(a.color, 'flat')\"\n >\n {{ a.label || a.id || tx('Action') }}\n </button>\n </ng-container>\n <span class=\"muted\">{{ tx('Preview') }}</span>\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeAction(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n <div class=\"g gap-8 col-span-2\" *ngIf=\"a.globalAction?.actionId\">\n <mat-slide-toggle\n [(ngModel)]=\"a.showLoading\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Show loading') }}</mat-slide-toggle\n >\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title\n >{{ tx('Confirmation') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"text-caption muted\">{{ tx('Type') }}</span>\n <mat-button-toggle-group\n [value]=\"a.confirmation?.type || ''\"\n (change)=\"applyConfirmationPreset(a, $event.value)\"\n >\n <mat-button-toggle value=\"\">{{ tx('Default') }}</mat-button-toggle>\n <mat-button-toggle value=\"danger\">{{ tx('Danger') }}</mat-button-toggle>\n <mat-button-toggle value=\"warning\">{{ tx('Warning') }}</mat-button-toggle>\n <mat-button-toggle value=\"info\">{{ tx('Info') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Title') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.title\"\n (ngModelChange)=\"setConfirmationField(a, 'title', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message') }}</mat-label>\n <input\n matInput\n [ngModel]=\"a.confirmation?.message\"\n (ngModelChange)=\"setConfirmationField(a, 'message', $event)\"\n />\n </mat-form-field>\n <div class=\"g gap-6\">\n <div class=\"text-caption muted\">{{ tx('Preview') }}</div>\n <div class=\"text-caption\">\n <strong>{{\n a.confirmation?.title || tx('Confirm action')\n }}</strong>\n </div>\n <div class=\"text-caption muted\">\n {{\n a.confirmation?.message ||\n tx('Are you sure you want to continue?')\n }}\n </div>\n <div class=\"text-caption\">\n <span\n class=\"confirm-type\"\n [ngClass]=\"a.confirmation?.type || 'default'\"\n >{{ tx('Type') }}:\n {{ a.confirmation?.type || tx('Default').toLowerCase() }}</span\n >\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!a.confirmation?.title && !a.confirmation?.message\"\n >\n {{ tx('Set a title or message for the confirmation.') }}\n </div>\n </div>\n </div>\n </mat-expansion-panel>\n <ng-container *ngIf=\"isSurfaceOpenCommand(a); else defaultGlobalActionPayloadEditor\">\n <div class=\"col-span-2\">\n <praxis-surface-open-action-editor\n [value]=\"getSurfaceOpenGlobalActionPayload(a)\"\n hostKind=\"list\"\n (valueChange)=\"onSurfaceOpenGlobalActionPayloadChange(a, $event)\"\n ></praxis-surface-open-action-editor>\n </div>\n </ng-container>\n <ng-template #defaultGlobalActionPayloadEditor>\n <mat-form-field appearance=\"outline\" class=\"col-span-2\">\n <mat-label>{{ tx('Payload (JSON/Template)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [ngModel]=\"getGlobalActionPayloadText(a)\"\n (ngModelChange)=\"onGlobalActionPayloadTextChange(a, $event)\"\n placeholder='{\"message\":\"${item.name} favoritado\"}'\n ></textarea>\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"globalActionPayloadSchemaTooltip(a)\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error *ngIf=\"isGlobalActionPayloadInvalid(a)\"\n >{{ tx('Invalid JSON') }}</mat-error\n >\n </mat-form-field>\n <div class=\"g row-flow gap-8 ai-center\">\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyGlobalActionPayloadExample(a)\"\n >\n {{ tx('Insert example') }}\n </button>\n <span class=\"muted text-caption\">{{\n globalActionPayloadExampleHint(a)\n }}</span>\n </div>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"a.emitLocal\"\n (ngModelChange)=\"onActionsChanged()\"\n >{{ tx('Emit local event too') }}</mat-slide-toggle\n >\n </div>\n </div>\n </div>\n </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 <ng-container\n *ngIf=\"working.layout.variant === 'list'; else cardModels\"\n >\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Media on the left') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel (large media)') }}</mat-option>\n </ng-container>\n <ng-template #cardModels>\n <ng-container\n *ngIf=\"working.layout.variant === 'tiles'; else cardsOnly\"\n >\n <mat-option value=\"standard\">{{ tx('Standard tile') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Tile with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel tile') }}</mat-option>\n </ng-container>\n <ng-template #cardsOnly>\n <mat-option value=\"standard\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"media\">{{ tx('Card with media') }}</mat-option>\n <mat-option value=\"hotel\">{{ tx('Hotel') }}</mat-option>\n </ng-template>\n </ng-template>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Lines') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.lines\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"1\">1</mat-option>\n <mat-option [value]=\"2\">2</mat-option>\n <mat-option [value]=\"3\">3</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Items per page') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"1\"\n [(ngModel)]=\"working.layout.pageSize\"\n (ngModelChange)=\"onPageSizeChange($event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Density') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.density\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"default\">{{ tx('Default') }}</mat-option>\n <mat-option value=\"comfortable\">{{ tx('Comfortable') }}</mat-option>\n <mat-option value=\"compact\">{{ tx('Compact') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Spacing between items') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.itemSpacing\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No extra space') }}</mat-option>\n <mat-option value=\"tight\">{{ tx('Tight') }}</mat-option>\n <mat-option value=\"default\">{{ tx('Standard') }}</mat-option>\n <mat-option value=\"relaxed\">{{ tx('Relaxed') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"working.layout.variant !== 'tiles'\"\n >\n <mat-label>{{ tx('Dividers') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.dividers\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option value=\"none\">{{ tx('None') }}</mat-option>\n <mat-option value=\"between\">{{ tx('Between groups') }}</mat-option>\n <mat-option value=\"all\">{{ tx('All') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <ng-container *ngIf=\"fields.length > 0; else groupByText\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Group by') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n <mat-option [value]=\"\">{{ tx('None') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n </ng-container>\n <ng-template #groupByText>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Group by') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.layout.groupBy\"\n (ngModelChange)=\"onLayoutChanged()\"\n [placeholder]=\"tx('e.g.: department')\"\n />\n </mat-form-field>\n </ng-template>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.stickySectionHeader\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Sticky section header') }}\n </mat-slide-toggle>\n <mat-slide-toggle\n [(ngModel)]=\"working.layout.virtualScroll\"\n (ngModelChange)=\"onLayoutChanged()\"\n >\n {{ tx('Virtual scroll') }}\n </mat-slide-toggle>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('List tools') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSearch\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show search') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showSort\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show sorting') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working.ui.showRange\"\n (ngModelChange)=\"onUiChanged()\"\n >{{ tx('Show total X-Y range') }}</mat-slide-toggle\n >\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngIf=\"working.ui?.showSearch\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field to search') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.ui.searchField\"\n (ngModelChange)=\"onUiChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Search placeholder') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.ui.searchPlaceholder\"\n (ngModelChange)=\"onUiChanged()\"\n [placeholder]=\"tx('e.g.: Search by title')\"\n />\n </mat-form-field>\n </div>\n <div class=\"mt-12\" *ngIf=\"working.ui?.showSort\">\n <div class=\"g g-1-auto ai-center gap-8\">\n <div class=\"muted\">\n {{ tx('Sorting options (label \u2192 field+direction)') }}\n </div>\n <button mat-flat-button color=\"primary\" (click)=\"addUiSortRow()\">\n {{ tx('Add option') }}\n </button>\n </div>\n <div\n class=\"g g-auto-220 gap-12 ai-end mt-12\"\n *ngFor=\"let r of uiSortRows; let i = index\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"r.label\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n [placeholder]=\"tx('e.g.: Most recent')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.field\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Direction') }}</mat-label>\n <mat-select\n [(ngModel)]=\"r.dir\"\n (ngModelChange)=\"onUiSortRowsChanged()\"\n >\n <mat-option value=\"desc\">{{ tx('Descending') }}</mat-option>\n <mat-option value=\"asc\">{{ tx('Ascending') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div class=\"error\" *ngIf=\"isUiSortRowDuplicate(i)\">\n {{ tx('Duplicate option (field+direction)') }}\n </div>\n <div class=\"flex-end\">\n <button mat-button color=\"warn\" (click)=\"removeUiSortRow(i)\">\n {{ tx('Remove') }}\n </button>\n </div>\n </div>\n </div>\n </div>\n </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 <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingPrimary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of primaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingPrimary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingPrimary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingPrimary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <!-- Advanced -->\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingPrimary.type === 'text' ||\n mappingPrimary.type === 'html'\n \"\n >\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('CSS class') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Inline style') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingPrimary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Secondary -->\n <mat-expansion-panel [expanded]=\"!!mappingSecondary.field\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingSecondary.type) }}</mat-icon>\n <span>{{ tx('Secondary (Summary)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSecondary.field || tx('Not mapped')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'text';\n mappingSecondary.field = 'subtitle';\n onMappingChanged()\n \"\n >\n {{ tx('Subtitle') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSecondary.type = 'date';\n mappingSecondary.field = 'hireDate';\n mappingSecondary.dateStyle = 'short';\n onMappingChanged()\n \"\n >\n {{ tx('Short date') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applySalaryPreset()\"\n >\n {{ tx('Salary') }}\n </button>\n </div>\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSecondary.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of secondaryTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n @switch (mappingSecondary.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSecondary\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingSecondary\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ tx('Formatting and style') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('CSS class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Inline style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSecondary.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <mat-expansion-panel\n [expanded]=\"!!mappingMeta.field || mappingMetaFields.length > 0\"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingMeta.type || \"text\")\n }}</mat-icon>\n <span>{{ tx('Meta (Detail/Side)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingMetaFields.length\n ? tx('Composed field ({{count}})', { count: mappingMetaFields.length })\n : mappingMeta.field || tx('Not mapped')\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n\n <div class=\"g gap-12\">\n <!-- Composition Mode Toggle -->\n <div class=\"g g-1-1 gap-12 p-12 bg-subtle rounded\">\n <div class=\"text-caption muted\">{{ tx('Composition mode') }}</div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Fields to compose (multi-select)') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMetaFields\"\n multiple\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <div\n class=\"g g-1-1 ai-center gap-12\"\n *ngIf=\"mappingMetaFields.length\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Separator') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingMetaSeparator\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-slide-toggle\n [(ngModel)]=\"mappingMetaWrapSecondInParens\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n {{ tx('(Second) in parentheses') }}\n </mat-slide-toggle>\n </div>\n </div>\n\n <!-- Single Field Mode (if no composition) -->\n <div class=\"g g-1-1 gap-12\" *ngIf=\"!mappingMetaFields.length\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Single field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option [value]=\"undefined\">{{ tx('-- None --') }}</mat-option>\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingMeta.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of metaTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n\n <!-- Type configuration (pluggable editors) -->\n @switch (mappingMeta.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingMeta\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingMeta\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingMeta\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingTrailing.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of trailingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'chip';\n mappingTrailing.chipColor = 'primary';\n mappingTrailing.chipVariant = 'filled';\n mappingTrailing.field = 'status';\n onMappingChanged()\n \"\n >\n {{ tx('Status chip') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingTrailing.type = 'icon';\n mappingTrailing.field = 'status';\n mappingTrailing.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Status icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"applyPricePreset()\"\n >\n {{ tx('Price') }}\n </button>\n </div>\n\n @switch (mappingTrailing.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingTrailing\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"currency\") {\n <praxis-meta-editor-currency\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-currency>\n }\n @case (\"date\") {\n <praxis-meta-editor-date\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-date>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-chip>\n }\n @case (\"rating\") {\n <praxis-meta-editor-rating\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-rating>\n }\n @case (\"icon\") {\n <praxis-meta-editor-icon\n [model]=\"mappingTrailing\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.bind(this)\"\n [isCustomColor]=\"isCustomColor.bind(this)\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-icon>\n }\n @case (\"image\") {\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('URL / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingTrailing.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingTrailing.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingTrailing\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingTrailing.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingTrailing.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Leading -->\n <mat-expansion-panel\n [expanded]=\"\n !!mappingLeading.field ||\n (mappingLeading.type === 'icon' && !!mappingLeading.icon) ||\n (mappingLeading.type === 'image' && !!mappingLeading.imageUrl)\n \"\n >\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{ getTypeIcon(mappingLeading.type) }}</mat-icon>\n <span>{{ tx('Leading (left)') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>\n {{\n mappingLeading.type === \"icon\"\n ? mappingLeading.icon || tx('Static icon')\n : mappingLeading.field ||\n (mappingLeading.imageUrl\n ? tx('Static image')\n : tx('Not mapped'))\n }}\n </mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of leadingTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <!-- Field (only if not static icon/image, though user might want dynamic) -->\n <mat-form-field\n appearance=\"outline\"\n *ngIf=\"\n mappingLeading.type !== 'icon' &&\n mappingLeading.type !== 'image'\n \"\n >\n <mat-label>{{ tx('Field') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingLeading.field\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option *ngFor=\"let f of fields\" [value]=\"f\">{{\n f\n }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'icon';\n mappingLeading.icon = 'person';\n mappingLeading.iconColor = 'primary';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar icon') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'image';\n mappingLeading.imageUrl = 'https://placehold.co/64x64';\n mappingLeading.imageAlt = 'Avatar';\n mappingLeading.badgeText = '${item.status}';\n onMappingChanged()\n \"\n >\n {{ tx('Avatar image + badge') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingLeading.type = 'chip';\n mappingLeading.field = 'tag';\n mappingLeading.chipColor = 'accent';\n mappingLeading.chipVariant = 'filled';\n onMappingChanged()\n \"\n >\n {{ tx('Tag chip') }}\n </button>\n </div>\n\n <!-- Icon Specific -->\n <div\n class=\"g g-1-auto gap-12 ai-center\"\n *ngIf=\"mappingLeading.type === 'icon'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Icon') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.icon\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n <button mat-icon-button matSuffix (click)=\"pickLeadingIcon()\">\n <mat-icon>search</mat-icon>\n </button>\n </mat-form-field>\n <div class=\"text-caption muted\">\n {{ tx('Use the `|iconMap` pipe in the extra pipe for dynamic rendering.') }}\n </div>\n </div>\n <div *ngIf=\"mappingLeading.type === 'icon'\">\n <praxis-meta-editor-icon\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground\"\n [isCustomColor]=\"isCustomColor\"\n [enableCustomColor]=\"enableCustomColor.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-icon>\n </div>\n\n <!-- Image Specific -->\n <div\n class=\"g g-1-1 gap-12\"\n *ngIf=\"mappingLeading.type === 'image'\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Image URL') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageUrl\"\n (ngModelChange)=\"onMappingChanged()\"\n [placeholder]=\"tx('https://... or ${item.imageUrl}')\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Use an absolute/relative URL or a ${item.field} expression.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n <mat-error\n *ngIf=\"isImageUrlRequiredInvalid(mappingLeading.imageUrl)\"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Alt text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.imageAlt\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Badge text') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingLeading.badgeText\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n\n @switch (mappingLeading.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingLeading\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingLeading\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <div\n *ngFor=\"let f of features; let i = index\"\n class=\"g g-auto-1 gap-8 ai-center p-8 border rounded mb-2\"\n >\n <button mat-icon-button (click)=\"pickFeatureIcon(i)\">\n <mat-icon>{{ f.icon || \"search\" }}</mat-icon>\n </button>\n <mat-form-field\n appearance=\"outline\"\n class=\"dense-form-field no-sub\"\n >\n <input\n matInput\n [(ngModel)]=\"f.expr\"\n (ngModelChange)=\"onFeaturesChanged()\"\n [placeholder]=\"tx('Expression/Text')\"\n />\n </mat-form-field>\n <button mat-icon-button color=\"warn\" (click)=\"removeFeature(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n <button mat-button color=\"primary\" (click)=\"addFeature()\">\n <mat-icon>add</mat-icon> {{ tx('Add feature') }}\n </button>\n </div>\n </mat-expansion-panel>\n <!-- Section Header -->\n <mat-expansion-panel [expanded]=\"!!mappingSectionHeader.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>{{\n getTypeIcon(mappingSectionHeader.type)\n }}</mat-icon>\n <span>{{ tx('Section header') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingSectionHeader.expr || tx('Not configured')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingSectionHeader.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of sectionHeaderTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Expression (item.key)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingSectionHeader.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n placeholder=\"item.key\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'text';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default text') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingSectionHeader.type = 'chip';\n mappingSectionHeader.chipColor = 'primary';\n mappingSectionHeader.chipVariant = 'filled';\n mappingSectionHeader.expr = '${item.key}';\n onMappingChanged()\n \"\n >\n {{ tx('Default chip') }}\n </button>\n </div>\n\n @switch (mappingSectionHeader.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingSectionHeader\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingSectionHeader\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(\n mappingSectionHeader.imageUrl\n )\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingSectionHeader.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingSectionHeader\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingSectionHeader.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n\n <!-- Empty State -->\n <mat-expansion-panel [expanded]=\"!!mappingEmptyState.expr\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <div class=\"g row-flow gap-8 ai-center\">\n <mat-icon>inbox</mat-icon>\n <span>{{ tx('Empty state') }}</span>\n </div>\n </mat-panel-title>\n <mat-panel-description>{{\n mappingEmptyState.expr || tx('Default')\n }}</mat-panel-description>\n </mat-expansion-panel-header>\n <div class=\"g gap-12\">\n <div class=\"g g-1-1 gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Type') }}</mat-label>\n <mat-select\n [(ngModel)]=\"mappingEmptyState.type\"\n (ngModelChange)=\"onMappingChanged()\"\n >\n <mat-option\n *ngFor=\"let mt of emptyStateTypeConfigs\"\n [value]=\"mt.type\"\n >\n <mat-icon class=\"option-icon\">{{ mt.icon }}</mat-icon>\n {{ tx(mt.label) }}\n </mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Message / Expr') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"mappingEmptyState.expr\"\n (ngModelChange)=\"onMappingChanged()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g row-flow gap-8\">\n <span class=\"text-caption muted\">{{ tx('Presets') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'text';\n mappingEmptyState.expr = tx('No items available');\n onMappingChanged()\n \"\n >\n {{ tx('Default message') }}\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"\n mappingEmptyState.type = 'image';\n mappingEmptyState.imageUrl = '/list-empty-state.svg';\n mappingEmptyState.imageAlt = tx('No results');\n onMappingChanged()\n \"\n >\n {{ tx('Default image') }}\n </button>\n </div>\n\n @switch (mappingEmptyState.type) {\n @case (\"text\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"html\") {\n <praxis-meta-editor-text\n [model]=\"mappingEmptyState\"\n [setPipe]=\"setPipe.bind(this)\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-text>\n }\n @case (\"chip\") {\n <praxis-meta-editor-chip\n [model]=\"mappingEmptyState\"\n [paletteOptions]=\"paletteOptions\"\n [colorDotBackground]=\"colorDotBackground.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 <mat-error\n *ngIf=\"\n isImageUrlRequiredInvalid(mappingEmptyState.imageUrl)\n \"\n >{{ tx('URL/expr is required') }}</mat-error\n >\n </mat-form-field>\n </div>\n <div\n class=\"text-caption muted\"\n *ngIf=\"!mappingEmptyState.imageUrl\"\n >\n {{ tx('Set the URL/expr to render the image.') }}\n </div>\n <praxis-meta-editor-image\n [model]=\"mappingEmptyState\"\n (change)=\"onMappingChanged()\"\n ></praxis-meta-editor-image>\n }\n }\n\n <mat-expansion-panel class=\"mat-elevation-z0 advanced-panel\">\n <mat-expansion-panel-header\n ><mat-panel-title>{{ tx('Style') }}</mat-panel-title\n ></mat-expansion-panel-header\n >\n <div class=\"g gap-12 pt-12\">\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Class') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.class\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n <mat-form-field appearance=\"outline\"\n ><mat-label>{{ tx('Style') }}</mat-label\n ><input\n matInput\n [(ngModel)]=\"mappingEmptyState.style\"\n (ngModelChange)=\"onMappingChanged()\"\n /></mat-form-field>\n </div>\n </mat-expansion-panel>\n </div>\n </mat-expansion-panel>\n </mat-accordion>\n\n <button mat-flat-button color=\"primary\" (click)=\"applyTemplate()\">\n {{ tx('Apply mapping') }}\n </button>\n <button\n mat-button\n (click)=\"inferFromFields()\"\n [disabled]=\"!fields.length\"\n >\n {{ tx('Infer from schema') }}\n </button>\n <div class=\"g g-auto-220 gap-12 ai-end mt-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Skeleton count') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"0\"\n [(ngModel)]=\"skeletonCountInput\"\n (ngModelChange)=\"onSkeletonChanged($event)\"\n />\n </mat-form-field>\n </div>\n\n <div class=\"g gap-12 mt-12\">\n <div class=\"g row-flow gap-8 ai-center\">\n <span class=\"section-title mat-subtitle-1\">{{ tx('Theme preview') }}</span>\n <mat-button-toggle-group\n [(ngModel)]=\"skinPreviewTheme\"\n (change)=\"onSkinChanged()\"\n appearance=\"legacy\"\n >\n <mat-button-toggle [value]=\"'light'\">{{ tx('Light') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'dark'\">{{ tx('Dark') }}</mat-button-toggle>\n <mat-button-toggle [value]=\"'grid'\">{{ tx('Grid') }}</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n <div class=\"skin-preview-wrap\">\n <praxis-list-skin-preview\n [config]=\"working\"\n [items]=\"previewData\"\n [theme]=\"skinPreviewTheme\"\n ></praxis-list-skin-preview>\n </div>\n </div>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('i18n/A11y')\">\n <ng-template matTabContent>\n <div\n class=\"editor-content grid gap-3\"\n *ngIf=\"working?.a11y && working?.events\"\n >\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default locale') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.locale\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"tx('e.g.: en-US')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Default currency') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.i18n.currency\"\n (ngModelChange)=\"markDirty()\"\n [placeholder]=\"currencyPlaceholder()\"\n />\n </mat-form-field>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Accessibility') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-label') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabel\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('aria-labelledby') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.a11y!.ariaLabelledBy\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.highContrast\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('High contrast') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n [(ngModel)]=\"working!.a11y!.reduceMotion\"\n (ngModelChange)=\"markDirty()\"\n >{{ tx('Reduce motion') }}</mat-slide-toggle\n >\n </div>\n <mat-divider class=\"my-8\"></mat-divider>\n <div class=\"subtitle\">{{ tx('Events') }}</div>\n <div class=\"g g-auto-220 gap-12 ai-end\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('itemClick') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.itemClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>actionClick</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.actionClick\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>selectionChange</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.selectionChange\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>loaded</mat-label>\n <input\n matInput\n [(ngModel)]=\"working!.events!.loaded\"\n (ngModelChange)=\"markDirty()\"\n />\n </mat-form-field>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Selection')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Mode') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.mode\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"none\">{{ tx('No selection') }}</mat-option>\n <mat-option value=\"single\">{{ tx('Single') }}</mat-option>\n <mat-option value=\"multiple\">{{ tx('Multiple') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form name') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlName\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlName\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Form path') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.formControlPath\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n matTooltip=\"formControlPath\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Compare by (field)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.selection.compareBy\"\n (ngModelChange)=\"onSelectionChanged()\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n class=\"help-icon-button\"\n [matTooltip]=\"tx('Unique item key.')\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Return') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.selection.return\"\n (ngModelChange)=\"onSelectionChanged()\"\n >\n <mat-option value=\"value\">{{ tx('Value') }}</mat-option>\n <mat-option value=\"item\">{{ tx('Item') }}</mat-option>\n <mat-option value=\"id\">{{ tx('ID') }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </ng-template>\n </mat-tab>\n <mat-tab [label]=\"tx('Appearance')\">\n <ng-template matTabContent>\n <div class=\"editor-content grid gap-3\">\n <div class=\"preset-row g row-flow gap-8\">\n <button mat-button (click)=\"applySkinPreset('pill-soft')\">\n {{ tx('Pill Soft') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('gradient-tile')\">\n {{ tx('Gradient Tile') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('glass')\">\n {{ tx('Glass') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('elevated')\">\n {{ tx('Elevated') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('outline')\">\n {{ tx('Outline') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('flat')\">\n {{ tx('Flat') }}\n </button>\n <button mat-button (click)=\"applySkinPreset('neumorphism')\">\n {{ tx('Neumorphism') }}\n </button>\n </div>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Style') }}</mat-label>\n <mat-select\n [(ngModel)]=\"working.skin.type\"\n (ngModelChange)=\"onSkinTypeChanged($event)\"\n >\n <mat-option value=\"pill-soft\">{{ tx('Pill Soft') }}</mat-option>\n <mat-option value=\"gradient-tile\">{{ tx('Gradient Tile') }}</mat-option>\n <mat-option value=\"glass\">{{ tx('Glass') }}</mat-option>\n <mat-option value=\"elevated\">{{ tx('Elevated') }}</mat-option>\n <mat-option value=\"outline\">{{ tx('Outline') }}</mat-option>\n <mat-option value=\"flat\">{{ tx('Flat') }}</mat-option>\n <mat-option value=\"neumorphism\">{{ tx('Neumorphism') }}</mat-option>\n <mat-option value=\"custom\">{{ tx('Custom') }}</mat-option>\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Radius') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.radius\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 1.25rem')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Shadow') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.shadow\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: var(--md-sys-elevation-level2)')\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Border') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.border\"\n (ngModelChange)=\"onSkinChanged()\"\n />\n </mat-form-field>\n <mat-form-field\n *ngIf=\"working.skin.type === 'glass'\"\n appearance=\"outline\"\n >\n <mat-label>{{ tx('Blur') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.backdropBlur\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: 8px')\"\n />\n </mat-form-field>\n <div *ngIf=\"working.skin.type === 'gradient-tile'\" class=\"g gap-12\">\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient from') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.from || ''\"\n (ngModelChange)=\"onSkinGradientChanged('from', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Gradient to') }}</mat-label>\n <input\n matInput\n [ngModel]=\"working.skin.gradient.to || ''\"\n (ngModelChange)=\"onSkinGradientChanged('to', $event)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Angle') }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"working.skin.gradient.angle ?? 135\"\n (ngModelChange)=\"onSkinGradientChanged('angle', $event)\"\n />\n </mat-form-field>\n </div>\n\n <mat-form-field appearance=\"outline\">\n <mat-label>{{ tx('Extra CSS class (skin.class)') }}</mat-label>\n <input\n matInput\n [(ngModel)]=\"working.skin.class\"\n (ngModelChange)=\"onSkinChanged()\"\n [placeholder]=\"tx('e.g.: my-list-skin')\"\n />\n </mat-form-field>\n\n <div\n *ngIf=\"working.skin.type === 'custom'\"\n class=\"g g-auto-220 gap-12 ai-end\"\n >\n <mat-form-field appearance=\"outline\" class=\"w-full\">\n <mat-label>{{ tx('Inline style (skin.inlineStyle)') }}</mat-label>\n <textarea\n matInput\n rows=\"4\"\n [(ngModel)]=\"working.skin.inlineStyle\"\n (ngModelChange)=\"onSkinChanged()\"\n [attr.placeholder]=\"':host{--p-list-radius: 1rem}'\"\n ></textarea>\n </mat-form-field>\n <div class=\"text-caption\">\n {{ tx('CSS class example (add this to your global styles):') }}\n <pre class=\"code-block\">\n.my-list-skin .item-card {\n border-radius: 14px;\n border: 1px solid var(--md-sys-color-outline-variant);\n box-shadow: var(--md-sys-elevation-level2);\n}\n.my-list-skin .mat-mdc-list-item .list-item-content {\n backdrop-filter: blur(6px);\n}</pre\n >\n </div>\n </div>\n </div>\n </ng-template>\n </mat-tab>\n</mat-tab-group>\n", styles: [".confirm-type{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;line-height:16px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.confirm-type.danger{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.confirm-type.warning{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.confirm-type.info{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}:host{display:block;color:var(--md-sys-color-on-surface)}.list-editor-tabs{--editor-surface: var(--md-sys-color-surface-container-lowest);--editor-border: 1px solid var(--md-sys-color-outline-variant);--editor-radius: var(--md-sys-shape-corner-large, 16px);--editor-muted: var(--md-sys-color-on-surface-variant);--editor-accent: var(--md-sys-color-primary)}.editor-content{padding:16px;background:var(--editor-surface);border:var(--editor-border);border-radius:var(--editor-radius);display:grid;gap:12px}.editor-content .mat-mdc-form-field{width:100%;max-width:none;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var( --md-sys-color-outline-variant );--mdc-outlined-text-field-hover-outline-color: var( --md-sys-color-outline );--mdc-outlined-text-field-focus-outline-color: var( --md-sys-color-primary );--mdc-outlined-text-field-error-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-error-focus-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-error-hover-outline-color: var( --md-sys-color-error );--mdc-outlined-text-field-label-text-color: var( --md-sys-color-on-surface-variant );--mdc-outlined-text-field-input-text-color: var( --md-sys-color-on-surface );--mdc-outlined-text-field-supporting-text-color: var( --md-sys-color-on-surface-variant )}.editor-content .mat-mdc-form-field.w-full{max-width:none}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.editor-split{grid-template-columns:minmax(0,1fr);align-items:start}.editor-main,.editor-aside{display:grid;gap:12px}.skin-preview-wrap{border-radius:calc(var(--editor-radius) - 4px);border:var(--editor-border);background:var(--md-sys-color-surface-container);padding:12px}.g{display:grid}.g-auto-220{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.g-auto-200{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.g-1-auto{grid-template-columns:1fr auto}.row-flow{grid-auto-flow:column}.gap-6{gap:6px}.gap-8{gap:8px}.gap-12{gap:12px}.ai-center{align-items:center}.ai-end{align-items:end}.mt-12{margin-top:12px}.mb-8{margin-bottom:8px}.mb-6{margin-bottom:6px}.my-8{margin:8px 0}.subtitle{margin:8px 0 4px;color:var(--editor-muted);font-weight:500}.section-title{color:var(--editor-muted);font-weight:600}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--md-sys-color-error);font-size:.85rem}.muted{color:var(--editor-muted)}.text-caption{color:var(--editor-muted);font-size:.8rem}:host ::ng-deep .mat-mdc-select-panel .option-icon{font-size:18px;margin-right:6px;vertical-align:middle}:host ::ng-deep .mat-mdc-select-panel .color-dot{width:10px;height:10px;border-radius:999px;display:inline-block;margin-right:6px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-outline)}:host ::ng-deep .mat-mdc-select-panel .color-primary{background:var(--md-sys-color-primary)}:host ::ng-deep .mat-mdc-select-panel .color-accent{background:var(--md-sys-color-tertiary)}:host ::ng-deep .mat-mdc-select-panel .color-warn{background:var(--md-sys-color-error)}:host ::ng-deep .mat-mdc-select-panel .color-default{background:var(--md-sys-color-outline)}@media(max-width:1024px){.editor-split{grid-template-columns:minmax(0,1fr)}}\n"] }]
|
|
5919
5997
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
5920
5998
|
type: Optional
|
|
5921
5999
|
}, {
|
|
@@ -5938,6 +6016,25 @@ const ENUMS = {
|
|
|
5938
6016
|
layoutLines: [1, 2, 3],
|
|
5939
6017
|
layoutDividers: ['none', 'between', 'all'],
|
|
5940
6018
|
layoutModel: ['standard', 'media', 'hotel'],
|
|
6019
|
+
rowLayoutType: ['grid', 'flex'],
|
|
6020
|
+
rowLayoutSlot: [
|
|
6021
|
+
'leading',
|
|
6022
|
+
'primary',
|
|
6023
|
+
'secondary',
|
|
6024
|
+
'meta',
|
|
6025
|
+
'trailing',
|
|
6026
|
+
'identity',
|
|
6027
|
+
'balance',
|
|
6028
|
+
'limit',
|
|
6029
|
+
'risk',
|
|
6030
|
+
'alerts',
|
|
6031
|
+
'owner',
|
|
6032
|
+
'actions',
|
|
6033
|
+
'expand',
|
|
6034
|
+
],
|
|
6035
|
+
rowLayoutAlign: ['start', 'center', 'end', 'stretch'],
|
|
6036
|
+
rowLayoutColumnAlign: ['start', 'center', 'end'],
|
|
6037
|
+
rowLayoutJustify: ['start', 'center', 'end', 'stretch'],
|
|
5941
6038
|
selectionMode: ['none', 'single', 'multiple'],
|
|
5942
6039
|
selectionReturn: ['value', 'item', 'id'],
|
|
5943
6040
|
skinType: [
|
|
@@ -5960,6 +6057,9 @@ const ENUMS = {
|
|
|
5960
6057
|
'date',
|
|
5961
6058
|
'html',
|
|
5962
6059
|
'slot',
|
|
6060
|
+
'metric',
|
|
6061
|
+
'compose',
|
|
6062
|
+
'component',
|
|
5963
6063
|
],
|
|
5964
6064
|
templateVariant: ['filled', 'outlined'],
|
|
5965
6065
|
metaPlacement: ['side', 'line'],
|
|
@@ -5968,6 +6068,23 @@ const ENUMS = {
|
|
|
5968
6068
|
actionKind: ['icon', 'button'],
|
|
5969
6069
|
actionButtonVariant: ['stroked', 'raised', 'flat'],
|
|
5970
6070
|
actionEmitPayload: ['item', 'id', 'value'],
|
|
6071
|
+
actionPlacement: ['actions', 'trailing'],
|
|
6072
|
+
confirmationType: ['danger', 'warning', 'info'],
|
|
6073
|
+
expandTrigger: ['row', 'icon', 'row+icon'],
|
|
6074
|
+
expandMode: ['single', 'multiple'],
|
|
6075
|
+
expandPlacement: ['expand', 'trailing'],
|
|
6076
|
+
expansionSectionType: ['info-list', 'chip-list', 'timeline', 'key-value', 'metadata', 'component'],
|
|
6077
|
+
expansionDataSourceMode: ['inline', 'resource', 'resourcePath'],
|
|
6078
|
+
expansionFallbackMode: ['none', 'inline', 'resource'],
|
|
6079
|
+
expansionRenderShell: ['attached', 'detached', 'modal'],
|
|
6080
|
+
expansionColumns: [1, 2, 3],
|
|
6081
|
+
metricLayout: ['value-only', 'value+caption', 'icon+value+caption', 'stacked-center'],
|
|
6082
|
+
metricAlign: ['start', 'center', 'end'],
|
|
6083
|
+
metricCaptionPosition: ['below-value', 'below-progress'],
|
|
6084
|
+
metricProgressMode: ['determinate', 'indeterminate'],
|
|
6085
|
+
metricIconPosition: ['left', 'right', 'top'],
|
|
6086
|
+
composeDirection: ['row', 'column'],
|
|
6087
|
+
composeOrientation: ['horizontal', 'vertical'],
|
|
5971
6088
|
};
|
|
5972
6089
|
function templateCaps(slot, label) {
|
|
5973
6090
|
const base = `templating.${slot}`;
|
|
@@ -6079,6 +6196,80 @@ function templateCaps(slot, label) {
|
|
|
6079
6196
|
valueKind: 'string',
|
|
6080
6197
|
description: `Rating color (theme/M3/custom).`,
|
|
6081
6198
|
},
|
|
6199
|
+
{
|
|
6200
|
+
path: `${base}.props.metric`,
|
|
6201
|
+
category: 'templating',
|
|
6202
|
+
valueKind: 'object',
|
|
6203
|
+
description: `Metric props for ${label} (type=metric).`,
|
|
6204
|
+
},
|
|
6205
|
+
{
|
|
6206
|
+
path: `${base}.props.metric.valueExpr`,
|
|
6207
|
+
category: 'templating',
|
|
6208
|
+
valueKind: 'expression',
|
|
6209
|
+
description: `Metric value expression for ${label}.`,
|
|
6210
|
+
},
|
|
6211
|
+
{
|
|
6212
|
+
path: `${base}.props.metric.caption`,
|
|
6213
|
+
category: 'templating',
|
|
6214
|
+
valueKind: 'string',
|
|
6215
|
+
description: `Metric caption for ${label}.`,
|
|
6216
|
+
},
|
|
6217
|
+
{
|
|
6218
|
+
path: `${base}.props.metric.layout`,
|
|
6219
|
+
category: 'templating',
|
|
6220
|
+
valueKind: 'enum',
|
|
6221
|
+
allowedValues: ENUMS.metricLayout,
|
|
6222
|
+
description: `Metric layout for ${label}.`,
|
|
6223
|
+
},
|
|
6224
|
+
{
|
|
6225
|
+
path: `${base}.props.metric.progress`,
|
|
6226
|
+
category: 'templating',
|
|
6227
|
+
valueKind: 'object',
|
|
6228
|
+
description: `Metric progress config for ${label}.`,
|
|
6229
|
+
},
|
|
6230
|
+
{
|
|
6231
|
+
path: `${base}.props.metric.progress.valueExpr`,
|
|
6232
|
+
category: 'templating',
|
|
6233
|
+
valueKind: 'expression',
|
|
6234
|
+
description: `Metric progress value expression for ${label}.`,
|
|
6235
|
+
},
|
|
6236
|
+
{
|
|
6237
|
+
path: `${base}.props.compose`,
|
|
6238
|
+
category: 'templating',
|
|
6239
|
+
valueKind: 'object',
|
|
6240
|
+
description: `Compose props for ${label} (type=compose).`,
|
|
6241
|
+
},
|
|
6242
|
+
{
|
|
6243
|
+
path: `${base}.props.compose.items`,
|
|
6244
|
+
category: 'templating',
|
|
6245
|
+
valueKind: 'array',
|
|
6246
|
+
description: `Compose child items for ${label}.`,
|
|
6247
|
+
},
|
|
6248
|
+
{
|
|
6249
|
+
path: `${base}.props.compose.direction`,
|
|
6250
|
+
category: 'templating',
|
|
6251
|
+
valueKind: 'enum',
|
|
6252
|
+
allowedValues: ENUMS.composeDirection,
|
|
6253
|
+
description: `Compose direction for ${label}.`,
|
|
6254
|
+
},
|
|
6255
|
+
{
|
|
6256
|
+
path: `${base}.props.component`,
|
|
6257
|
+
category: 'templating',
|
|
6258
|
+
valueKind: 'object',
|
|
6259
|
+
description: `Runtime component props for ${label} (type=component).`,
|
|
6260
|
+
},
|
|
6261
|
+
{
|
|
6262
|
+
path: `${base}.props.component.id`,
|
|
6263
|
+
category: 'templating',
|
|
6264
|
+
valueKind: 'string',
|
|
6265
|
+
description: `Runtime component id for ${label}.`,
|
|
6266
|
+
},
|
|
6267
|
+
{
|
|
6268
|
+
path: `${base}.props.component.inputs`,
|
|
6269
|
+
category: 'templating',
|
|
6270
|
+
valueKind: 'object',
|
|
6271
|
+
description: `Runtime component input map for ${label}.`,
|
|
6272
|
+
},
|
|
6082
6273
|
];
|
|
6083
6274
|
}
|
|
6084
6275
|
const LIST_AI_CAPABILITIES = {
|
|
@@ -6215,6 +6406,108 @@ const LIST_AI_CAPABILITIES = {
|
|
|
6215
6406
|
valueKind: 'number',
|
|
6216
6407
|
description: 'Remote page size.',
|
|
6217
6408
|
},
|
|
6409
|
+
{
|
|
6410
|
+
path: 'layout.rowLayout',
|
|
6411
|
+
category: 'layout',
|
|
6412
|
+
valueKind: 'object',
|
|
6413
|
+
description: 'Row layout grid/flex configuration for list variant.',
|
|
6414
|
+
},
|
|
6415
|
+
{
|
|
6416
|
+
path: 'layout.rowLayout.type',
|
|
6417
|
+
category: 'layout',
|
|
6418
|
+
valueKind: 'enum',
|
|
6419
|
+
allowedValues: ENUMS.rowLayoutType,
|
|
6420
|
+
description: 'Row layout rendering type.',
|
|
6421
|
+
},
|
|
6422
|
+
{
|
|
6423
|
+
path: 'layout.rowLayout.columns',
|
|
6424
|
+
category: 'layout',
|
|
6425
|
+
valueKind: 'array',
|
|
6426
|
+
description: 'Stable row layout columns.',
|
|
6427
|
+
},
|
|
6428
|
+
{
|
|
6429
|
+
path: 'layout.rowLayout.columns[].slot',
|
|
6430
|
+
category: 'layout',
|
|
6431
|
+
valueKind: 'enum',
|
|
6432
|
+
allowedValues: ENUMS.rowLayoutSlot,
|
|
6433
|
+
description: 'Runtime-supported row layout slot.',
|
|
6434
|
+
},
|
|
6435
|
+
{
|
|
6436
|
+
path: 'layout.rowLayout.columns[].width',
|
|
6437
|
+
category: 'layout',
|
|
6438
|
+
valueKind: 'string',
|
|
6439
|
+
description: 'CSS width for a row layout column.',
|
|
6440
|
+
},
|
|
6441
|
+
{
|
|
6442
|
+
path: 'layout.rowLayout.columns[].minWidth',
|
|
6443
|
+
category: 'layout',
|
|
6444
|
+
valueKind: 'string',
|
|
6445
|
+
description: 'Minimum CSS width for a row layout column.',
|
|
6446
|
+
},
|
|
6447
|
+
{
|
|
6448
|
+
path: 'layout.rowLayout.columns[].maxWidth',
|
|
6449
|
+
category: 'layout',
|
|
6450
|
+
valueKind: 'string',
|
|
6451
|
+
description: 'Maximum CSS width for a row layout column.',
|
|
6452
|
+
},
|
|
6453
|
+
{
|
|
6454
|
+
path: 'layout.rowLayout.columns[].align',
|
|
6455
|
+
category: 'layout',
|
|
6456
|
+
valueKind: 'enum',
|
|
6457
|
+
allowedValues: ENUMS.rowLayoutColumnAlign,
|
|
6458
|
+
description: 'Column content alignment.',
|
|
6459
|
+
},
|
|
6460
|
+
{
|
|
6461
|
+
path: 'layout.rowLayout.columns[].justify',
|
|
6462
|
+
category: 'layout',
|
|
6463
|
+
valueKind: 'enum',
|
|
6464
|
+
allowedValues: ENUMS.rowLayoutJustify,
|
|
6465
|
+
description: 'Column grid justification.',
|
|
6466
|
+
},
|
|
6467
|
+
{
|
|
6468
|
+
path: 'layout.rowLayout.columns[].class',
|
|
6469
|
+
category: 'layout',
|
|
6470
|
+
valueKind: 'string',
|
|
6471
|
+
description: 'CSS class for a row layout column.',
|
|
6472
|
+
},
|
|
6473
|
+
{
|
|
6474
|
+
path: 'layout.rowLayout.columns[].style',
|
|
6475
|
+
category: 'layout',
|
|
6476
|
+
valueKind: 'string',
|
|
6477
|
+
description: 'Inline style for a row layout column.',
|
|
6478
|
+
},
|
|
6479
|
+
{
|
|
6480
|
+
path: 'layout.rowLayout.gap',
|
|
6481
|
+
category: 'layout',
|
|
6482
|
+
valueKind: 'string',
|
|
6483
|
+
description: 'Gap between row layout columns.',
|
|
6484
|
+
},
|
|
6485
|
+
{
|
|
6486
|
+
path: 'layout.rowLayout.align',
|
|
6487
|
+
category: 'layout',
|
|
6488
|
+
valueKind: 'enum',
|
|
6489
|
+
allowedValues: ENUMS.rowLayoutAlign,
|
|
6490
|
+
description: 'Vertical alignment for the row layout.',
|
|
6491
|
+
},
|
|
6492
|
+
{
|
|
6493
|
+
path: 'layout.rowLayout.itemAlignY',
|
|
6494
|
+
category: 'layout',
|
|
6495
|
+
valueKind: 'enum',
|
|
6496
|
+
allowedValues: ENUMS.rowLayoutAlign,
|
|
6497
|
+
description: 'Alias for row vertical alignment.',
|
|
6498
|
+
},
|
|
6499
|
+
{
|
|
6500
|
+
path: 'layout.rowLayout.class',
|
|
6501
|
+
category: 'layout',
|
|
6502
|
+
valueKind: 'string',
|
|
6503
|
+
description: 'CSS class for row layout content.',
|
|
6504
|
+
},
|
|
6505
|
+
{
|
|
6506
|
+
path: 'layout.rowLayout.style',
|
|
6507
|
+
category: 'layout',
|
|
6508
|
+
valueKind: 'string',
|
|
6509
|
+
description: 'Inline style for row layout content.',
|
|
6510
|
+
},
|
|
6218
6511
|
{
|
|
6219
6512
|
path: 'skin',
|
|
6220
6513
|
category: 'skin',
|
|
@@ -6326,6 +6619,39 @@ const LIST_AI_CAPABILITIES = {
|
|
|
6326
6619
|
allowedValues: ENUMS.selectionReturn,
|
|
6327
6620
|
description: 'Selection return payload.',
|
|
6328
6621
|
},
|
|
6622
|
+
{
|
|
6623
|
+
path: 'interaction',
|
|
6624
|
+
category: 'interaction',
|
|
6625
|
+
valueKind: 'object',
|
|
6626
|
+
description: 'Expandable row interaction settings.',
|
|
6627
|
+
},
|
|
6628
|
+
{
|
|
6629
|
+
path: 'interaction.expandable',
|
|
6630
|
+
category: 'interaction',
|
|
6631
|
+
valueKind: 'boolean',
|
|
6632
|
+
description: 'Enable inline row expansion.',
|
|
6633
|
+
},
|
|
6634
|
+
{
|
|
6635
|
+
path: 'interaction.expandTrigger',
|
|
6636
|
+
category: 'interaction',
|
|
6637
|
+
valueKind: 'enum',
|
|
6638
|
+
allowedValues: ENUMS.expandTrigger,
|
|
6639
|
+
description: 'How expansion is triggered.',
|
|
6640
|
+
},
|
|
6641
|
+
{
|
|
6642
|
+
path: 'interaction.expandMode',
|
|
6643
|
+
category: 'interaction',
|
|
6644
|
+
valueKind: 'enum',
|
|
6645
|
+
allowedValues: ENUMS.expandMode,
|
|
6646
|
+
description: 'Whether one or many rows may be expanded.',
|
|
6647
|
+
},
|
|
6648
|
+
{
|
|
6649
|
+
path: 'interaction.expandPlacement',
|
|
6650
|
+
category: 'interaction',
|
|
6651
|
+
valueKind: 'enum',
|
|
6652
|
+
allowedValues: ENUMS.expandPlacement,
|
|
6653
|
+
description: 'Where the expansion control is rendered.',
|
|
6654
|
+
},
|
|
6329
6655
|
{
|
|
6330
6656
|
path: 'templating',
|
|
6331
6657
|
category: 'templating',
|
|
@@ -6337,6 +6663,12 @@ const LIST_AI_CAPABILITIES = {
|
|
|
6337
6663
|
...templateCaps('secondary', 'secondary'),
|
|
6338
6664
|
...templateCaps('meta', 'meta'),
|
|
6339
6665
|
...templateCaps('trailing', 'trailing'),
|
|
6666
|
+
...templateCaps('identity', 'identity'),
|
|
6667
|
+
...templateCaps('balance', 'balance'),
|
|
6668
|
+
...templateCaps('limit', 'limit'),
|
|
6669
|
+
...templateCaps('risk', 'risk'),
|
|
6670
|
+
...templateCaps('alerts', 'alerts'),
|
|
6671
|
+
...templateCaps('owner', 'owner'),
|
|
6340
6672
|
...templateCaps('sectionHeader', 'section header'),
|
|
6341
6673
|
...templateCaps('emptyState', 'empty state'),
|
|
6342
6674
|
{
|
|
@@ -6438,6 +6770,12 @@ const LIST_AI_CAPABILITIES = {
|
|
|
6438
6770
|
valueKind: 'array',
|
|
6439
6771
|
description: 'Item actions array.',
|
|
6440
6772
|
},
|
|
6773
|
+
{
|
|
6774
|
+
path: 'actions[]',
|
|
6775
|
+
category: 'actions',
|
|
6776
|
+
valueKind: 'object',
|
|
6777
|
+
description: 'Single item action entry.',
|
|
6778
|
+
},
|
|
6441
6779
|
{
|
|
6442
6780
|
path: 'actions[].id',
|
|
6443
6781
|
category: 'actions',
|
|
@@ -6490,70 +6828,469 @@ const LIST_AI_CAPABILITIES = {
|
|
|
6490
6828
|
description: 'Action emitted payload.',
|
|
6491
6829
|
},
|
|
6492
6830
|
{
|
|
6493
|
-
path: 'actions[].
|
|
6831
|
+
path: 'actions[].globalAction.[actionId]',
|
|
6494
6832
|
category: 'actions',
|
|
6495
6833
|
valueKind: 'string',
|
|
6496
|
-
description: '
|
|
6834
|
+
description: 'Canonical global app action id (ex.: "toast.success").',
|
|
6497
6835
|
},
|
|
6498
6836
|
{
|
|
6499
|
-
path: 'actions[].
|
|
6837
|
+
path: 'actions[].globalAction.payload',
|
|
6500
6838
|
category: 'actions',
|
|
6501
6839
|
valueKind: 'object',
|
|
6502
|
-
description: '
|
|
6840
|
+
description: 'Structured payload for the global app action (JSON/template).',
|
|
6503
6841
|
},
|
|
6504
6842
|
{
|
|
6505
6843
|
path: 'actions[].emitLocal',
|
|
6506
6844
|
category: 'actions',
|
|
6507
6845
|
valueKind: 'boolean',
|
|
6508
|
-
description: 'Emit local actionClick even when
|
|
6846
|
+
description: 'Emit local actionClick even when globalAction is set.',
|
|
6509
6847
|
},
|
|
6510
6848
|
{
|
|
6511
|
-
path: '
|
|
6512
|
-
category: '
|
|
6513
|
-
valueKind: '
|
|
6514
|
-
description: '
|
|
6849
|
+
path: 'actions[].showLoading',
|
|
6850
|
+
category: 'actions',
|
|
6851
|
+
valueKind: 'boolean',
|
|
6852
|
+
description: 'Show loading state while a global action executes.',
|
|
6515
6853
|
},
|
|
6516
6854
|
{
|
|
6517
|
-
path: '
|
|
6518
|
-
category: '
|
|
6519
|
-
valueKind: '
|
|
6520
|
-
|
|
6855
|
+
path: 'actions[].placement',
|
|
6856
|
+
category: 'actions',
|
|
6857
|
+
valueKind: 'enum',
|
|
6858
|
+
allowedValues: ENUMS.actionPlacement,
|
|
6859
|
+
description: 'Action placement in row layout actions or trailing slot.',
|
|
6521
6860
|
},
|
|
6522
6861
|
{
|
|
6523
|
-
path: '
|
|
6524
|
-
category: '
|
|
6862
|
+
path: 'actions[].confirmation',
|
|
6863
|
+
category: 'actions',
|
|
6864
|
+
valueKind: 'object',
|
|
6865
|
+
description: 'Confirmation prompt before executing the action.',
|
|
6866
|
+
},
|
|
6867
|
+
{
|
|
6868
|
+
path: 'actions[].confirmation.title',
|
|
6869
|
+
category: 'actions',
|
|
6525
6870
|
valueKind: 'string',
|
|
6526
|
-
description: '
|
|
6871
|
+
description: 'Confirmation title.',
|
|
6527
6872
|
},
|
|
6528
6873
|
{
|
|
6529
|
-
path: '
|
|
6530
|
-
category: '
|
|
6874
|
+
path: 'actions[].confirmation.message',
|
|
6875
|
+
category: 'actions',
|
|
6531
6876
|
valueKind: 'string',
|
|
6532
|
-
description: '
|
|
6877
|
+
description: 'Confirmation message.',
|
|
6533
6878
|
},
|
|
6534
6879
|
{
|
|
6535
|
-
path: '
|
|
6536
|
-
category: '
|
|
6537
|
-
valueKind: '
|
|
6538
|
-
|
|
6880
|
+
path: 'actions[].confirmation.type',
|
|
6881
|
+
category: 'actions',
|
|
6882
|
+
valueKind: 'enum',
|
|
6883
|
+
allowedValues: ENUMS.confirmationType,
|
|
6884
|
+
description: 'Confirmation severity.',
|
|
6539
6885
|
},
|
|
6540
6886
|
{
|
|
6541
|
-
path: '
|
|
6542
|
-
category: '
|
|
6543
|
-
valueKind: '
|
|
6544
|
-
description: '
|
|
6887
|
+
path: 'rules',
|
|
6888
|
+
category: 'rules',
|
|
6889
|
+
valueKind: 'object',
|
|
6890
|
+
description: 'Conditional list item styling and slot override rules.',
|
|
6545
6891
|
},
|
|
6546
6892
|
{
|
|
6547
|
-
path: '
|
|
6548
|
-
category: '
|
|
6549
|
-
valueKind: '
|
|
6550
|
-
description: '
|
|
6893
|
+
path: 'rules.itemStyles',
|
|
6894
|
+
category: 'rules',
|
|
6895
|
+
valueKind: 'array',
|
|
6896
|
+
description: 'Conditional item style rules.',
|
|
6551
6897
|
},
|
|
6552
6898
|
{
|
|
6553
|
-
path: '
|
|
6554
|
-
category: '
|
|
6899
|
+
path: 'rules.itemStyles[].id',
|
|
6900
|
+
category: 'rules',
|
|
6555
6901
|
valueKind: 'string',
|
|
6556
|
-
description: '
|
|
6902
|
+
description: 'Stable item style rule id.',
|
|
6903
|
+
},
|
|
6904
|
+
{
|
|
6905
|
+
path: 'rules.itemStyles[].condition',
|
|
6906
|
+
category: 'rules',
|
|
6907
|
+
valueKind: 'expression',
|
|
6908
|
+
description: 'Json Logic condition in row context.',
|
|
6909
|
+
},
|
|
6910
|
+
{
|
|
6911
|
+
path: 'rules.itemStyles[].class',
|
|
6912
|
+
category: 'rules',
|
|
6913
|
+
valueKind: 'string',
|
|
6914
|
+
description: 'CSS class applied when the rule matches.',
|
|
6915
|
+
},
|
|
6916
|
+
{
|
|
6917
|
+
path: 'rules.itemStyles[].style',
|
|
6918
|
+
category: 'rules',
|
|
6919
|
+
valueKind: 'string',
|
|
6920
|
+
description: 'Inline style applied when the rule matches.',
|
|
6921
|
+
},
|
|
6922
|
+
{
|
|
6923
|
+
path: 'rules.itemStyles[].border',
|
|
6924
|
+
category: 'rules',
|
|
6925
|
+
valueKind: 'string',
|
|
6926
|
+
description: 'Border style override applied when the rule matches.',
|
|
6927
|
+
},
|
|
6928
|
+
{
|
|
6929
|
+
path: 'rules.itemStyles[].background',
|
|
6930
|
+
category: 'rules',
|
|
6931
|
+
valueKind: 'string',
|
|
6932
|
+
description: 'Background style override applied when the rule matches.',
|
|
6933
|
+
},
|
|
6934
|
+
{
|
|
6935
|
+
path: 'rules.slotOverrides',
|
|
6936
|
+
category: 'rules',
|
|
6937
|
+
valueKind: 'array',
|
|
6938
|
+
description: 'Conditional slot template override rules.',
|
|
6939
|
+
},
|
|
6940
|
+
{
|
|
6941
|
+
path: 'rules.slotOverrides[].id',
|
|
6942
|
+
category: 'rules',
|
|
6943
|
+
valueKind: 'string',
|
|
6944
|
+
description: 'Stable slot override rule id.',
|
|
6945
|
+
},
|
|
6946
|
+
{
|
|
6947
|
+
path: 'rules.slotOverrides[].slot',
|
|
6948
|
+
category: 'rules',
|
|
6949
|
+
valueKind: 'enum',
|
|
6950
|
+
allowedValues: ENUMS.rowLayoutSlot.filter((slot) => slot !== 'actions' && slot !== 'expand'),
|
|
6951
|
+
description: 'Templating slot affected by the override.',
|
|
6952
|
+
},
|
|
6953
|
+
{
|
|
6954
|
+
path: 'rules.slotOverrides[].condition',
|
|
6955
|
+
category: 'rules',
|
|
6956
|
+
valueKind: 'expression',
|
|
6957
|
+
description: 'Json Logic condition in row context.',
|
|
6958
|
+
},
|
|
6959
|
+
{
|
|
6960
|
+
path: 'rules.slotOverrides[].template',
|
|
6961
|
+
category: 'rules',
|
|
6962
|
+
valueKind: 'object',
|
|
6963
|
+
description: 'Template override applied when the rule matches.',
|
|
6964
|
+
},
|
|
6965
|
+
{
|
|
6966
|
+
path: 'rules.slotOverrides[].class',
|
|
6967
|
+
category: 'rules',
|
|
6968
|
+
valueKind: 'string',
|
|
6969
|
+
description: 'CSS class appended when the rule matches.',
|
|
6970
|
+
},
|
|
6971
|
+
{
|
|
6972
|
+
path: 'rules.slotOverrides[].style',
|
|
6973
|
+
category: 'rules',
|
|
6974
|
+
valueKind: 'string',
|
|
6975
|
+
description: 'Inline style appended when the rule matches.',
|
|
6976
|
+
},
|
|
6977
|
+
{
|
|
6978
|
+
path: 'rules.slotOverrides[].hide',
|
|
6979
|
+
category: 'rules',
|
|
6980
|
+
valueKind: 'boolean',
|
|
6981
|
+
description: 'Hide the slot when the rule matches.',
|
|
6982
|
+
},
|
|
6983
|
+
{
|
|
6984
|
+
path: 'expansion',
|
|
6985
|
+
category: 'expansion',
|
|
6986
|
+
valueKind: 'object',
|
|
6987
|
+
description: 'Inline expansion sections and remote detail contract.',
|
|
6988
|
+
},
|
|
6989
|
+
{
|
|
6990
|
+
path: 'expansion.sections',
|
|
6991
|
+
category: 'expansion',
|
|
6992
|
+
valueKind: 'array',
|
|
6993
|
+
description: 'Expansion detail sections.',
|
|
6994
|
+
},
|
|
6995
|
+
{
|
|
6996
|
+
path: 'expansion.sections[].id',
|
|
6997
|
+
category: 'expansion',
|
|
6998
|
+
valueKind: 'string',
|
|
6999
|
+
description: 'Stable expansion section id.',
|
|
7000
|
+
},
|
|
7001
|
+
{
|
|
7002
|
+
path: 'expansion.sections[].title',
|
|
7003
|
+
category: 'expansion',
|
|
7004
|
+
valueKind: 'string',
|
|
7005
|
+
description: 'Expansion section title.',
|
|
7006
|
+
},
|
|
7007
|
+
{
|
|
7008
|
+
path: 'expansion.sections[].type',
|
|
7009
|
+
category: 'expansion',
|
|
7010
|
+
valueKind: 'enum',
|
|
7011
|
+
allowedValues: ENUMS.expansionSectionType,
|
|
7012
|
+
description: 'Expansion section renderer type.',
|
|
7013
|
+
},
|
|
7014
|
+
{
|
|
7015
|
+
path: 'expansion.sections[].itemsExpr',
|
|
7016
|
+
category: 'expansion',
|
|
7017
|
+
valueKind: 'expression',
|
|
7018
|
+
description: 'Expression used to resolve section items from the row.',
|
|
7019
|
+
},
|
|
7020
|
+
{
|
|
7021
|
+
path: 'expansion.sections[].emptyLabel',
|
|
7022
|
+
category: 'expansion',
|
|
7023
|
+
valueKind: 'string',
|
|
7024
|
+
description: 'Empty label for an expansion section.',
|
|
7025
|
+
},
|
|
7026
|
+
{
|
|
7027
|
+
path: 'expansion.sections[].metadata',
|
|
7028
|
+
category: 'expansion',
|
|
7029
|
+
valueKind: 'object',
|
|
7030
|
+
description: 'Metadata rendering options for metadata expansion sections.',
|
|
7031
|
+
},
|
|
7032
|
+
{
|
|
7033
|
+
path: 'expansion.sections[].component',
|
|
7034
|
+
category: 'expansion',
|
|
7035
|
+
valueKind: 'object',
|
|
7036
|
+
description: 'Runtime component rendering options for component expansion sections.',
|
|
7037
|
+
},
|
|
7038
|
+
{
|
|
7039
|
+
path: 'expansion.sections[].class',
|
|
7040
|
+
category: 'expansion',
|
|
7041
|
+
valueKind: 'string',
|
|
7042
|
+
description: 'CSS class for an expansion section.',
|
|
7043
|
+
},
|
|
7044
|
+
{
|
|
7045
|
+
path: 'expansion.sections[].style',
|
|
7046
|
+
category: 'expansion',
|
|
7047
|
+
valueKind: 'string',
|
|
7048
|
+
description: 'Inline style for an expansion section.',
|
|
7049
|
+
},
|
|
7050
|
+
{
|
|
7051
|
+
path: 'expansion.sections[].showIf',
|
|
7052
|
+
category: 'expansion',
|
|
7053
|
+
valueKind: 'expression',
|
|
7054
|
+
description: 'Json Logic visibility condition for an expansion section.',
|
|
7055
|
+
},
|
|
7056
|
+
{
|
|
7057
|
+
path: 'expansion.dataSource',
|
|
7058
|
+
category: 'expansion',
|
|
7059
|
+
valueKind: 'object',
|
|
7060
|
+
description: 'Expansion detail data source.',
|
|
7061
|
+
},
|
|
7062
|
+
{
|
|
7063
|
+
path: 'expansion.dataSource.mode',
|
|
7064
|
+
category: 'expansion',
|
|
7065
|
+
valueKind: 'enum',
|
|
7066
|
+
allowedValues: ENUMS.expansionDataSourceMode,
|
|
7067
|
+
description: 'Expansion data source mode.',
|
|
7068
|
+
},
|
|
7069
|
+
{
|
|
7070
|
+
path: 'expansion.dataSource.resource',
|
|
7071
|
+
category: 'expansion',
|
|
7072
|
+
valueKind: 'object',
|
|
7073
|
+
description: 'Canonical resource descriptor for expansion details.',
|
|
7074
|
+
},
|
|
7075
|
+
{
|
|
7076
|
+
path: 'expansion.dataSource.resource.kind',
|
|
7077
|
+
category: 'expansion',
|
|
7078
|
+
valueKind: 'string',
|
|
7079
|
+
description: 'Canonical expansion resource kind.',
|
|
7080
|
+
},
|
|
7081
|
+
{
|
|
7082
|
+
path: 'expansion.dataSource.resource.id',
|
|
7083
|
+
category: 'expansion',
|
|
7084
|
+
valueKind: 'string',
|
|
7085
|
+
description: 'Canonical expansion resource id.',
|
|
7086
|
+
},
|
|
7087
|
+
{
|
|
7088
|
+
path: 'expansion.dataSource.resource.version',
|
|
7089
|
+
category: 'expansion',
|
|
7090
|
+
valueKind: 'string',
|
|
7091
|
+
description: 'Canonical expansion resource version.',
|
|
7092
|
+
},
|
|
7093
|
+
{
|
|
7094
|
+
path: 'expansion.dataSource.resourcePath',
|
|
7095
|
+
category: 'expansion',
|
|
7096
|
+
valueKind: 'object',
|
|
7097
|
+
description: 'Dynamic resource path descriptor for expansion details.',
|
|
7098
|
+
},
|
|
7099
|
+
{
|
|
7100
|
+
path: 'expansion.dataSource.resourcePath.path',
|
|
7101
|
+
category: 'expansion',
|
|
7102
|
+
valueKind: 'string',
|
|
7103
|
+
description: 'Expression-backed remote path for expansion details.',
|
|
7104
|
+
},
|
|
7105
|
+
{
|
|
7106
|
+
path: 'expansion.dataSource.resourcePath.method',
|
|
7107
|
+
category: 'expansion',
|
|
7108
|
+
valueKind: 'enum',
|
|
7109
|
+
allowedValues: ['GET', 'POST'],
|
|
7110
|
+
description: 'HTTP method for dynamic expansion resource path.',
|
|
7111
|
+
},
|
|
7112
|
+
{
|
|
7113
|
+
path: 'expansion.dataSource.resourcePath.paramsMap',
|
|
7114
|
+
category: 'expansion',
|
|
7115
|
+
valueKind: 'object',
|
|
7116
|
+
description: 'Parameter map for dynamic expansion resource path.',
|
|
7117
|
+
},
|
|
7118
|
+
{
|
|
7119
|
+
path: 'expansion.dataSource.resourceAllowList',
|
|
7120
|
+
category: 'expansion',
|
|
7121
|
+
valueKind: 'array',
|
|
7122
|
+
description: 'Allowlist for dynamic expansion resource paths.',
|
|
7123
|
+
},
|
|
7124
|
+
{
|
|
7125
|
+
path: 'expansion.dataSource.fallbackMode',
|
|
7126
|
+
category: 'expansion',
|
|
7127
|
+
valueKind: 'enum',
|
|
7128
|
+
allowedValues: ENUMS.expansionFallbackMode,
|
|
7129
|
+
description: 'Fallback mode for expansion detail loading.',
|
|
7130
|
+
},
|
|
7131
|
+
{
|
|
7132
|
+
path: 'expansion.dataSource.cache',
|
|
7133
|
+
category: 'expansion',
|
|
7134
|
+
valueKind: 'object',
|
|
7135
|
+
description: 'Expansion detail cache configuration.',
|
|
7136
|
+
},
|
|
7137
|
+
{
|
|
7138
|
+
path: 'expansion.dataSource.cache.enabled',
|
|
7139
|
+
category: 'expansion',
|
|
7140
|
+
valueKind: 'boolean',
|
|
7141
|
+
description: 'Enable expansion detail cache.',
|
|
7142
|
+
},
|
|
7143
|
+
{
|
|
7144
|
+
path: 'expansion.dataSource.cancelOnCollapse',
|
|
7145
|
+
category: 'expansion',
|
|
7146
|
+
valueKind: 'boolean',
|
|
7147
|
+
description: 'Cancel expansion detail loading when a row collapses.',
|
|
7148
|
+
},
|
|
7149
|
+
{
|
|
7150
|
+
path: 'expansion.schemaContract',
|
|
7151
|
+
category: 'expansion',
|
|
7152
|
+
valueKind: 'object',
|
|
7153
|
+
description: 'Fail-closed schema contract for remote expansion details.',
|
|
7154
|
+
},
|
|
7155
|
+
{
|
|
7156
|
+
path: 'expansion.schemaContract.kind',
|
|
7157
|
+
category: 'expansion',
|
|
7158
|
+
valueKind: 'string',
|
|
7159
|
+
description: 'Expansion schema contract kind.',
|
|
7160
|
+
},
|
|
7161
|
+
{
|
|
7162
|
+
path: 'expansion.schemaContract.version',
|
|
7163
|
+
category: 'expansion',
|
|
7164
|
+
valueKind: 'string',
|
|
7165
|
+
description: 'Expansion schema contract version.',
|
|
7166
|
+
},
|
|
7167
|
+
{
|
|
7168
|
+
path: 'expansion.schemaContract.allowedNodes',
|
|
7169
|
+
category: 'expansion',
|
|
7170
|
+
valueKind: 'array',
|
|
7171
|
+
description: 'Allowed remote expansion node types.',
|
|
7172
|
+
},
|
|
7173
|
+
{
|
|
7174
|
+
path: 'expansion.schemaContract.maxSections',
|
|
7175
|
+
category: 'expansion',
|
|
7176
|
+
valueKind: 'number',
|
|
7177
|
+
description: 'Maximum number of remote expansion sections.',
|
|
7178
|
+
},
|
|
7179
|
+
{
|
|
7180
|
+
path: 'expansion.schemaContract.maxItemsPerSection',
|
|
7181
|
+
category: 'expansion',
|
|
7182
|
+
valueKind: 'number',
|
|
7183
|
+
description: 'Maximum number of items per remote expansion section.',
|
|
7184
|
+
},
|
|
7185
|
+
{
|
|
7186
|
+
path: 'expansion.schemaContract.requireSectionIds',
|
|
7187
|
+
category: 'expansion',
|
|
7188
|
+
valueKind: 'boolean',
|
|
7189
|
+
description: 'Require explicit ids for remote expansion sections.',
|
|
7190
|
+
},
|
|
7191
|
+
{
|
|
7192
|
+
path: 'expansion.rendering',
|
|
7193
|
+
category: 'expansion',
|
|
7194
|
+
valueKind: 'object',
|
|
7195
|
+
description: 'Expansion shell rendering configuration.',
|
|
7196
|
+
},
|
|
7197
|
+
{
|
|
7198
|
+
path: 'expansion.rendering.shell',
|
|
7199
|
+
category: 'expansion',
|
|
7200
|
+
valueKind: 'enum',
|
|
7201
|
+
allowedValues: ENUMS.expansionRenderShell,
|
|
7202
|
+
description: 'Expansion shell attachment mode.',
|
|
7203
|
+
},
|
|
7204
|
+
{
|
|
7205
|
+
path: 'expansion.rendering.columns',
|
|
7206
|
+
category: 'expansion',
|
|
7207
|
+
valueKind: 'enum',
|
|
7208
|
+
allowedValues: ENUMS.expansionColumns,
|
|
7209
|
+
description: 'Expansion detail column count.',
|
|
7210
|
+
},
|
|
7211
|
+
{
|
|
7212
|
+
path: 'expansion.rendering.gap',
|
|
7213
|
+
category: 'expansion',
|
|
7214
|
+
valueKind: 'string',
|
|
7215
|
+
description: 'Gap between expansion sections.',
|
|
7216
|
+
},
|
|
7217
|
+
{
|
|
7218
|
+
path: 'expansion.rendering.padding',
|
|
7219
|
+
category: 'expansion',
|
|
7220
|
+
valueKind: 'string',
|
|
7221
|
+
description: 'Padding inside the expansion shell.',
|
|
7222
|
+
},
|
|
7223
|
+
{
|
|
7224
|
+
path: 'expansion.rendering.class',
|
|
7225
|
+
category: 'expansion',
|
|
7226
|
+
valueKind: 'string',
|
|
7227
|
+
description: 'CSS class for the expansion shell.',
|
|
7228
|
+
},
|
|
7229
|
+
{
|
|
7230
|
+
path: 'expansion.rendering.style',
|
|
7231
|
+
category: 'expansion',
|
|
7232
|
+
valueKind: 'string',
|
|
7233
|
+
description: 'Inline style for the expansion shell.',
|
|
7234
|
+
},
|
|
7235
|
+
{
|
|
7236
|
+
path: 'expansion.rendering.loadingTemplate',
|
|
7237
|
+
category: 'expansion',
|
|
7238
|
+
valueKind: 'object',
|
|
7239
|
+
description: 'Template rendered while expansion detail loads.',
|
|
7240
|
+
},
|
|
7241
|
+
{
|
|
7242
|
+
path: 'expansion.rendering.errorTemplate',
|
|
7243
|
+
category: 'expansion',
|
|
7244
|
+
valueKind: 'object',
|
|
7245
|
+
description: 'Template rendered when expansion detail loading fails.',
|
|
7246
|
+
},
|
|
7247
|
+
{
|
|
7248
|
+
path: 'ui',
|
|
7249
|
+
category: 'ui',
|
|
7250
|
+
valueKind: 'object',
|
|
7251
|
+
description: 'Lightweight toolbar UI.',
|
|
7252
|
+
},
|
|
7253
|
+
{
|
|
7254
|
+
path: 'ui.showSearch',
|
|
7255
|
+
category: 'ui',
|
|
7256
|
+
valueKind: 'boolean',
|
|
7257
|
+
description: 'Show search input.',
|
|
7258
|
+
},
|
|
7259
|
+
{
|
|
7260
|
+
path: 'ui.searchField',
|
|
7261
|
+
category: 'ui',
|
|
7262
|
+
valueKind: 'string',
|
|
7263
|
+
description: 'Search field.',
|
|
7264
|
+
},
|
|
7265
|
+
{
|
|
7266
|
+
path: 'ui.searchPlaceholder',
|
|
7267
|
+
category: 'ui',
|
|
7268
|
+
valueKind: 'string',
|
|
7269
|
+
description: 'Search placeholder.',
|
|
7270
|
+
},
|
|
7271
|
+
{
|
|
7272
|
+
path: 'ui.showSort',
|
|
7273
|
+
category: 'ui',
|
|
7274
|
+
valueKind: 'boolean',
|
|
7275
|
+
description: 'Show sort selector.',
|
|
7276
|
+
},
|
|
7277
|
+
{
|
|
7278
|
+
path: 'ui.sortOptions',
|
|
7279
|
+
category: 'ui',
|
|
7280
|
+
valueKind: 'array',
|
|
7281
|
+
description: 'Sort options (string or {label,value}).',
|
|
7282
|
+
},
|
|
7283
|
+
{
|
|
7284
|
+
path: 'ui.sortOptions[].label',
|
|
7285
|
+
category: 'ui',
|
|
7286
|
+
valueKind: 'string',
|
|
7287
|
+
description: 'Sort option label.',
|
|
7288
|
+
},
|
|
7289
|
+
{
|
|
7290
|
+
path: 'ui.sortOptions[].value',
|
|
7291
|
+
category: 'ui',
|
|
7292
|
+
valueKind: 'string',
|
|
7293
|
+
description: 'Sort option value.',
|
|
6557
7294
|
},
|
|
6558
7295
|
{
|
|
6559
7296
|
path: 'ui.showRange',
|
|
@@ -6579,6 +7316,12 @@ const LIST_AI_CAPABILITIES = {
|
|
|
6579
7316
|
valueKind: 'string',
|
|
6580
7317
|
description: 'Currency code (ex.: BRL).',
|
|
6581
7318
|
},
|
|
7319
|
+
{
|
|
7320
|
+
path: 'i18n.localization',
|
|
7321
|
+
category: 'i18n',
|
|
7322
|
+
valueKind: 'object',
|
|
7323
|
+
description: 'Core localization defaults reused by list template pipes.',
|
|
7324
|
+
},
|
|
6582
7325
|
{
|
|
6583
7326
|
path: 'a11y',
|
|
6584
7327
|
category: 'a11y',
|
|
@@ -8946,36 +9689,36 @@ class PraxisList {
|
|
|
8946
9689
|
if (!confirmed)
|
|
8947
9690
|
return;
|
|
8948
9691
|
}
|
|
8949
|
-
const
|
|
8950
|
-
const isGlobal = this.isGlobalCommand(command);
|
|
9692
|
+
const globalAction = action?.globalAction;
|
|
8951
9693
|
const loadingKey = this.buildActionLoadingKey(actionId, item, index);
|
|
8952
9694
|
if (action?.showLoading)
|
|
8953
9695
|
this.actionLoadingState[loadingKey] = true;
|
|
8954
9696
|
try {
|
|
8955
|
-
if (
|
|
8956
|
-
const
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
9697
|
+
if (globalAction?.actionId) {
|
|
9698
|
+
const payload = this.resolveActionPayload(globalAction.payload, item, index);
|
|
9699
|
+
if (!this.globalActions) {
|
|
9700
|
+
return;
|
|
9701
|
+
}
|
|
9702
|
+
const result = await this.globalActions.executeRef({ ...globalAction, ...(payload !== undefined ? { payload } : {}) }, {
|
|
9703
|
+
sourceId: this.listId,
|
|
9704
|
+
output: 'actionClick',
|
|
9705
|
+
payload,
|
|
9706
|
+
runtime: { item },
|
|
9707
|
+
meta: { actionId },
|
|
9708
|
+
});
|
|
9709
|
+
if (result && !result.success && isDevMode()) {
|
|
9710
|
+
this.logger.warn('[PraxisList] global action returned an error.', this.buildLogOptions({ globalActionId: globalAction.actionId, actionId, error: result.error }, {
|
|
9711
|
+
context: { actionId },
|
|
9712
|
+
throttleKey: `praxis-list:global-action-result-error:${actionId}`,
|
|
9713
|
+
}));
|
|
8971
9714
|
}
|
|
8972
9715
|
}
|
|
8973
9716
|
}
|
|
8974
9717
|
catch (error) {
|
|
8975
9718
|
if (isDevMode()) {
|
|
8976
|
-
this.logger.warn('[PraxisList] global
|
|
9719
|
+
this.logger.warn('[PraxisList] global action execution failed.', this.buildLogOptions({ globalActionId: globalAction?.actionId, actionId, error }, {
|
|
8977
9720
|
context: { actionId },
|
|
8978
|
-
throttleKey: `praxis-list:global-
|
|
9721
|
+
throttleKey: `praxis-list:global-action-execution-failed:${actionId}`,
|
|
8979
9722
|
}));
|
|
8980
9723
|
}
|
|
8981
9724
|
}
|
|
@@ -8983,15 +9726,11 @@ class PraxisList {
|
|
|
8983
9726
|
if (action?.showLoading)
|
|
8984
9727
|
delete this.actionLoadingState[loadingKey];
|
|
8985
9728
|
}
|
|
8986
|
-
const emitLocal = action?.emitLocal ?? !
|
|
9729
|
+
const emitLocal = action?.emitLocal ?? !globalAction?.actionId;
|
|
8987
9730
|
if (emitLocal) {
|
|
8988
9731
|
this.actionClick.emit({ actionId, item, index });
|
|
8989
9732
|
}
|
|
8990
9733
|
}
|
|
8991
|
-
isGlobalCommand(command) {
|
|
8992
|
-
const c = (command || '').trim();
|
|
8993
|
-
return c.startsWith('global:') || c.startsWith('global.');
|
|
8994
|
-
}
|
|
8995
9734
|
resolveActionPayload(raw, item, index) {
|
|
8996
9735
|
if (raw == null)
|
|
8997
9736
|
return { item, index };
|
|
@@ -9016,19 +9755,27 @@ class PraxisList {
|
|
|
9016
9755
|
return this.resolveTemplate(raw, ctx);
|
|
9017
9756
|
return raw;
|
|
9018
9757
|
}
|
|
9019
|
-
resolveTemplate(raw, ctx) {
|
|
9758
|
+
resolveTemplate(raw, ctx, key) {
|
|
9759
|
+
if (this.isDeferredTemplateExpressionKey(key) && typeof raw === 'string') {
|
|
9760
|
+
return raw;
|
|
9761
|
+
}
|
|
9020
9762
|
if (typeof raw === 'string')
|
|
9021
9763
|
return this.resolveStringTemplate(raw, ctx);
|
|
9022
9764
|
if (Array.isArray(raw))
|
|
9023
|
-
return raw.map((v) => this.resolveTemplate(v, ctx));
|
|
9765
|
+
return raw.map((v) => this.resolveTemplate(v, ctx, key));
|
|
9024
9766
|
if (raw && typeof raw === 'object') {
|
|
9025
9767
|
const out = {};
|
|
9026
9768
|
for (const [k, v] of Object.entries(raw))
|
|
9027
|
-
out[k] = this.resolveTemplate(v, ctx);
|
|
9769
|
+
out[k] = this.resolveTemplate(v, ctx, k);
|
|
9028
9770
|
return out;
|
|
9029
9771
|
}
|
|
9030
9772
|
return raw;
|
|
9031
9773
|
}
|
|
9774
|
+
isDeferredTemplateExpressionKey(key) {
|
|
9775
|
+
if (!key)
|
|
9776
|
+
return false;
|
|
9777
|
+
return key === 'expr' || key.endsWith('Expr');
|
|
9778
|
+
}
|
|
9032
9779
|
resolveStringTemplate(raw, ctx) {
|
|
9033
9780
|
const single = raw.match(/^\$\{([^}]+)\}$/);
|
|
9034
9781
|
if (single)
|
|
@@ -9806,15 +10553,6 @@ class PraxisList {
|
|
|
9806
10553
|
this.actionItemObjectIds.set(value, next);
|
|
9807
10554
|
return next;
|
|
9808
10555
|
}
|
|
9809
|
-
warnGlobalCommandUnavailableOnce(actionIdResolved) {
|
|
9810
|
-
if (!isDevMode())
|
|
9811
|
-
return;
|
|
9812
|
-
this.logger.warnOnce('[PraxisList] global command ignored because GlobalActionService is unavailable.', this.buildLogOptions({ actionId: actionIdResolved }, {
|
|
9813
|
-
context: { actionId: actionIdResolved },
|
|
9814
|
-
dedupeKey: `praxis-list:global-action-unavailable:${actionIdResolved}`,
|
|
9815
|
-
throttleKey: 'praxis-list:global-action-unavailable',
|
|
9816
|
-
}));
|
|
9817
|
-
}
|
|
9818
10556
|
buildLogOptions(data, options = {}) {
|
|
9819
10557
|
const context = {
|
|
9820
10558
|
...this.logContext,
|
|
@@ -10218,6 +10956,1460 @@ function providePraxisListMetadata() {
|
|
|
10218
10956
|
};
|
|
10219
10957
|
}
|
|
10220
10958
|
|
|
10959
|
+
const templateSchema = {
|
|
10960
|
+
type: 'object',
|
|
10961
|
+
required: ['type', 'expr'],
|
|
10962
|
+
properties: {
|
|
10963
|
+
type: { enum: ['text', 'icon', 'image', 'chip', 'rating', 'currency', 'date', 'html', 'slot', 'metric', 'compose', 'component'] },
|
|
10964
|
+
expr: { type: 'string' },
|
|
10965
|
+
class: { type: 'string' },
|
|
10966
|
+
style: { type: 'string' },
|
|
10967
|
+
color: { type: 'string' },
|
|
10968
|
+
variant: { enum: ['filled', 'outlined'] },
|
|
10969
|
+
imageAlt: { type: 'string' },
|
|
10970
|
+
props: {
|
|
10971
|
+
type: 'object',
|
|
10972
|
+
properties: {
|
|
10973
|
+
rating: {
|
|
10974
|
+
type: 'object',
|
|
10975
|
+
properties: {
|
|
10976
|
+
max: { type: 'number' },
|
|
10977
|
+
size: { type: 'number' },
|
|
10978
|
+
color: { type: 'string' },
|
|
10979
|
+
},
|
|
10980
|
+
},
|
|
10981
|
+
metric: {
|
|
10982
|
+
type: 'object',
|
|
10983
|
+
properties: {
|
|
10984
|
+
label: { type: 'string' },
|
|
10985
|
+
valueExpr: { type: 'string' },
|
|
10986
|
+
caption: { type: 'string' },
|
|
10987
|
+
subcaption: { type: 'string' },
|
|
10988
|
+
captionPosition: { enum: ['below-value', 'below-progress'] },
|
|
10989
|
+
icon: { type: 'string' },
|
|
10990
|
+
iconExpr: { type: 'string' },
|
|
10991
|
+
tone: { type: 'string' },
|
|
10992
|
+
toneExpr: { type: 'string' },
|
|
10993
|
+
layout: { enum: ['value-only', 'value+caption', 'icon+value+caption', 'stacked-center'] },
|
|
10994
|
+
align: { enum: ['start', 'center', 'end'] },
|
|
10995
|
+
progress: {
|
|
10996
|
+
type: 'object',
|
|
10997
|
+
properties: {
|
|
10998
|
+
valueExpr: { type: 'string' },
|
|
10999
|
+
max: { type: 'number' },
|
|
11000
|
+
mode: { enum: ['determinate', 'indeterminate'] },
|
|
11001
|
+
color: { type: 'string' },
|
|
11002
|
+
colorExpr: { type: 'string' },
|
|
11003
|
+
trackColor: { type: 'string' },
|
|
11004
|
+
},
|
|
11005
|
+
},
|
|
11006
|
+
valueClass: { type: 'string' },
|
|
11007
|
+
valueStyle: { type: 'string' },
|
|
11008
|
+
showBar: { type: 'boolean' },
|
|
11009
|
+
barValueExpr: { type: 'string' },
|
|
11010
|
+
barColor: { type: 'string' },
|
|
11011
|
+
barVariant: { enum: ['determinate', 'indeterminate'] },
|
|
11012
|
+
legendExpr: { type: 'string' },
|
|
11013
|
+
iconPosition: { enum: ['left', 'right', 'top'] },
|
|
11014
|
+
iconColorExpr: { type: 'string' },
|
|
11015
|
+
},
|
|
11016
|
+
},
|
|
11017
|
+
compose: {
|
|
11018
|
+
type: 'object',
|
|
11019
|
+
properties: {
|
|
11020
|
+
items: { type: 'array', items: { type: 'object' } },
|
|
11021
|
+
nodes: { type: 'array', items: { type: 'object' } },
|
|
11022
|
+
direction: { enum: ['row', 'column'] },
|
|
11023
|
+
orientation: { enum: ['horizontal', 'vertical'] },
|
|
11024
|
+
align: { enum: ['start', 'center', 'end'] },
|
|
11025
|
+
gap: { type: 'string' },
|
|
11026
|
+
wrap: { type: 'boolean' },
|
|
11027
|
+
separator: { type: 'string' },
|
|
11028
|
+
class: { type: 'string' },
|
|
11029
|
+
style: { type: 'string' },
|
|
11030
|
+
},
|
|
11031
|
+
},
|
|
11032
|
+
component: {
|
|
11033
|
+
type: 'object',
|
|
11034
|
+
required: ['id'],
|
|
11035
|
+
properties: {
|
|
11036
|
+
id: { type: 'string' },
|
|
11037
|
+
inputs: { type: 'object' },
|
|
11038
|
+
class: { type: 'string' },
|
|
11039
|
+
style: { type: 'string' },
|
|
11040
|
+
},
|
|
11041
|
+
},
|
|
11042
|
+
},
|
|
11043
|
+
},
|
|
11044
|
+
badge: {
|
|
11045
|
+
type: 'object',
|
|
11046
|
+
properties: {
|
|
11047
|
+
expr: { type: 'string' },
|
|
11048
|
+
color: { type: 'string' },
|
|
11049
|
+
variant: { enum: ['filled', 'outlined'] },
|
|
11050
|
+
},
|
|
11051
|
+
},
|
|
11052
|
+
},
|
|
11053
|
+
};
|
|
11054
|
+
const templateSlotEnum = [
|
|
11055
|
+
'leading',
|
|
11056
|
+
'primary',
|
|
11057
|
+
'secondary',
|
|
11058
|
+
'meta',
|
|
11059
|
+
'trailing',
|
|
11060
|
+
'identity',
|
|
11061
|
+
'balance',
|
|
11062
|
+
'limit',
|
|
11063
|
+
'risk',
|
|
11064
|
+
'alerts',
|
|
11065
|
+
'owner',
|
|
11066
|
+
'sectionHeader',
|
|
11067
|
+
'emptyState',
|
|
11068
|
+
];
|
|
11069
|
+
const rowLayoutSlotEnum = [...templateSlotEnum.filter(slot => slot !== 'sectionHeader' && slot !== 'emptyState'), 'actions', 'expand'];
|
|
11070
|
+
const actionSchema = {
|
|
11071
|
+
type: 'object',
|
|
11072
|
+
required: ['id', 'label'],
|
|
11073
|
+
properties: {
|
|
11074
|
+
id: { type: 'string' },
|
|
11075
|
+
label: { type: 'string' },
|
|
11076
|
+
icon: { type: 'string' },
|
|
11077
|
+
color: { type: 'string' },
|
|
11078
|
+
kind: { enum: ['icon', 'button'] },
|
|
11079
|
+
buttonVariant: { enum: ['stroked', 'raised', 'flat'] },
|
|
11080
|
+
showIf: { type: 'object' },
|
|
11081
|
+
emitPayload: { enum: ['item', 'id', 'value'] },
|
|
11082
|
+
globalAction: {
|
|
11083
|
+
type: 'object',
|
|
11084
|
+
properties: {
|
|
11085
|
+
actionId: { type: 'string' },
|
|
11086
|
+
payload: { type: 'object' },
|
|
11087
|
+
},
|
|
11088
|
+
},
|
|
11089
|
+
emitLocal: { type: 'boolean' },
|
|
11090
|
+
showLoading: { type: 'boolean' },
|
|
11091
|
+
placement: { enum: ['actions', 'trailing'] },
|
|
11092
|
+
confirmation: {
|
|
11093
|
+
type: 'object',
|
|
11094
|
+
properties: {
|
|
11095
|
+
title: { type: 'string' },
|
|
11096
|
+
message: { type: 'string' },
|
|
11097
|
+
type: { enum: ['danger', 'warning', 'info'] },
|
|
11098
|
+
},
|
|
11099
|
+
},
|
|
11100
|
+
},
|
|
11101
|
+
};
|
|
11102
|
+
const rowLayoutSchema = {
|
|
11103
|
+
type: 'object',
|
|
11104
|
+
properties: {
|
|
11105
|
+
type: { enum: ['grid', 'flex'] },
|
|
11106
|
+
columns: {
|
|
11107
|
+
type: 'array',
|
|
11108
|
+
items: {
|
|
11109
|
+
type: 'object',
|
|
11110
|
+
required: ['slot'],
|
|
11111
|
+
properties: {
|
|
11112
|
+
slot: { enum: rowLayoutSlotEnum },
|
|
11113
|
+
width: { type: 'string' },
|
|
11114
|
+
minWidth: { type: 'string' },
|
|
11115
|
+
maxWidth: { type: 'string' },
|
|
11116
|
+
align: { enum: ['start', 'center', 'end'] },
|
|
11117
|
+
justify: { enum: ['start', 'center', 'end', 'stretch'] },
|
|
11118
|
+
class: { type: 'string' },
|
|
11119
|
+
style: { type: 'string' },
|
|
11120
|
+
},
|
|
11121
|
+
},
|
|
11122
|
+
},
|
|
11123
|
+
gap: { type: 'string' },
|
|
11124
|
+
align: { enum: ['start', 'center', 'end', 'stretch'] },
|
|
11125
|
+
itemAlignY: { enum: ['start', 'center', 'end', 'stretch'] },
|
|
11126
|
+
class: { type: 'string' },
|
|
11127
|
+
style: { type: 'string' },
|
|
11128
|
+
},
|
|
11129
|
+
};
|
|
11130
|
+
const expansionSectionSchema = {
|
|
11131
|
+
type: 'object',
|
|
11132
|
+
required: ['id', 'type'],
|
|
11133
|
+
properties: {
|
|
11134
|
+
id: { type: 'string' },
|
|
11135
|
+
title: { type: 'string' },
|
|
11136
|
+
type: { enum: ['info-list', 'chip-list', 'timeline', 'key-value', 'metadata', 'component'] },
|
|
11137
|
+
itemsExpr: { type: 'string' },
|
|
11138
|
+
emptyLabel: { type: 'string' },
|
|
11139
|
+
metadata: {
|
|
11140
|
+
type: 'object',
|
|
11141
|
+
properties: {
|
|
11142
|
+
orientation: { enum: ['horizontal', 'vertical'] },
|
|
11143
|
+
columns: { enum: [1, 2, 3] },
|
|
11144
|
+
gap: { type: 'string' },
|
|
11145
|
+
keyClass: { type: 'string' },
|
|
11146
|
+
valueClass: { type: 'string' },
|
|
11147
|
+
keyStyle: { type: 'string' },
|
|
11148
|
+
valueStyle: { type: 'string' },
|
|
11149
|
+
},
|
|
11150
|
+
},
|
|
11151
|
+
component: {
|
|
11152
|
+
type: 'object',
|
|
11153
|
+
properties: {
|
|
11154
|
+
id: { type: 'string' },
|
|
11155
|
+
inputs: { type: 'object' },
|
|
11156
|
+
class: { type: 'string' },
|
|
11157
|
+
style: { type: 'string' },
|
|
11158
|
+
},
|
|
11159
|
+
},
|
|
11160
|
+
class: { type: 'string' },
|
|
11161
|
+
style: { type: 'string' },
|
|
11162
|
+
showIf: { type: 'object' },
|
|
11163
|
+
},
|
|
11164
|
+
};
|
|
11165
|
+
const expansionSchema = {
|
|
11166
|
+
type: 'object',
|
|
11167
|
+
properties: {
|
|
11168
|
+
sections: { type: 'array', items: expansionSectionSchema },
|
|
11169
|
+
dataSource: {
|
|
11170
|
+
type: 'object',
|
|
11171
|
+
properties: {
|
|
11172
|
+
mode: { enum: ['inline', 'resource', 'resourcePath'] },
|
|
11173
|
+
resource: {
|
|
11174
|
+
type: 'object',
|
|
11175
|
+
properties: {
|
|
11176
|
+
kind: { type: 'string' },
|
|
11177
|
+
id: { type: 'string' },
|
|
11178
|
+
version: { type: 'string' },
|
|
11179
|
+
},
|
|
11180
|
+
},
|
|
11181
|
+
resourcePath: {
|
|
11182
|
+
type: 'object',
|
|
11183
|
+
properties: {
|
|
11184
|
+
path: { type: 'string' },
|
|
11185
|
+
method: { enum: ['GET', 'POST'] },
|
|
11186
|
+
paramsMap: { type: 'object' },
|
|
11187
|
+
},
|
|
11188
|
+
},
|
|
11189
|
+
resourceAllowList: { type: 'array', items: { type: 'string' } },
|
|
11190
|
+
fallbackMode: { enum: ['none', 'inline', 'resource'] },
|
|
11191
|
+
cache: { type: 'object', properties: { enabled: { type: 'boolean' } } },
|
|
11192
|
+
cancelOnCollapse: { type: 'boolean' },
|
|
11193
|
+
},
|
|
11194
|
+
},
|
|
11195
|
+
schemaContract: {
|
|
11196
|
+
type: 'object',
|
|
11197
|
+
properties: {
|
|
11198
|
+
kind: { enum: ['praxis.detail.schema'] },
|
|
11199
|
+
version: { type: 'string' },
|
|
11200
|
+
allowedNodes: { type: 'array', items: { enum: ['info-list', 'chip-list', 'timeline', 'key-value', 'metadata', 'component'] } },
|
|
11201
|
+
maxSections: { type: 'number' },
|
|
11202
|
+
maxItemsPerSection: { type: 'number' },
|
|
11203
|
+
requireSectionIds: { type: 'boolean' },
|
|
11204
|
+
},
|
|
11205
|
+
},
|
|
11206
|
+
rendering: {
|
|
11207
|
+
type: 'object',
|
|
11208
|
+
properties: {
|
|
11209
|
+
shell: { enum: ['attached', 'detached', 'modal'] },
|
|
11210
|
+
columns: { enum: [1, 2, 3] },
|
|
11211
|
+
gap: { type: 'string' },
|
|
11212
|
+
padding: { type: 'string' },
|
|
11213
|
+
class: { type: 'string' },
|
|
11214
|
+
style: { type: 'string' },
|
|
11215
|
+
loadingTemplate: templateSchema,
|
|
11216
|
+
errorTemplate: templateSchema,
|
|
11217
|
+
},
|
|
11218
|
+
},
|
|
11219
|
+
},
|
|
11220
|
+
};
|
|
11221
|
+
const sortOptionSchema = {
|
|
11222
|
+
oneOf: [
|
|
11223
|
+
{ type: 'string' },
|
|
11224
|
+
{
|
|
11225
|
+
type: 'object',
|
|
11226
|
+
required: ['label', 'value'],
|
|
11227
|
+
properties: {
|
|
11228
|
+
label: { type: 'string' },
|
|
11229
|
+
value: { type: 'string' },
|
|
11230
|
+
},
|
|
11231
|
+
},
|
|
11232
|
+
],
|
|
11233
|
+
};
|
|
11234
|
+
const PRAXIS_LIST_AUTHORING_MANIFEST = {
|
|
11235
|
+
schemaVersion: '1.0.0',
|
|
11236
|
+
componentId: 'praxis-list',
|
|
11237
|
+
ownerPackage: '@praxisui/list',
|
|
11238
|
+
configSchemaId: 'PraxisListConfig',
|
|
11239
|
+
manifestVersion: '1.1.0',
|
|
11240
|
+
runtimeInputs: [
|
|
11241
|
+
{ name: 'config', type: 'PraxisListConfig', description: 'Canonical list configuration.' },
|
|
11242
|
+
{ name: 'listId', type: 'string', description: 'Stable list id used by persistence.' },
|
|
11243
|
+
{ name: 'enableCustomization', type: 'boolean', description: 'Enables runtime authoring surfaces.' },
|
|
11244
|
+
],
|
|
11245
|
+
editableTargets: [
|
|
11246
|
+
{ kind: 'itemTemplate', resolver: 'list-template-slot', description: 'Template slot under templating.*.' },
|
|
11247
|
+
{ kind: 'primaryText', resolver: 'templating-primary', description: 'Primary text template.' },
|
|
11248
|
+
{ kind: 'secondaryText', resolver: 'templating-secondary', description: 'Secondary text template.' },
|
|
11249
|
+
{ kind: 'avatar', resolver: 'templating-leading', description: 'Leading/avatar template.' },
|
|
11250
|
+
{ kind: 'badge', resolver: 'templating-trailing-chip', description: 'Trailing badge/status template.' },
|
|
11251
|
+
{ kind: 'itemAction', resolver: 'actions-by-id', description: 'Item action resolved by actions[].id.' },
|
|
11252
|
+
{ kind: 'emptyState', resolver: 'templating-empty-state', description: 'Empty state template.' },
|
|
11253
|
+
{ kind: 'selection', resolver: 'selection-config', description: 'Selection configuration.' },
|
|
11254
|
+
{ kind: 'layout', resolver: 'layout-config', description: 'Layout configuration.' },
|
|
11255
|
+
{ kind: 'rowLayout', resolver: 'row-layout-config', description: 'Row layout grid/flex columns and placement slots.' },
|
|
11256
|
+
{ kind: 'dataBinding', resolver: 'data-source-config', description: 'Local or remote data source binding.' },
|
|
11257
|
+
{ kind: 'interaction', resolver: 'interaction-config', description: 'Expandable row interaction configuration.' },
|
|
11258
|
+
{ kind: 'expansion', resolver: 'expansion-config', description: 'Inline expansion sections, data source and rendering configuration.' },
|
|
11259
|
+
{ kind: 'rules', resolver: 'rules-config', description: 'Item style rules and slot override rules.' },
|
|
11260
|
+
{ kind: 'meta', resolver: 'list-root-config', description: 'Root list metadata such as id.' },
|
|
11261
|
+
{ kind: 'skin', resolver: 'skin-config', description: 'Visual skin configuration.' },
|
|
11262
|
+
{ kind: 'toolbarUi', resolver: 'ui-config', description: 'Search, sort and footer UI configuration.' },
|
|
11263
|
+
{ kind: 'localization', resolver: 'i18n-config', description: 'Locale and currency configuration.' },
|
|
11264
|
+
{ kind: 'accessibility', resolver: 'a11y-config', description: 'Accessibility configuration.' },
|
|
11265
|
+
{ kind: 'eventMapping', resolver: 'events-config', description: 'Declarative event name mappings.' },
|
|
11266
|
+
],
|
|
11267
|
+
operations: [
|
|
11268
|
+
{
|
|
11269
|
+
operationId: 'list.id.set',
|
|
11270
|
+
title: 'Set list id',
|
|
11271
|
+
scope: 'meta',
|
|
11272
|
+
targetKind: 'meta',
|
|
11273
|
+
target: { kind: 'meta', resolver: 'list-root-config', required: false },
|
|
11274
|
+
inputSchema: {
|
|
11275
|
+
type: 'object',
|
|
11276
|
+
required: ['id'],
|
|
11277
|
+
properties: {
|
|
11278
|
+
id: { type: 'string' },
|
|
11279
|
+
},
|
|
11280
|
+
},
|
|
11281
|
+
effects: [{ kind: 'set-value', path: 'id' }],
|
|
11282
|
+
validators: ['list-id-stable', 'editor-round-trip-preserve'],
|
|
11283
|
+
affectedPaths: ['id'],
|
|
11284
|
+
submissionImpact: false,
|
|
11285
|
+
destructive: false,
|
|
11286
|
+
requiresConfirmation: false,
|
|
11287
|
+
preconditions: ['config-initialized'],
|
|
11288
|
+
},
|
|
11289
|
+
{
|
|
11290
|
+
operationId: 'item.primaryText.set',
|
|
11291
|
+
title: 'Set primary item text',
|
|
11292
|
+
scope: 'itemTemplate',
|
|
11293
|
+
targetKind: 'primaryText',
|
|
11294
|
+
target: { kind: 'primaryText', resolver: 'templating-primary', required: false },
|
|
11295
|
+
inputSchema: templateSchema,
|
|
11296
|
+
effects: [{ kind: 'merge-object', path: 'templating.primary' }],
|
|
11297
|
+
validators: ['bound-field-exists', 'template-expression-safe', 'editor-round-trip-preserve'],
|
|
11298
|
+
affectedPaths: ['templating.primary'],
|
|
11299
|
+
submissionImpact: false,
|
|
11300
|
+
destructive: false,
|
|
11301
|
+
requiresConfirmation: false,
|
|
11302
|
+
preconditions: ['config-initialized'],
|
|
11303
|
+
},
|
|
11304
|
+
{
|
|
11305
|
+
operationId: 'item.secondaryText.set',
|
|
11306
|
+
title: 'Set secondary item text',
|
|
11307
|
+
scope: 'itemTemplate',
|
|
11308
|
+
targetKind: 'secondaryText',
|
|
11309
|
+
target: { kind: 'secondaryText', resolver: 'templating-secondary', required: false },
|
|
11310
|
+
inputSchema: templateSchema,
|
|
11311
|
+
effects: [{ kind: 'merge-object', path: 'templating.secondary' }],
|
|
11312
|
+
validators: ['bound-field-exists', 'template-expression-safe', 'editor-round-trip-preserve'],
|
|
11313
|
+
affectedPaths: ['templating.secondary'],
|
|
11314
|
+
submissionImpact: false,
|
|
11315
|
+
destructive: false,
|
|
11316
|
+
requiresConfirmation: false,
|
|
11317
|
+
preconditions: ['config-initialized'],
|
|
11318
|
+
},
|
|
11319
|
+
{
|
|
11320
|
+
operationId: 'item.avatar.configure',
|
|
11321
|
+
title: 'Configure item avatar',
|
|
11322
|
+
scope: 'itemTemplate',
|
|
11323
|
+
targetKind: 'avatar',
|
|
11324
|
+
target: { kind: 'avatar', resolver: 'templating-leading', required: false },
|
|
11325
|
+
inputSchema: {
|
|
11326
|
+
...templateSchema,
|
|
11327
|
+
required: ['type'],
|
|
11328
|
+
},
|
|
11329
|
+
effects: [{ kind: 'merge-object', path: 'templating.leading' }],
|
|
11330
|
+
validators: ['bound-field-exists', 'template-expression-safe', 'editor-round-trip-preserve'],
|
|
11331
|
+
affectedPaths: ['templating.leading'],
|
|
11332
|
+
submissionImpact: false,
|
|
11333
|
+
destructive: false,
|
|
11334
|
+
requiresConfirmation: false,
|
|
11335
|
+
preconditions: ['config-initialized'],
|
|
11336
|
+
},
|
|
11337
|
+
{
|
|
11338
|
+
operationId: 'item.badge.configure',
|
|
11339
|
+
title: 'Configure item badge',
|
|
11340
|
+
scope: 'itemTemplate',
|
|
11341
|
+
targetKind: 'badge',
|
|
11342
|
+
target: { kind: 'badge', resolver: 'templating-trailing-chip', required: false },
|
|
11343
|
+
inputSchema: {
|
|
11344
|
+
type: 'object',
|
|
11345
|
+
required: ['expr'],
|
|
11346
|
+
properties: {
|
|
11347
|
+
expr: { type: 'string' },
|
|
11348
|
+
color: { type: 'string' },
|
|
11349
|
+
variant: { enum: ['filled', 'outlined'] },
|
|
11350
|
+
statusPosition: { enum: ['inline', 'top-right'] },
|
|
11351
|
+
labelMap: { type: 'object' },
|
|
11352
|
+
colorMap: { type: 'object' },
|
|
11353
|
+
},
|
|
11354
|
+
},
|
|
11355
|
+
effects: [
|
|
11356
|
+
{ kind: 'merge-object', path: 'templating.trailing' },
|
|
11357
|
+
{ kind: 'set-value', path: 'templating.statusPosition' },
|
|
11358
|
+
{ kind: 'merge-object', path: 'templating.chipLabelMap' },
|
|
11359
|
+
{ kind: 'merge-object', path: 'templating.chipColorMap' },
|
|
11360
|
+
],
|
|
11361
|
+
validators: ['bound-field-exists', 'template-expression-safe', 'editor-round-trip-preserve'],
|
|
11362
|
+
affectedPaths: ['templating.trailing', 'templating.statusPosition', 'templating.chipLabelMap', 'templating.chipColorMap'],
|
|
11363
|
+
submissionImpact: false,
|
|
11364
|
+
destructive: false,
|
|
11365
|
+
requiresConfirmation: false,
|
|
11366
|
+
preconditions: ['config-initialized'],
|
|
11367
|
+
},
|
|
11368
|
+
{
|
|
11369
|
+
operationId: 'template.slot.set',
|
|
11370
|
+
title: 'Set any list template slot',
|
|
11371
|
+
scope: 'templating',
|
|
11372
|
+
targetKind: 'itemTemplate',
|
|
11373
|
+
target: { kind: 'itemTemplate', resolver: 'list-template-slot', required: true },
|
|
11374
|
+
inputSchema: {
|
|
11375
|
+
type: 'object',
|
|
11376
|
+
required: ['slot', 'template'],
|
|
11377
|
+
properties: {
|
|
11378
|
+
slot: { enum: templateSlotEnum },
|
|
11379
|
+
template: templateSchema,
|
|
11380
|
+
},
|
|
11381
|
+
},
|
|
11382
|
+
effects: [{
|
|
11383
|
+
kind: 'compile-domain-patch',
|
|
11384
|
+
handler: 'list-template-slot-set',
|
|
11385
|
+
handlerContract: {
|
|
11386
|
+
reads: ['templating'],
|
|
11387
|
+
writes: [
|
|
11388
|
+
'templating.leading',
|
|
11389
|
+
'templating.primary',
|
|
11390
|
+
'templating.secondary',
|
|
11391
|
+
'templating.meta',
|
|
11392
|
+
'templating.trailing',
|
|
11393
|
+
'templating.identity',
|
|
11394
|
+
'templating.balance',
|
|
11395
|
+
'templating.limit',
|
|
11396
|
+
'templating.risk',
|
|
11397
|
+
'templating.alerts',
|
|
11398
|
+
'templating.owner',
|
|
11399
|
+
'templating.sectionHeader',
|
|
11400
|
+
'templating.emptyState',
|
|
11401
|
+
],
|
|
11402
|
+
identityKeys: ['slot'],
|
|
11403
|
+
inputSchema: {
|
|
11404
|
+
type: 'object',
|
|
11405
|
+
required: ['slot', 'template'],
|
|
11406
|
+
properties: {
|
|
11407
|
+
slot: { enum: templateSlotEnum },
|
|
11408
|
+
template: templateSchema,
|
|
11409
|
+
},
|
|
11410
|
+
},
|
|
11411
|
+
failureModes: ['unsupported-template-slot', 'invalid-template-expression', 'ambiguous-template-slot'],
|
|
11412
|
+
description: 'Writes the requested template into one supported templating slot; slot is the stable identity, not array position.',
|
|
11413
|
+
},
|
|
11414
|
+
}],
|
|
11415
|
+
validators: ['template-slot-supported', 'bound-field-exists', 'template-expression-safe', 'editor-round-trip-preserve'],
|
|
11416
|
+
affectedPaths: [
|
|
11417
|
+
'templating.leading',
|
|
11418
|
+
'templating.primary',
|
|
11419
|
+
'templating.secondary',
|
|
11420
|
+
'templating.meta',
|
|
11421
|
+
'templating.trailing',
|
|
11422
|
+
'templating.identity',
|
|
11423
|
+
'templating.balance',
|
|
11424
|
+
'templating.limit',
|
|
11425
|
+
'templating.risk',
|
|
11426
|
+
'templating.alerts',
|
|
11427
|
+
'templating.owner',
|
|
11428
|
+
'templating.sectionHeader',
|
|
11429
|
+
'templating.emptyState',
|
|
11430
|
+
],
|
|
11431
|
+
submissionImpact: false,
|
|
11432
|
+
destructive: false,
|
|
11433
|
+
requiresConfirmation: false,
|
|
11434
|
+
preconditions: ['config-initialized'],
|
|
11435
|
+
},
|
|
11436
|
+
{
|
|
11437
|
+
operationId: 'item.action.add',
|
|
11438
|
+
title: 'Add item action',
|
|
11439
|
+
scope: 'itemAction',
|
|
11440
|
+
targetKind: 'itemAction',
|
|
11441
|
+
target: { kind: 'itemAction', resolver: 'actions-by-id', required: false },
|
|
11442
|
+
inputSchema: actionSchema,
|
|
11443
|
+
effects: [{ kind: 'append-unique', path: 'actions[]', key: 'id' }],
|
|
11444
|
+
validators: ['action-id-stable', 'json-logic-valid', 'global-action-ref-valid', 'editor-round-trip-preserve'],
|
|
11445
|
+
affectedPaths: ['actions[]'],
|
|
11446
|
+
submissionImpact: false,
|
|
11447
|
+
destructive: false,
|
|
11448
|
+
requiresConfirmation: false,
|
|
11449
|
+
preconditions: ['config-initialized'],
|
|
11450
|
+
},
|
|
11451
|
+
{
|
|
11452
|
+
operationId: 'item.action.update',
|
|
11453
|
+
title: 'Update item action',
|
|
11454
|
+
scope: 'itemAction',
|
|
11455
|
+
targetKind: 'itemAction',
|
|
11456
|
+
target: { kind: 'itemAction', resolver: 'actions-by-id', required: true },
|
|
11457
|
+
inputSchema: { ...actionSchema, required: ['id'] },
|
|
11458
|
+
effects: [{ kind: 'merge-by-key', path: 'actions[]', key: 'id' }],
|
|
11459
|
+
validators: ['action-exists', 'action-id-stable', 'json-logic-valid', 'global-action-ref-valid', 'declared-only-runtime-warning', 'editor-round-trip-preserve'],
|
|
11460
|
+
affectedPaths: [
|
|
11461
|
+
'actions',
|
|
11462
|
+
'actions[].id',
|
|
11463
|
+
'actions[].icon',
|
|
11464
|
+
'actions[].label',
|
|
11465
|
+
'actions[].color',
|
|
11466
|
+
'actions[].kind',
|
|
11467
|
+
'actions[].buttonVariant',
|
|
11468
|
+
'actions[].showIf',
|
|
11469
|
+
'actions[].emitPayload',
|
|
11470
|
+
'actions[].globalAction.[actionId]',
|
|
11471
|
+
'actions[].globalAction.payload',
|
|
11472
|
+
'actions[].emitLocal',
|
|
11473
|
+
'actions[].showLoading',
|
|
11474
|
+
'actions[].placement',
|
|
11475
|
+
'actions[].confirmation',
|
|
11476
|
+
'actions[].confirmation.title',
|
|
11477
|
+
'actions[].confirmation.message',
|
|
11478
|
+
'actions[].confirmation.type',
|
|
11479
|
+
],
|
|
11480
|
+
submissionImpact: false,
|
|
11481
|
+
destructive: false,
|
|
11482
|
+
requiresConfirmation: false,
|
|
11483
|
+
preconditions: ['config-initialized', 'target-exists'],
|
|
11484
|
+
},
|
|
11485
|
+
{
|
|
11486
|
+
operationId: 'item.action.remove',
|
|
11487
|
+
title: 'Remove item action',
|
|
11488
|
+
scope: 'itemAction',
|
|
11489
|
+
targetKind: 'itemAction',
|
|
11490
|
+
target: { kind: 'itemAction', resolver: 'actions-by-id', required: true },
|
|
11491
|
+
inputSchema: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },
|
|
11492
|
+
effects: [{ kind: 'remove-by-key', path: 'actions[]', key: 'id' }],
|
|
11493
|
+
destructive: true,
|
|
11494
|
+
requiresConfirmation: true,
|
|
11495
|
+
validators: ['action-exists', 'destructive-removal-confirmation', 'editor-round-trip-preserve'],
|
|
11496
|
+
affectedPaths: ['actions[]'],
|
|
11497
|
+
submissionImpact: false,
|
|
11498
|
+
preconditions: ['config-initialized', 'target-exists'],
|
|
11499
|
+
},
|
|
11500
|
+
{
|
|
11501
|
+
operationId: 'selection.mode.set',
|
|
11502
|
+
title: 'Set selection mode',
|
|
11503
|
+
scope: 'selection',
|
|
11504
|
+
targetKind: 'selection',
|
|
11505
|
+
target: { kind: 'selection', resolver: 'selection-config', required: false },
|
|
11506
|
+
inputSchema: {
|
|
11507
|
+
type: 'object',
|
|
11508
|
+
required: ['mode'],
|
|
11509
|
+
properties: {
|
|
11510
|
+
mode: { enum: ['none', 'single', 'multiple'] },
|
|
11511
|
+
compareBy: { type: 'string' },
|
|
11512
|
+
return: { enum: ['value', 'item', 'id'] },
|
|
11513
|
+
formControlName: { type: 'string' },
|
|
11514
|
+
formControlPath: { type: 'string' },
|
|
11515
|
+
},
|
|
11516
|
+
},
|
|
11517
|
+
effects: [{ kind: 'merge-object', path: 'selection' }],
|
|
11518
|
+
validators: ['selection-mode-supported', 'bound-field-exists', 'editor-round-trip-preserve'],
|
|
11519
|
+
affectedPaths: [
|
|
11520
|
+
'selection',
|
|
11521
|
+
'selection.mode',
|
|
11522
|
+
'selection.formControlName',
|
|
11523
|
+
'selection.formControlPath',
|
|
11524
|
+
'selection.compareBy',
|
|
11525
|
+
'selection.return',
|
|
11526
|
+
],
|
|
11527
|
+
submissionImpact: false,
|
|
11528
|
+
destructive: false,
|
|
11529
|
+
requiresConfirmation: false,
|
|
11530
|
+
preconditions: ['config-initialized'],
|
|
11531
|
+
},
|
|
11532
|
+
{
|
|
11533
|
+
operationId: 'interaction.expansion.configure',
|
|
11534
|
+
title: 'Configure expandable row interaction',
|
|
11535
|
+
scope: 'interaction',
|
|
11536
|
+
targetKind: 'interaction',
|
|
11537
|
+
target: { kind: 'interaction', resolver: 'interaction-config', required: false },
|
|
11538
|
+
inputSchema: {
|
|
11539
|
+
type: 'object',
|
|
11540
|
+
properties: {
|
|
11541
|
+
expandable: { type: 'boolean' },
|
|
11542
|
+
expandTrigger: { enum: ['row', 'icon', 'row+icon'] },
|
|
11543
|
+
expandMode: { enum: ['single', 'multiple'] },
|
|
11544
|
+
expandPlacement: { enum: ['expand', 'trailing'] },
|
|
11545
|
+
},
|
|
11546
|
+
},
|
|
11547
|
+
effects: [{ kind: 'merge-object', path: 'interaction' }],
|
|
11548
|
+
validators: ['interaction-value-supported', 'row-layout-placement-reachable', 'editor-round-trip-preserve'],
|
|
11549
|
+
affectedPaths: [
|
|
11550
|
+
'interaction',
|
|
11551
|
+
'interaction.expandable',
|
|
11552
|
+
'interaction.expandTrigger',
|
|
11553
|
+
'interaction.expandMode',
|
|
11554
|
+
'interaction.expandPlacement',
|
|
11555
|
+
],
|
|
11556
|
+
submissionImpact: false,
|
|
11557
|
+
destructive: false,
|
|
11558
|
+
requiresConfirmation: false,
|
|
11559
|
+
preconditions: ['config-initialized'],
|
|
11560
|
+
},
|
|
11561
|
+
{
|
|
11562
|
+
operationId: 'layout.density.set',
|
|
11563
|
+
title: 'Set list density',
|
|
11564
|
+
scope: 'layout',
|
|
11565
|
+
targetKind: 'layout',
|
|
11566
|
+
target: { kind: 'layout', resolver: 'layout-config', required: false },
|
|
11567
|
+
inputSchema: {
|
|
11568
|
+
type: 'object',
|
|
11569
|
+
properties: {
|
|
11570
|
+
density: { enum: ['default', 'comfortable', 'compact'] },
|
|
11571
|
+
variant: { enum: ['list', 'cards', 'tiles'] },
|
|
11572
|
+
itemSpacing: { enum: ['none', 'tight', 'default', 'relaxed'] },
|
|
11573
|
+
lines: { enum: [1, 2, 3] },
|
|
11574
|
+
dividers: { enum: ['none', 'between', 'all'] },
|
|
11575
|
+
model: { enum: ['standard', 'media', 'hotel'] },
|
|
11576
|
+
groupBy: { type: 'string' },
|
|
11577
|
+
stickySectionHeader: { type: 'boolean' },
|
|
11578
|
+
virtualScroll: { type: 'boolean' },
|
|
11579
|
+
pageSize: { type: 'number' },
|
|
11580
|
+
},
|
|
11581
|
+
},
|
|
11582
|
+
effects: [{ kind: 'merge-object', path: 'layout' }],
|
|
11583
|
+
validators: ['layout-value-supported', 'bound-field-exists', 'declared-only-runtime-warning', 'editor-round-trip-preserve'],
|
|
11584
|
+
affectedPaths: [
|
|
11585
|
+
'layout',
|
|
11586
|
+
'layout.variant',
|
|
11587
|
+
'layout.density',
|
|
11588
|
+
'layout.itemSpacing',
|
|
11589
|
+
'layout.lines',
|
|
11590
|
+
'layout.dividers',
|
|
11591
|
+
'layout.model',
|
|
11592
|
+
'layout.groupBy',
|
|
11593
|
+
'layout.stickySectionHeader',
|
|
11594
|
+
'layout.virtualScroll',
|
|
11595
|
+
'layout.pageSize',
|
|
11596
|
+
],
|
|
11597
|
+
submissionImpact: false,
|
|
11598
|
+
destructive: false,
|
|
11599
|
+
requiresConfirmation: false,
|
|
11600
|
+
preconditions: ['config-initialized'],
|
|
11601
|
+
},
|
|
11602
|
+
{
|
|
11603
|
+
operationId: 'layout.rowLayout.configure',
|
|
11604
|
+
title: 'Configure list row layout',
|
|
11605
|
+
scope: 'rowLayout',
|
|
11606
|
+
targetKind: 'rowLayout',
|
|
11607
|
+
target: { kind: 'rowLayout', resolver: 'row-layout-config', required: false },
|
|
11608
|
+
inputSchema: rowLayoutSchema,
|
|
11609
|
+
effects: [{ kind: 'merge-object', path: 'layout.rowLayout' }],
|
|
11610
|
+
validators: ['row-layout-supported', 'row-layout-placement-reachable', 'style-value-safe', 'editor-round-trip-preserve'],
|
|
11611
|
+
affectedPaths: [
|
|
11612
|
+
'layout.rowLayout',
|
|
11613
|
+
'layout.rowLayout.type',
|
|
11614
|
+
'layout.rowLayout.columns',
|
|
11615
|
+
'layout.rowLayout.columns[].slot',
|
|
11616
|
+
'layout.rowLayout.columns[].width',
|
|
11617
|
+
'layout.rowLayout.columns[].minWidth',
|
|
11618
|
+
'layout.rowLayout.columns[].maxWidth',
|
|
11619
|
+
'layout.rowLayout.columns[].align',
|
|
11620
|
+
'layout.rowLayout.columns[].justify',
|
|
11621
|
+
'layout.rowLayout.columns[].class',
|
|
11622
|
+
'layout.rowLayout.columns[].style',
|
|
11623
|
+
'layout.rowLayout.gap',
|
|
11624
|
+
'layout.rowLayout.align',
|
|
11625
|
+
'layout.rowLayout.itemAlignY',
|
|
11626
|
+
'layout.rowLayout.class',
|
|
11627
|
+
'layout.rowLayout.style',
|
|
11628
|
+
],
|
|
11629
|
+
submissionImpact: false,
|
|
11630
|
+
destructive: false,
|
|
11631
|
+
requiresConfirmation: false,
|
|
11632
|
+
preconditions: ['config-initialized'],
|
|
11633
|
+
},
|
|
11634
|
+
{
|
|
11635
|
+
operationId: 'skin.configure',
|
|
11636
|
+
title: 'Configure list skin',
|
|
11637
|
+
scope: 'skin',
|
|
11638
|
+
targetKind: 'skin',
|
|
11639
|
+
target: { kind: 'skin', resolver: 'skin-config', required: false },
|
|
11640
|
+
inputSchema: {
|
|
11641
|
+
type: 'object',
|
|
11642
|
+
properties: {
|
|
11643
|
+
type: { enum: ['pill-soft', 'gradient-tile', 'glass', 'elevated', 'outline', 'flat', 'neumorphism', 'custom'] },
|
|
11644
|
+
gradient: {
|
|
11645
|
+
type: 'object',
|
|
11646
|
+
properties: {
|
|
11647
|
+
from: { type: 'string' },
|
|
11648
|
+
to: { type: 'string' },
|
|
11649
|
+
angle: { type: 'number' },
|
|
11650
|
+
},
|
|
11651
|
+
},
|
|
11652
|
+
radius: { type: 'string' },
|
|
11653
|
+
shadow: { type: 'string' },
|
|
11654
|
+
border: { type: 'string' },
|
|
11655
|
+
backdropBlur: { type: 'string' },
|
|
11656
|
+
class: { type: 'string' },
|
|
11657
|
+
inlineStyle: { type: 'string' },
|
|
11658
|
+
},
|
|
11659
|
+
},
|
|
11660
|
+
effects: [{ kind: 'merge-object', path: 'skin' }],
|
|
11661
|
+
validators: ['skin-value-supported', 'style-value-safe', 'editor-round-trip-preserve'],
|
|
11662
|
+
affectedPaths: [
|
|
11663
|
+
'skin',
|
|
11664
|
+
'skin.type',
|
|
11665
|
+
'skin.gradient',
|
|
11666
|
+
'skin.gradient.from',
|
|
11667
|
+
'skin.gradient.to',
|
|
11668
|
+
'skin.gradient.angle',
|
|
11669
|
+
'skin.radius',
|
|
11670
|
+
'skin.shadow',
|
|
11671
|
+
'skin.border',
|
|
11672
|
+
'skin.backdropBlur',
|
|
11673
|
+
'skin.class',
|
|
11674
|
+
'skin.inlineStyle',
|
|
11675
|
+
],
|
|
11676
|
+
submissionImpact: false,
|
|
11677
|
+
destructive: false,
|
|
11678
|
+
requiresConfirmation: false,
|
|
11679
|
+
preconditions: ['config-initialized'],
|
|
11680
|
+
},
|
|
11681
|
+
{
|
|
11682
|
+
operationId: 'templating.display.configure',
|
|
11683
|
+
title: 'Configure list template display options',
|
|
11684
|
+
scope: 'templating',
|
|
11685
|
+
targetKind: 'itemTemplate',
|
|
11686
|
+
target: { kind: 'itemTemplate', resolver: 'templating-display-options', required: false },
|
|
11687
|
+
inputSchema: {
|
|
11688
|
+
type: 'object',
|
|
11689
|
+
properties: {
|
|
11690
|
+
metaPlacement: { enum: ['side', 'line'] },
|
|
11691
|
+
metaPrefixIcon: { type: 'string' },
|
|
11692
|
+
statusPosition: { enum: ['inline', 'top-right'] },
|
|
11693
|
+
chipColorMap: { type: 'object' },
|
|
11694
|
+
chipLabelMap: { type: 'object' },
|
|
11695
|
+
iconColorMap: { type: 'object' },
|
|
11696
|
+
featuresVisible: { type: 'boolean' },
|
|
11697
|
+
featuresMode: { enum: ['icons+labels', 'icons-only', 'labels-only'] },
|
|
11698
|
+
},
|
|
11699
|
+
},
|
|
11700
|
+
effects: [{ kind: 'merge-object', path: 'templating' }],
|
|
11701
|
+
validators: ['template-display-supported', 'editor-round-trip-preserve'],
|
|
11702
|
+
affectedPaths: [
|
|
11703
|
+
'templating',
|
|
11704
|
+
'templating.metaPlacement',
|
|
11705
|
+
'templating.metaPrefixIcon',
|
|
11706
|
+
'templating.statusPosition',
|
|
11707
|
+
'templating.chipColorMap',
|
|
11708
|
+
'templating.chipLabelMap',
|
|
11709
|
+
'templating.iconColorMap',
|
|
11710
|
+
'templating.featuresVisible',
|
|
11711
|
+
'templating.featuresMode',
|
|
11712
|
+
],
|
|
11713
|
+
submissionImpact: false,
|
|
11714
|
+
destructive: false,
|
|
11715
|
+
requiresConfirmation: false,
|
|
11716
|
+
preconditions: ['config-initialized'],
|
|
11717
|
+
},
|
|
11718
|
+
{
|
|
11719
|
+
operationId: 'templating.features.set',
|
|
11720
|
+
title: 'Set list feature line',
|
|
11721
|
+
scope: 'templating',
|
|
11722
|
+
targetKind: 'itemTemplate',
|
|
11723
|
+
target: { kind: 'itemTemplate', resolver: 'templating-features', required: false },
|
|
11724
|
+
inputSchema: {
|
|
11725
|
+
type: 'object',
|
|
11726
|
+
required: ['features'],
|
|
11727
|
+
properties: {
|
|
11728
|
+
features: {
|
|
11729
|
+
type: 'array',
|
|
11730
|
+
items: {
|
|
11731
|
+
type: 'object',
|
|
11732
|
+
required: ['expr'],
|
|
11733
|
+
properties: {
|
|
11734
|
+
icon: { type: 'string' },
|
|
11735
|
+
expr: { type: 'string' },
|
|
11736
|
+
class: { type: 'string' },
|
|
11737
|
+
style: { type: 'string' },
|
|
11738
|
+
},
|
|
11739
|
+
},
|
|
11740
|
+
},
|
|
11741
|
+
},
|
|
11742
|
+
},
|
|
11743
|
+
effects: [{ kind: 'set-value', path: 'templating.features' }],
|
|
11744
|
+
validators: ['bound-field-exists', 'template-expression-safe', 'editor-round-trip-preserve'],
|
|
11745
|
+
affectedPaths: [
|
|
11746
|
+
'templating.features',
|
|
11747
|
+
'templating.features[].icon',
|
|
11748
|
+
'templating.features[].expr',
|
|
11749
|
+
'templating.features[].class',
|
|
11750
|
+
'templating.features[].style',
|
|
11751
|
+
],
|
|
11752
|
+
submissionImpact: false,
|
|
11753
|
+
destructive: false,
|
|
11754
|
+
requiresConfirmation: false,
|
|
11755
|
+
preconditions: ['config-initialized'],
|
|
11756
|
+
},
|
|
11757
|
+
{
|
|
11758
|
+
operationId: 'templating.skeleton.configure',
|
|
11759
|
+
title: 'Configure list skeleton',
|
|
11760
|
+
scope: 'templating',
|
|
11761
|
+
targetKind: 'itemTemplate',
|
|
11762
|
+
target: { kind: 'itemTemplate', resolver: 'templating-skeleton', required: false },
|
|
11763
|
+
inputSchema: {
|
|
11764
|
+
type: 'object',
|
|
11765
|
+
required: ['count'],
|
|
11766
|
+
properties: {
|
|
11767
|
+
count: { type: 'number' },
|
|
11768
|
+
},
|
|
11769
|
+
},
|
|
11770
|
+
effects: [{ kind: 'merge-object', path: 'templating.skeleton' }],
|
|
11771
|
+
validators: ['skeleton-value-supported', 'editor-round-trip-preserve'],
|
|
11772
|
+
affectedPaths: ['templating.skeleton', 'templating.skeleton.count'],
|
|
11773
|
+
submissionImpact: false,
|
|
11774
|
+
destructive: false,
|
|
11775
|
+
requiresConfirmation: false,
|
|
11776
|
+
preconditions: ['config-initialized'],
|
|
11777
|
+
},
|
|
11778
|
+
{
|
|
11779
|
+
operationId: 'rules.itemStyle.upsert',
|
|
11780
|
+
title: 'Upsert item style rule',
|
|
11781
|
+
scope: 'rules',
|
|
11782
|
+
targetKind: 'rules',
|
|
11783
|
+
target: { kind: 'rules', resolver: 'rules-config', required: false },
|
|
11784
|
+
inputSchema: {
|
|
11785
|
+
type: 'object',
|
|
11786
|
+
required: ['id'],
|
|
11787
|
+
properties: {
|
|
11788
|
+
id: { type: 'string' },
|
|
11789
|
+
condition: { type: 'object' },
|
|
11790
|
+
class: { type: 'string' },
|
|
11791
|
+
style: { type: 'string' },
|
|
11792
|
+
border: { type: 'string' },
|
|
11793
|
+
background: { type: 'string' },
|
|
11794
|
+
},
|
|
11795
|
+
},
|
|
11796
|
+
effects: [{ kind: 'merge-by-key', path: 'rules.itemStyles[]', key: 'id' }],
|
|
11797
|
+
validators: ['rule-id-stable', 'json-logic-valid', 'style-value-safe', 'editor-round-trip-preserve'],
|
|
11798
|
+
affectedPaths: [
|
|
11799
|
+
'rules',
|
|
11800
|
+
'rules.itemStyles',
|
|
11801
|
+
'rules.itemStyles[].id',
|
|
11802
|
+
'rules.itemStyles[].condition',
|
|
11803
|
+
'rules.itemStyles[].class',
|
|
11804
|
+
'rules.itemStyles[].style',
|
|
11805
|
+
'rules.itemStyles[].border',
|
|
11806
|
+
'rules.itemStyles[].background',
|
|
11807
|
+
],
|
|
11808
|
+
submissionImpact: false,
|
|
11809
|
+
destructive: false,
|
|
11810
|
+
requiresConfirmation: false,
|
|
11811
|
+
preconditions: ['config-initialized'],
|
|
11812
|
+
},
|
|
11813
|
+
{
|
|
11814
|
+
operationId: 'rules.slotOverride.upsert',
|
|
11815
|
+
title: 'Upsert slot override rule',
|
|
11816
|
+
scope: 'rules',
|
|
11817
|
+
targetKind: 'rules',
|
|
11818
|
+
target: { kind: 'rules', resolver: 'rules-config', required: false },
|
|
11819
|
+
inputSchema: {
|
|
11820
|
+
type: 'object',
|
|
11821
|
+
required: ['id', 'slot'],
|
|
11822
|
+
properties: {
|
|
11823
|
+
id: { type: 'string' },
|
|
11824
|
+
slot: { enum: templateSlotEnum.filter(slot => slot !== 'sectionHeader' && slot !== 'emptyState') },
|
|
11825
|
+
condition: { type: 'object' },
|
|
11826
|
+
template: templateSchema,
|
|
11827
|
+
class: { type: 'string' },
|
|
11828
|
+
style: { type: 'string' },
|
|
11829
|
+
hide: { type: 'boolean' },
|
|
11830
|
+
},
|
|
11831
|
+
},
|
|
11832
|
+
effects: [{ kind: 'merge-by-key', path: 'rules.slotOverrides[]', key: 'id' }],
|
|
11833
|
+
validators: ['rule-id-stable', 'template-slot-supported', 'json-logic-valid', 'template-expression-safe', 'style-value-safe', 'editor-round-trip-preserve'],
|
|
11834
|
+
affectedPaths: [
|
|
11835
|
+
'rules',
|
|
11836
|
+
'rules.slotOverrides',
|
|
11837
|
+
'rules.slotOverrides[].id',
|
|
11838
|
+
'rules.slotOverrides[].slot',
|
|
11839
|
+
'rules.slotOverrides[].condition',
|
|
11840
|
+
'rules.slotOverrides[].template',
|
|
11841
|
+
'rules.slotOverrides[].class',
|
|
11842
|
+
'rules.slotOverrides[].style',
|
|
11843
|
+
'rules.slotOverrides[].hide',
|
|
11844
|
+
],
|
|
11845
|
+
submissionImpact: false,
|
|
11846
|
+
destructive: false,
|
|
11847
|
+
requiresConfirmation: false,
|
|
11848
|
+
preconditions: ['config-initialized'],
|
|
11849
|
+
},
|
|
11850
|
+
{
|
|
11851
|
+
operationId: 'emptyState.set',
|
|
11852
|
+
title: 'Set empty state',
|
|
11853
|
+
scope: 'itemTemplate',
|
|
11854
|
+
targetKind: 'emptyState',
|
|
11855
|
+
target: { kind: 'emptyState', resolver: 'templating-empty-state', required: false },
|
|
11856
|
+
inputSchema: templateSchema,
|
|
11857
|
+
effects: [{ kind: 'merge-object', path: 'templating.emptyState' }],
|
|
11858
|
+
validators: ['template-expression-safe', 'editor-round-trip-preserve'],
|
|
11859
|
+
affectedPaths: ['templating.emptyState'],
|
|
11860
|
+
submissionImpact: false,
|
|
11861
|
+
destructive: false,
|
|
11862
|
+
requiresConfirmation: false,
|
|
11863
|
+
preconditions: ['config-initialized'],
|
|
11864
|
+
},
|
|
11865
|
+
{
|
|
11866
|
+
operationId: 'expansion.configure',
|
|
11867
|
+
title: 'Configure list expansion',
|
|
11868
|
+
scope: 'expansion',
|
|
11869
|
+
targetKind: 'expansion',
|
|
11870
|
+
target: { kind: 'expansion', resolver: 'expansion-config', required: false },
|
|
11871
|
+
inputSchema: expansionSchema,
|
|
11872
|
+
effects: [{ kind: 'merge-object', path: 'expansion' }],
|
|
11873
|
+
validators: [
|
|
11874
|
+
'expansion-section-id-stable',
|
|
11875
|
+
'expansion-value-supported',
|
|
11876
|
+
'json-logic-valid',
|
|
11877
|
+
'template-expression-safe',
|
|
11878
|
+
'remote-resource-binding-safe',
|
|
11879
|
+
'style-value-safe',
|
|
11880
|
+
'editor-round-trip-preserve',
|
|
11881
|
+
],
|
|
11882
|
+
affectedPaths: [
|
|
11883
|
+
'expansion',
|
|
11884
|
+
'expansion.sections',
|
|
11885
|
+
'expansion.sections[].id',
|
|
11886
|
+
'expansion.sections[].title',
|
|
11887
|
+
'expansion.sections[].type',
|
|
11888
|
+
'expansion.sections[].itemsExpr',
|
|
11889
|
+
'expansion.sections[].emptyLabel',
|
|
11890
|
+
'expansion.sections[].metadata',
|
|
11891
|
+
'expansion.sections[].component',
|
|
11892
|
+
'expansion.sections[].class',
|
|
11893
|
+
'expansion.sections[].style',
|
|
11894
|
+
'expansion.sections[].showIf',
|
|
11895
|
+
'expansion.dataSource',
|
|
11896
|
+
'expansion.dataSource.mode',
|
|
11897
|
+
'expansion.dataSource.resource',
|
|
11898
|
+
'expansion.dataSource.resource.kind',
|
|
11899
|
+
'expansion.dataSource.resource.id',
|
|
11900
|
+
'expansion.dataSource.resource.version',
|
|
11901
|
+
'expansion.dataSource.resourcePath',
|
|
11902
|
+
'expansion.dataSource.resourcePath.path',
|
|
11903
|
+
'expansion.dataSource.resourcePath.method',
|
|
11904
|
+
'expansion.dataSource.resourcePath.paramsMap',
|
|
11905
|
+
'expansion.dataSource.resourceAllowList',
|
|
11906
|
+
'expansion.dataSource.fallbackMode',
|
|
11907
|
+
'expansion.dataSource.cache',
|
|
11908
|
+
'expansion.dataSource.cache.enabled',
|
|
11909
|
+
'expansion.dataSource.cancelOnCollapse',
|
|
11910
|
+
'expansion.schemaContract',
|
|
11911
|
+
'expansion.schemaContract.kind',
|
|
11912
|
+
'expansion.schemaContract.version',
|
|
11913
|
+
'expansion.schemaContract.allowedNodes',
|
|
11914
|
+
'expansion.schemaContract.maxSections',
|
|
11915
|
+
'expansion.schemaContract.maxItemsPerSection',
|
|
11916
|
+
'expansion.schemaContract.requireSectionIds',
|
|
11917
|
+
'expansion.rendering',
|
|
11918
|
+
'expansion.rendering.shell',
|
|
11919
|
+
'expansion.rendering.columns',
|
|
11920
|
+
'expansion.rendering.gap',
|
|
11921
|
+
'expansion.rendering.padding',
|
|
11922
|
+
'expansion.rendering.class',
|
|
11923
|
+
'expansion.rendering.style',
|
|
11924
|
+
'expansion.rendering.loadingTemplate',
|
|
11925
|
+
'expansion.rendering.errorTemplate',
|
|
11926
|
+
],
|
|
11927
|
+
submissionImpact: false,
|
|
11928
|
+
destructive: false,
|
|
11929
|
+
requiresConfirmation: false,
|
|
11930
|
+
preconditions: ['config-initialized'],
|
|
11931
|
+
},
|
|
11932
|
+
{
|
|
11933
|
+
operationId: 'data.resource.bind',
|
|
11934
|
+
title: 'Bind remote resource',
|
|
11935
|
+
scope: 'dataBinding',
|
|
11936
|
+
targetKind: 'dataBinding',
|
|
11937
|
+
target: { kind: 'dataBinding', resolver: 'data-source-config', required: false },
|
|
11938
|
+
inputSchema: {
|
|
11939
|
+
type: 'object',
|
|
11940
|
+
required: ['resourcePath'],
|
|
11941
|
+
properties: {
|
|
11942
|
+
resourcePath: { type: 'string' },
|
|
11943
|
+
query: { type: 'object' },
|
|
11944
|
+
sort: { type: 'array', items: { type: 'string' } },
|
|
11945
|
+
clearLocalData: { type: 'boolean', default: true },
|
|
11946
|
+
},
|
|
11947
|
+
},
|
|
11948
|
+
effects: [{ kind: 'merge-object', path: 'dataSource' }],
|
|
11949
|
+
validators: ['remote-resource-binding-safe', 'local-remote-precedence-safe', 'editor-round-trip-preserve'],
|
|
11950
|
+
affectedPaths: ['dataSource.resourcePath', 'dataSource.query', 'dataSource.sort', 'dataSource.data'],
|
|
11951
|
+
submissionImpact: false,
|
|
11952
|
+
destructive: false,
|
|
11953
|
+
requiresConfirmation: false,
|
|
11954
|
+
preconditions: ['config-initialized'],
|
|
11955
|
+
},
|
|
11956
|
+
{
|
|
11957
|
+
operationId: 'data.local.set',
|
|
11958
|
+
title: 'Set local list data',
|
|
11959
|
+
scope: 'dataBinding',
|
|
11960
|
+
targetKind: 'dataBinding',
|
|
11961
|
+
target: { kind: 'dataBinding', resolver: 'data-source-config', required: false },
|
|
11962
|
+
inputSchema: {
|
|
11963
|
+
type: 'object',
|
|
11964
|
+
required: ['data'],
|
|
11965
|
+
properties: {
|
|
11966
|
+
data: { type: 'array' },
|
|
11967
|
+
clearResourcePath: { type: 'boolean', default: false },
|
|
11968
|
+
},
|
|
11969
|
+
},
|
|
11970
|
+
effects: [{ kind: 'merge-object', path: 'dataSource' }],
|
|
11971
|
+
validators: ['local-data-array-safe', 'local-remote-precedence-safe', 'editor-round-trip-preserve'],
|
|
11972
|
+
affectedPaths: ['dataSource', 'dataSource.data', 'dataSource.resourcePath'],
|
|
11973
|
+
submissionImpact: false,
|
|
11974
|
+
destructive: false,
|
|
11975
|
+
requiresConfirmation: false,
|
|
11976
|
+
preconditions: ['config-initialized'],
|
|
11977
|
+
},
|
|
11978
|
+
{
|
|
11979
|
+
operationId: 'data.query.set',
|
|
11980
|
+
title: 'Set remote list query',
|
|
11981
|
+
scope: 'dataBinding',
|
|
11982
|
+
targetKind: 'dataBinding',
|
|
11983
|
+
target: { kind: 'dataBinding', resolver: 'data-source-config', required: false },
|
|
11984
|
+
inputSchema: {
|
|
11985
|
+
type: 'object',
|
|
11986
|
+
required: ['query'],
|
|
11987
|
+
properties: {
|
|
11988
|
+
query: { type: 'object' },
|
|
11989
|
+
},
|
|
11990
|
+
},
|
|
11991
|
+
effects: [{ kind: 'set-value', path: 'dataSource.query' }],
|
|
11992
|
+
validators: ['remote-resource-binding-safe', 'editor-round-trip-preserve'],
|
|
11993
|
+
affectedPaths: ['dataSource.query'],
|
|
11994
|
+
submissionImpact: false,
|
|
11995
|
+
destructive: false,
|
|
11996
|
+
requiresConfirmation: false,
|
|
11997
|
+
preconditions: ['config-initialized'],
|
|
11998
|
+
},
|
|
11999
|
+
{
|
|
12000
|
+
operationId: 'data.sort.set',
|
|
12001
|
+
title: 'Set remote list sort',
|
|
12002
|
+
scope: 'dataBinding',
|
|
12003
|
+
targetKind: 'dataBinding',
|
|
12004
|
+
target: { kind: 'dataBinding', resolver: 'data-source-config', required: false },
|
|
12005
|
+
inputSchema: {
|
|
12006
|
+
type: 'object',
|
|
12007
|
+
required: ['sort'],
|
|
12008
|
+
properties: {
|
|
12009
|
+
sort: { type: 'array', items: { type: 'string' } },
|
|
12010
|
+
},
|
|
12011
|
+
},
|
|
12012
|
+
effects: [{ kind: 'set-value', path: 'dataSource.sort' }],
|
|
12013
|
+
validators: ['sort-entry-supported', 'editor-round-trip-preserve'],
|
|
12014
|
+
affectedPaths: ['dataSource.sort', 'dataSource.sort[]'],
|
|
12015
|
+
submissionImpact: false,
|
|
12016
|
+
destructive: false,
|
|
12017
|
+
requiresConfirmation: false,
|
|
12018
|
+
preconditions: ['config-initialized'],
|
|
12019
|
+
},
|
|
12020
|
+
{
|
|
12021
|
+
operationId: 'ui.toolbar.configure',
|
|
12022
|
+
title: 'Configure list toolbar UI',
|
|
12023
|
+
scope: 'toolbarUi',
|
|
12024
|
+
targetKind: 'toolbarUi',
|
|
12025
|
+
target: { kind: 'toolbarUi', resolver: 'ui-config', required: false },
|
|
12026
|
+
inputSchema: {
|
|
12027
|
+
type: 'object',
|
|
12028
|
+
properties: {
|
|
12029
|
+
showSearch: { type: 'boolean' },
|
|
12030
|
+
searchField: { type: 'string' },
|
|
12031
|
+
searchPlaceholder: { type: 'string' },
|
|
12032
|
+
showSort: { type: 'boolean' },
|
|
12033
|
+
sortOptions: { type: 'array', items: sortOptionSchema },
|
|
12034
|
+
showRange: { type: 'boolean' },
|
|
12035
|
+
},
|
|
12036
|
+
},
|
|
12037
|
+
effects: [{ kind: 'merge-object', path: 'ui' }],
|
|
12038
|
+
validators: ['bound-field-exists', 'sort-entry-supported', 'editor-round-trip-preserve'],
|
|
12039
|
+
affectedPaths: [
|
|
12040
|
+
'ui',
|
|
12041
|
+
'ui.showSearch',
|
|
12042
|
+
'ui.searchField',
|
|
12043
|
+
'ui.searchPlaceholder',
|
|
12044
|
+
'ui.showSort',
|
|
12045
|
+
'ui.sortOptions',
|
|
12046
|
+
'ui.sortOptions[].label',
|
|
12047
|
+
'ui.sortOptions[].value',
|
|
12048
|
+
'ui.showRange',
|
|
12049
|
+
],
|
|
12050
|
+
submissionImpact: false,
|
|
12051
|
+
destructive: false,
|
|
12052
|
+
requiresConfirmation: false,
|
|
12053
|
+
preconditions: ['config-initialized'],
|
|
12054
|
+
},
|
|
12055
|
+
{
|
|
12056
|
+
operationId: 'i18n.configure',
|
|
12057
|
+
title: 'Configure list localization',
|
|
12058
|
+
scope: 'localization',
|
|
12059
|
+
targetKind: 'localization',
|
|
12060
|
+
target: { kind: 'localization', resolver: 'i18n-config', required: false },
|
|
12061
|
+
inputSchema: {
|
|
12062
|
+
type: 'object',
|
|
12063
|
+
properties: {
|
|
12064
|
+
locale: { type: 'string' },
|
|
12065
|
+
currency: { type: 'string' },
|
|
12066
|
+
localization: { type: 'object' },
|
|
12067
|
+
},
|
|
12068
|
+
},
|
|
12069
|
+
effects: [{ kind: 'merge-object', path: 'i18n' }],
|
|
12070
|
+
validators: ['i18n-value-supported', 'editor-round-trip-preserve'],
|
|
12071
|
+
affectedPaths: ['i18n', 'i18n.locale', 'i18n.currency', 'i18n.localization'],
|
|
12072
|
+
submissionImpact: false,
|
|
12073
|
+
destructive: false,
|
|
12074
|
+
requiresConfirmation: false,
|
|
12075
|
+
preconditions: ['config-initialized'],
|
|
12076
|
+
},
|
|
12077
|
+
{
|
|
12078
|
+
operationId: 'a11y.configure',
|
|
12079
|
+
title: 'Configure list accessibility',
|
|
12080
|
+
scope: 'accessibility',
|
|
12081
|
+
targetKind: 'accessibility',
|
|
12082
|
+
target: { kind: 'accessibility', resolver: 'a11y-config', required: false },
|
|
12083
|
+
inputSchema: {
|
|
12084
|
+
type: 'object',
|
|
12085
|
+
properties: {
|
|
12086
|
+
ariaLabel: { type: 'string' },
|
|
12087
|
+
ariaLabelledBy: { type: 'string' },
|
|
12088
|
+
highContrast: { type: 'boolean' },
|
|
12089
|
+
reduceMotion: { type: 'boolean' },
|
|
12090
|
+
},
|
|
12091
|
+
},
|
|
12092
|
+
effects: [{ kind: 'merge-object', path: 'a11y' }],
|
|
12093
|
+
validators: ['a11y-value-supported', 'declared-only-runtime-warning', 'editor-round-trip-preserve'],
|
|
12094
|
+
affectedPaths: [
|
|
12095
|
+
'a11y',
|
|
12096
|
+
'a11y.ariaLabel',
|
|
12097
|
+
'a11y.ariaLabelledBy',
|
|
12098
|
+
'a11y.highContrast',
|
|
12099
|
+
'a11y.reduceMotion',
|
|
12100
|
+
],
|
|
12101
|
+
submissionImpact: false,
|
|
12102
|
+
destructive: false,
|
|
12103
|
+
requiresConfirmation: false,
|
|
12104
|
+
preconditions: ['config-initialized'],
|
|
12105
|
+
},
|
|
12106
|
+
{
|
|
12107
|
+
operationId: 'events.map',
|
|
12108
|
+
title: 'Configure declarative event mappings',
|
|
12109
|
+
scope: 'eventMapping',
|
|
12110
|
+
targetKind: 'eventMapping',
|
|
12111
|
+
target: { kind: 'eventMapping', resolver: 'events-config', required: false },
|
|
12112
|
+
inputSchema: {
|
|
12113
|
+
type: 'object',
|
|
12114
|
+
properties: {
|
|
12115
|
+
itemClick: { type: 'string' },
|
|
12116
|
+
actionClick: { type: 'string' },
|
|
12117
|
+
selectionChange: { type: 'string' },
|
|
12118
|
+
loaded: { type: 'string' },
|
|
12119
|
+
},
|
|
12120
|
+
},
|
|
12121
|
+
effects: [{ kind: 'merge-object', path: 'events' }],
|
|
12122
|
+
validators: ['event-name-supported', 'declared-only-runtime-warning', 'editor-round-trip-preserve'],
|
|
12123
|
+
affectedPaths: [
|
|
12124
|
+
'events',
|
|
12125
|
+
'events.itemClick',
|
|
12126
|
+
'events.actionClick',
|
|
12127
|
+
'events.selectionChange',
|
|
12128
|
+
'events.loaded',
|
|
12129
|
+
],
|
|
12130
|
+
submissionImpact: false,
|
|
12131
|
+
destructive: false,
|
|
12132
|
+
requiresConfirmation: false,
|
|
12133
|
+
preconditions: ['config-initialized'],
|
|
12134
|
+
},
|
|
12135
|
+
],
|
|
12136
|
+
validators: [
|
|
12137
|
+
{ validatorId: 'list-id-stable', level: 'error', code: 'PL000', description: 'List id must remain stable for persistence and editor reopen flows.' },
|
|
12138
|
+
{ validatorId: 'bound-field-exists', level: 'error', code: 'PL001', description: 'Bound template and selection fields must exist in local data or schema context when available.' },
|
|
12139
|
+
{ validatorId: 'template-expression-safe', level: 'error', code: 'PL002', description: 'Template expressions must use supported ${item.path} syntax and allowed pipes.' },
|
|
12140
|
+
{ validatorId: 'action-id-stable', level: 'error', code: 'PL003', description: 'Item actions must declare stable unique ids.' },
|
|
12141
|
+
{ validatorId: 'action-exists', level: 'error', code: 'PL004', description: 'Target action id must exist before update or removal.' },
|
|
12142
|
+
{ validatorId: 'destructive-removal-confirmation', level: 'error', code: 'PL005', description: 'Destructive action removal requires explicit confirmation.' },
|
|
12143
|
+
{ validatorId: 'remote-resource-binding-safe', level: 'error', code: 'PL006', description: 'Remote resource binding must use canonical resource metadata and safe resourcePath values.' },
|
|
12144
|
+
{ validatorId: 'local-remote-precedence-safe', level: 'warning', code: 'PL007', description: 'When binding resourcePath, tools must account for dataSource.data taking precedence over remote data.' },
|
|
12145
|
+
{ validatorId: 'selection-mode-supported', level: 'error', code: 'PL008', description: 'Selection mode and return policy must match the runtime enum.' },
|
|
12146
|
+
{ validatorId: 'layout-value-supported', level: 'error', code: 'PL009', description: 'Layout density and related enum values must match PraxisListConfig.' },
|
|
12147
|
+
{ validatorId: 'json-logic-valid', level: 'error', code: 'PL010', description: 'Action visibility conditions must be serializable Json Logic.' },
|
|
12148
|
+
{ validatorId: 'global-action-ref-valid', level: 'warning', code: 'PL011', description: 'Structured globalAction refs must use a registered action id when registry context is available.' },
|
|
12149
|
+
{ validatorId: 'editor-round-trip-preserve', level: 'error', code: 'PL012', description: 'The list config editor and JSON editor must preserve the saved shape without drift.' },
|
|
12150
|
+
{ validatorId: 'template-slot-supported', level: 'error', code: 'PL013', description: 'Template slot names must be one of the runtime-supported catalog slots.' },
|
|
12151
|
+
{ validatorId: 'skin-value-supported', level: 'error', code: 'PL014', description: 'Skin values must match the runtime-supported skin contract.' },
|
|
12152
|
+
{ validatorId: 'style-value-safe', level: 'warning', code: 'PL015', description: 'Inline styles and classes must be safe for the host sanitization policy.' },
|
|
12153
|
+
{ validatorId: 'template-display-supported', level: 'error', code: 'PL016', description: 'Template display options must match the runtime enum contract.' },
|
|
12154
|
+
{ validatorId: 'skeleton-value-supported', level: 'error', code: 'PL017', description: 'Skeleton counts must be numeric and non-negative.' },
|
|
12155
|
+
{ validatorId: 'local-data-array-safe', level: 'error', code: 'PL018', description: 'Local list data must be an array and must not hide an intended remote binding accidentally.' },
|
|
12156
|
+
{ validatorId: 'sort-entry-supported', level: 'error', code: 'PL019', description: 'Sort entries must use the canonical field,direction format or labeled sort option shape.' },
|
|
12157
|
+
{ validatorId: 'i18n-value-supported', level: 'warning', code: 'PL020', description: 'Locale and currency values should be valid BCP 47 / ISO currency values when host validation is available.' },
|
|
12158
|
+
{ validatorId: 'a11y-value-supported', level: 'error', code: 'PL021', description: 'Accessibility labels must be non-empty when provided.' },
|
|
12159
|
+
{ validatorId: 'event-name-supported', level: 'warning', code: 'PL022', description: 'Declarative event names must be stable host event identifiers.' },
|
|
12160
|
+
{ validatorId: 'declared-only-runtime-warning', level: 'warning', code: 'PL023', description: 'Fields documented as declared-only may round-trip through config/editor without changing current runtime behavior.' },
|
|
12161
|
+
{ validatorId: 'row-layout-supported', level: 'error', code: 'PL024', description: 'Row layout columns must use supported slots and enum values.' },
|
|
12162
|
+
{ validatorId: 'row-layout-placement-reachable', level: 'warning', code: 'PL025', description: 'Action and expansion placements must be reachable from the active row layout.' },
|
|
12163
|
+
{ validatorId: 'interaction-value-supported', level: 'error', code: 'PL026', description: 'Interaction expansion values must match the runtime enum contract.' },
|
|
12164
|
+
{ validatorId: 'rule-id-stable', level: 'error', code: 'PL027', description: 'List style and slot override rules must declare stable unique ids.' },
|
|
12165
|
+
{ validatorId: 'expansion-section-id-stable', level: 'error', code: 'PL028', description: 'Expansion sections must declare stable unique ids.' },
|
|
12166
|
+
{ validatorId: 'expansion-value-supported', level: 'error', code: 'PL029', description: 'Expansion section, data source, schema contract and rendering values must match the runtime contract.' },
|
|
12167
|
+
],
|
|
12168
|
+
roundTripRequirements: [
|
|
12169
|
+
'List editor must preserve templating.primary, templating.secondary, templating.leading, templating.trailing and templating.emptyState.',
|
|
12170
|
+
'Actions must round-trip by actions[].id rather than array index.',
|
|
12171
|
+
'Rules must round-trip by rules.itemStyles[].id and rules.slotOverrides[].id rather than array index.',
|
|
12172
|
+
'Expansion sections must round-trip by expansion.sections[].id rather than array index.',
|
|
12173
|
+
'Row layout must preserve executive slots identity, balance, limit, risk, alerts, owner, actions and expand.',
|
|
12174
|
+
'dataSource.data precedence over dataSource.resourcePath must remain explicit when binding remote resources.',
|
|
12175
|
+
],
|
|
12176
|
+
examples: [
|
|
12177
|
+
{
|
|
12178
|
+
id: 'set-list-id',
|
|
12179
|
+
request: 'Set the stable list id to employees-list.',
|
|
12180
|
+
operationId: 'list.id.set',
|
|
12181
|
+
params: { id: 'employees-list' },
|
|
12182
|
+
isPositive: true,
|
|
12183
|
+
},
|
|
12184
|
+
{
|
|
12185
|
+
id: 'bind-title-subtitle-fields',
|
|
12186
|
+
request: 'Use name as title and department as subtitle.',
|
|
12187
|
+
operationId: 'item.primaryText.set',
|
|
12188
|
+
params: { type: 'text', expr: '${item.name}' },
|
|
12189
|
+
isPositive: true,
|
|
12190
|
+
},
|
|
12191
|
+
{
|
|
12192
|
+
id: 'bind-secondary-field',
|
|
12193
|
+
request: 'Show department below the title.',
|
|
12194
|
+
operationId: 'item.secondaryText.set',
|
|
12195
|
+
params: { type: 'text', expr: '${item.department}' },
|
|
12196
|
+
isPositive: true,
|
|
12197
|
+
},
|
|
12198
|
+
{
|
|
12199
|
+
id: 'configure-avatar-image',
|
|
12200
|
+
request: 'Use photoUrl as the item avatar.',
|
|
12201
|
+
operationId: 'item.avatar.configure',
|
|
12202
|
+
params: { type: 'image', expr: '${item.photoUrl}', imageAlt: 'Employee photo' },
|
|
12203
|
+
isPositive: true,
|
|
12204
|
+
},
|
|
12205
|
+
{
|
|
12206
|
+
id: 'set-template-slot',
|
|
12207
|
+
request: 'Set the meta slot to show the employee score.',
|
|
12208
|
+
operationId: 'template.slot.set',
|
|
12209
|
+
params: { slot: 'meta', template: { type: 'metric', expr: '${item.score}' } },
|
|
12210
|
+
isPositive: true,
|
|
12211
|
+
},
|
|
12212
|
+
{
|
|
12213
|
+
id: 'add-item-action',
|
|
12214
|
+
request: 'Add an edit action to every item.',
|
|
12215
|
+
operationId: 'item.action.add',
|
|
12216
|
+
params: { id: 'edit', label: 'Edit', icon: 'edit', kind: 'icon' },
|
|
12217
|
+
isPositive: true,
|
|
12218
|
+
},
|
|
12219
|
+
{
|
|
12220
|
+
id: 'update-item-action-placement',
|
|
12221
|
+
request: 'Move the edit action into the dedicated actions column and show loading.',
|
|
12222
|
+
operationId: 'item.action.update',
|
|
12223
|
+
params: { id: 'edit', placement: 'actions', showLoading: true },
|
|
12224
|
+
isPositive: true,
|
|
12225
|
+
},
|
|
12226
|
+
{
|
|
12227
|
+
id: 'remove-item-action-with-confirmation',
|
|
12228
|
+
request: 'Remove the edit action after confirmation.',
|
|
12229
|
+
operationId: 'item.action.remove',
|
|
12230
|
+
params: { id: 'edit' },
|
|
12231
|
+
isPositive: true,
|
|
12232
|
+
},
|
|
12233
|
+
{
|
|
12234
|
+
id: 'configure-badge-from-status',
|
|
12235
|
+
request: 'Show a status badge in the item corner.',
|
|
12236
|
+
operationId: 'item.badge.configure',
|
|
12237
|
+
params: { expr: '${item.status}', statusPosition: 'top-right', color: 'primary' },
|
|
12238
|
+
isPositive: true,
|
|
12239
|
+
},
|
|
12240
|
+
{
|
|
12241
|
+
id: 'switch-selection-mode',
|
|
12242
|
+
request: 'Allow selecting multiple employees by id.',
|
|
12243
|
+
operationId: 'selection.mode.set',
|
|
12244
|
+
params: { mode: 'multiple', compareBy: 'id', return: 'id' },
|
|
12245
|
+
isPositive: true,
|
|
12246
|
+
},
|
|
12247
|
+
{
|
|
12248
|
+
id: 'set-compact-density',
|
|
12249
|
+
request: 'Make the list compact.',
|
|
12250
|
+
operationId: 'layout.density.set',
|
|
12251
|
+
params: { density: 'compact' },
|
|
12252
|
+
isPositive: true,
|
|
12253
|
+
},
|
|
12254
|
+
{
|
|
12255
|
+
id: 'reject-missing-field-binding',
|
|
12256
|
+
request: 'Use missingField as the list title.',
|
|
12257
|
+
operationId: 'item.primaryText.set',
|
|
12258
|
+
params: { type: 'text', expr: '${item.missingField}' },
|
|
12259
|
+
isPositive: false,
|
|
12260
|
+
},
|
|
12261
|
+
{
|
|
12262
|
+
id: 'configure-list-skin',
|
|
12263
|
+
request: 'Use an elevated card skin with rounded corners.',
|
|
12264
|
+
operationId: 'skin.configure',
|
|
12265
|
+
params: { type: 'elevated', radius: '8px', shadow: 'var(--mat-sys-level2)' },
|
|
12266
|
+
isPositive: true,
|
|
12267
|
+
},
|
|
12268
|
+
{
|
|
12269
|
+
id: 'configure-list-toolbar',
|
|
12270
|
+
request: 'Show search by name and sort by created date.',
|
|
12271
|
+
operationId: 'ui.toolbar.configure',
|
|
12272
|
+
params: {
|
|
12273
|
+
showSearch: true,
|
|
12274
|
+
searchField: 'name',
|
|
12275
|
+
showSort: true,
|
|
12276
|
+
sortOptions: [{ label: 'Newest', value: 'createdAt,desc' }],
|
|
12277
|
+
},
|
|
12278
|
+
isPositive: true,
|
|
12279
|
+
},
|
|
12280
|
+
{
|
|
12281
|
+
id: 'configure-accessibility-label',
|
|
12282
|
+
request: 'Set an accessible label for the customer list.',
|
|
12283
|
+
operationId: 'a11y.configure',
|
|
12284
|
+
params: { ariaLabel: 'Customer list' },
|
|
12285
|
+
isPositive: true,
|
|
12286
|
+
},
|
|
12287
|
+
{
|
|
12288
|
+
id: 'map-declarative-events',
|
|
12289
|
+
request: 'Map selection changes to the customerSelectionChanged event name.',
|
|
12290
|
+
operationId: 'events.map',
|
|
12291
|
+
params: { selectionChange: 'customerSelectionChanged' },
|
|
12292
|
+
isPositive: true,
|
|
12293
|
+
},
|
|
12294
|
+
{
|
|
12295
|
+
id: 'configure-template-display',
|
|
12296
|
+
request: 'Move status chips to the top right and hide feature labels.',
|
|
12297
|
+
operationId: 'templating.display.configure',
|
|
12298
|
+
params: { statusPosition: 'top-right', featuresMode: 'icons-only' },
|
|
12299
|
+
isPositive: true,
|
|
12300
|
+
},
|
|
12301
|
+
{
|
|
12302
|
+
id: 'set-feature-line',
|
|
12303
|
+
request: 'Show the amenities feature line.',
|
|
12304
|
+
operationId: 'templating.features.set',
|
|
12305
|
+
params: { features: [{ icon: 'wifi', expr: '${item.hasWifi}' }] },
|
|
12306
|
+
isPositive: true,
|
|
12307
|
+
},
|
|
12308
|
+
{
|
|
12309
|
+
id: 'configure-skeleton-count',
|
|
12310
|
+
request: 'Render six skeleton rows while loading.',
|
|
12311
|
+
operationId: 'templating.skeleton.configure',
|
|
12312
|
+
params: { count: 6 },
|
|
12313
|
+
isPositive: true,
|
|
12314
|
+
},
|
|
12315
|
+
{
|
|
12316
|
+
id: 'override-risk-slot',
|
|
12317
|
+
request: 'Show a warning icon in the risk slot for high-risk rows.',
|
|
12318
|
+
operationId: 'rules.slotOverride.upsert',
|
|
12319
|
+
params: {
|
|
12320
|
+
id: 'high-risk-icon',
|
|
12321
|
+
slot: 'risk',
|
|
12322
|
+
condition: { '==': [{ var: 'row.risk' }, 'high'] },
|
|
12323
|
+
template: { type: 'icon', expr: 'warning', color: 'warn' },
|
|
12324
|
+
},
|
|
12325
|
+
isPositive: true,
|
|
12326
|
+
},
|
|
12327
|
+
{
|
|
12328
|
+
id: 'set-empty-state',
|
|
12329
|
+
request: 'Show an empty employee message.',
|
|
12330
|
+
operationId: 'emptyState.set',
|
|
12331
|
+
params: { type: 'text', expr: 'No employees found' },
|
|
12332
|
+
isPositive: true,
|
|
12333
|
+
},
|
|
12334
|
+
{
|
|
12335
|
+
id: 'bind-remote-resource',
|
|
12336
|
+
request: 'Bind the list to the employees resource.',
|
|
12337
|
+
operationId: 'data.resource.bind',
|
|
12338
|
+
params: { resourcePath: 'employees', sort: ['name,asc'] },
|
|
12339
|
+
isPositive: true,
|
|
12340
|
+
},
|
|
12341
|
+
{
|
|
12342
|
+
id: 'set-local-data',
|
|
12343
|
+
request: 'Use a local employee array for the list.',
|
|
12344
|
+
operationId: 'data.local.set',
|
|
12345
|
+
params: { data: [{ id: 1, name: 'Ana' }] },
|
|
12346
|
+
isPositive: true,
|
|
12347
|
+
},
|
|
12348
|
+
{
|
|
12349
|
+
id: 'set-remote-query',
|
|
12350
|
+
request: 'Filter remote employees by active status.',
|
|
12351
|
+
operationId: 'data.query.set',
|
|
12352
|
+
params: { query: { status: 'active' } },
|
|
12353
|
+
isPositive: true,
|
|
12354
|
+
},
|
|
12355
|
+
{
|
|
12356
|
+
id: 'set-remote-sort',
|
|
12357
|
+
request: 'Sort remote employees by name ascending.',
|
|
12358
|
+
operationId: 'data.sort.set',
|
|
12359
|
+
params: { sort: ['name,asc'] },
|
|
12360
|
+
isPositive: true,
|
|
12361
|
+
},
|
|
12362
|
+
{
|
|
12363
|
+
id: 'configure-list-i18n',
|
|
12364
|
+
request: 'Use Brazilian Portuguese and BRL defaults.',
|
|
12365
|
+
operationId: 'i18n.configure',
|
|
12366
|
+
params: { locale: 'pt-BR', currency: 'BRL' },
|
|
12367
|
+
isPositive: true,
|
|
12368
|
+
},
|
|
12369
|
+
{
|
|
12370
|
+
id: 'configure-row-layout-executive-slots',
|
|
12371
|
+
request: 'Use an executive row with identity, balance and actions columns.',
|
|
12372
|
+
operationId: 'layout.rowLayout.configure',
|
|
12373
|
+
params: {
|
|
12374
|
+
type: 'grid',
|
|
12375
|
+
columns: [
|
|
12376
|
+
{ slot: 'identity', width: '280px' },
|
|
12377
|
+
{ slot: 'balance', width: '1fr' },
|
|
12378
|
+
{ slot: 'actions', width: 'auto' },
|
|
12379
|
+
],
|
|
12380
|
+
},
|
|
12381
|
+
isPositive: true,
|
|
12382
|
+
},
|
|
12383
|
+
{
|
|
12384
|
+
id: 'enable-inline-expansion',
|
|
12385
|
+
request: 'Allow expanding one row at a time using the expand icon.',
|
|
12386
|
+
operationId: 'interaction.expansion.configure',
|
|
12387
|
+
params: { expandable: true, expandTrigger: 'icon', expandMode: 'single', expandPlacement: 'expand' },
|
|
12388
|
+
isPositive: true,
|
|
12389
|
+
},
|
|
12390
|
+
{
|
|
12391
|
+
id: 'add-expansion-section',
|
|
12392
|
+
request: 'Add a detail expansion section backed by item details.',
|
|
12393
|
+
operationId: 'expansion.configure',
|
|
12394
|
+
params: {
|
|
12395
|
+
sections: [{ id: 'details', type: 'metadata', title: 'Details', itemsExpr: '${item.details}' }],
|
|
12396
|
+
},
|
|
12397
|
+
isPositive: true,
|
|
12398
|
+
},
|
|
12399
|
+
{
|
|
12400
|
+
id: 'style-high-risk-items',
|
|
12401
|
+
request: 'Highlight rows where risk is high.',
|
|
12402
|
+
operationId: 'rules.itemStyle.upsert',
|
|
12403
|
+
params: {
|
|
12404
|
+
id: 'high-risk',
|
|
12405
|
+
condition: { '==': [{ var: 'row.risk' }, 'high'] },
|
|
12406
|
+
class: 'risk-high',
|
|
12407
|
+
},
|
|
12408
|
+
isPositive: true,
|
|
12409
|
+
},
|
|
12410
|
+
],
|
|
12411
|
+
};
|
|
12412
|
+
|
|
10221
12413
|
function createDemoCover(label, from, to) {
|
|
10222
12414
|
const svg = `
|
|
10223
12415
|
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="220" viewBox="0 0 360 220">
|
|
@@ -11494,4 +13686,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
11494
13686
|
* Generated bundle index. Do not edit.
|
|
11495
13687
|
*/
|
|
11496
13688
|
|
|
11497
|
-
export { ExecutiveAlertsComponent, ExecutiveBadgeComponent, ExecutiveOwnerComponent, LIST_AI_CAPABILITIES, ListDataService, ListSkinService, PRAXIS_LIST_COMPONENT_METADATA, PRAXIS_LIST_EN_US, PRAXIS_LIST_I18N_NAMESPACE, PRAXIS_LIST_PT_BR, PraxisList, PraxisListConfigEditor, PraxisListDocPageComponent, PraxisListJsonConfigEditorComponent, adaptSelection, buildListApplyPlan, createListAuthoringDocument, evalExpr, evaluateTemplate, inferListAuthoringDocument, inferTemplatingFromSchema, isListTemplateSupportedByRichContentP0, mapListTemplateToRichContentP0, normalizeListActionPayloads, normalizeListAuthoringDocument, normalizeListConfig, parseLegacyOrListDocument, projectListAuthoringDocument, providePraxisListI18n, providePraxisListMetadata, serializeListAuthoringDocument, toCanonicalListConfig, validateListAuthoringDocument };
|
|
13689
|
+
export { ExecutiveAlertsComponent, ExecutiveBadgeComponent, ExecutiveOwnerComponent, LIST_AI_CAPABILITIES, ListDataService, ListSkinService, PRAXIS_LIST_AUTHORING_MANIFEST, PRAXIS_LIST_COMPONENT_METADATA, PRAXIS_LIST_EN_US, PRAXIS_LIST_I18N_NAMESPACE, PRAXIS_LIST_PT_BR, PraxisList, PraxisListConfigEditor, PraxisListDocPageComponent, PraxisListJsonConfigEditorComponent, adaptSelection, buildListApplyPlan, createListAuthoringDocument, evalExpr, evaluateTemplate, inferListAuthoringDocument, inferTemplatingFromSchema, isListTemplateSupportedByRichContentP0, mapListTemplateToRichContentP0, normalizeListActionPayloads, normalizeListAuthoringDocument, normalizeListConfig, parseLegacyOrListDocument, projectListAuthoringDocument, providePraxisListI18n, providePraxisListMetadata, serializeListAuthoringDocument, toCanonicalListConfig, validateListAuthoringDocument };
|