@praxisui/list 1.0.0-beta.29 → 1.0.0-beta.30

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.
@@ -16,21 +16,23 @@ import * as i4$1 from '@angular/material/form-field';
16
16
  import { MatFormFieldModule } from '@angular/material/form-field';
17
17
  import * as i6 from '@angular/material/select';
18
18
  import { MatSelectModule } from '@angular/material/select';
19
+ import * as i5 from '@angular/material/input';
20
+ import { MatInputModule } from '@angular/material/input';
19
21
  import * as i2 from '@angular/forms';
20
22
  import { FormsModule, FormControl, ReactiveFormsModule } from '@angular/forms';
21
- import { BehaviorSubject, combineLatest, of, Subject } from 'rxjs';
23
+ import { BehaviorSubject, combineLatest, of, Subject, debounceTime as debounceTime$1, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
22
24
  import { auditTime, switchMap, map, catchError, finalize, shareReplay, debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
23
25
  import { SETTINGS_PANEL_DATA, SettingsPanelService } from '@praxisui/settings-panel';
24
26
  import * as i3$1 from '@angular/material/tabs';
25
27
  import { MatTabsModule } from '@angular/material/tabs';
26
- import * as i5 from '@angular/material/input';
27
- import { MatInputModule } from '@angular/material/input';
28
28
  import * as i8 from '@angular/material/slide-toggle';
29
29
  import { MatSlideToggleModule } from '@angular/material/slide-toggle';
30
30
  import * as i11 from '@angular/material/button-toggle';
31
31
  import { MatButtonToggleModule } from '@angular/material/button-toggle';
32
32
  import * as i12 from '@angular/material/tooltip';
33
33
  import { MatTooltipModule } from '@angular/material/tooltip';
34
+ import * as i15 from '@angular/material/menu';
35
+ import { MatMenuModule } from '@angular/material/menu';
34
36
  import * as i10 from '@angular/material/expansion';
35
37
  import { MatExpansionModule } from '@angular/material/expansion';
36
38
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -100,7 +102,9 @@ function evalExpr(expr, ctx) {
100
102
  function evaluateTemplate(def, item) {
101
103
  if (!def)
102
104
  return null;
103
- const { baseExpr, pipe } = splitFirstPipe(def.expr);
105
+ // Support a top-level pipe outside of template placeholders,
106
+ // e.g.: "${item.active}|bool:Ativo:Inativo".
107
+ const { baseExpr, pipe } = splitFirstTopLevelPipe(def.expr);
104
108
  let value = evalExpr(baseExpr, { item });
105
109
  if (pipe) {
106
110
  const { name, args } = parsePipe(pipe);
@@ -112,7 +116,7 @@ function evaluateTemplate(def, item) {
112
116
  }
113
117
  const out = { type: def.type, value, class: def.class, style: def.style, color: def.color, variant: def.variant };
114
118
  if (def.badge?.expr) {
115
- const { baseExpr: b2, pipe: p2 } = splitFirstPipe(def.badge.expr);
119
+ const { baseExpr: b2, pipe: p2 } = splitFirstTopLevelPipe(def.badge.expr);
116
120
  let badgeVal = evalExpr(b2, { item });
117
121
  if (p2) {
118
122
  const { name, args } = parsePipe(p2);
@@ -136,6 +140,26 @@ function splitFirstPipe(expr) {
136
140
  return { baseExpr: expr.trim() };
137
141
  return { baseExpr: expr.slice(0, idx).trim(), pipe: expr.slice(idx + 1).trim() };
138
142
  }
143
+ // Find first '|' that is NOT inside a "${...}" placeholder
144
+ function splitFirstTopLevelPipe(expr) {
145
+ let inTpl = 0;
146
+ for (let i = 0; i < expr.length; i++) {
147
+ const c = expr[i];
148
+ if (c === '$' && expr[i + 1] === '{') {
149
+ inTpl++;
150
+ i++;
151
+ continue;
152
+ }
153
+ if (c === '}' && inTpl > 0) {
154
+ inTpl--;
155
+ continue;
156
+ }
157
+ if (c === '|' && inTpl === 0) {
158
+ return { baseExpr: expr.slice(0, i).trim(), pipe: expr.slice(i + 1).trim() };
159
+ }
160
+ }
161
+ return { baseExpr: expr.trim() };
162
+ }
139
163
  function parsePipe(pipeSpec) {
140
164
  const idx = pipeSpec.indexOf(':');
141
165
  if (idx === -1)
@@ -214,6 +238,7 @@ class ListDataService {
214
238
  query$ = new BehaviorSubject({});
215
239
  loading$ = new BehaviorSubject(false);
216
240
  total$ = new BehaviorSubject(0);
241
+ pageState$ = this.pageable$.asObservable();
217
242
  lastSig = '';
218
243
  crud = inject(GenericCrudService, {
219
244
  optional: true,
@@ -314,6 +339,9 @@ class ListDataService {
314
339
  }
315
340
  setQuery(q) {
316
341
  this.query$.next(q || {});
342
+ // Reset to first page when query changes
343
+ const p = this.pageable$.value;
344
+ this.pageable$.next({ ...p, pageNumber: 0 });
317
345
  }
318
346
  groupedStream() {
319
347
  return this.stream().pipe(switchMap((items) => this.config$.pipe(map((cfg) => cfg?.layout?.groupBy), map((groupBy) => {
@@ -515,6 +543,7 @@ class PraxisListConfigEditor {
515
543
  selection: { mode: 'none', return: 'value' },
516
544
  skin: { type: 'elevated', gradient: { from: '', to: '', angle: 135 } },
517
545
  i18n: { locale: 'en-US', currency: 'USD' },
546
+ ui: {},
518
547
  };
519
548
  isDirty$ = new BehaviorSubject(false);
520
549
  isValid$ = new BehaviorSubject(true);
@@ -533,6 +562,10 @@ class PraxisListConfigEditor {
533
562
  mappingLeading = { type: 'icon' };
534
563
  mapping = {};
535
564
  mappingDirty = false;
565
+ statusPosition = undefined;
566
+ iconColorMapEntries = [];
567
+ // UI (search/sort/range) editor state
568
+ uiSortRows = [];
536
569
  // Meta composition (multi-field)
537
570
  mappingMetaFields = [];
538
571
  mappingMetaSeparator = ' • ';
@@ -552,6 +585,7 @@ class PraxisListConfigEditor {
552
585
  this.working = this.normalize(structuredClone(cfg));
553
586
  // Initialize mapping UI from existing templating
554
587
  this.hydrateMappingFromTemplating(this.working.templating);
588
+ this.hydrateUiEditorFromConfig();
555
589
  }
556
590
  this.initialJson = JSON.stringify(this.working);
557
591
  this.lastJson = this.initialJson;
@@ -632,6 +666,7 @@ class PraxisListConfigEditor {
632
666
  class: cfg.skin?.class,
633
667
  },
634
668
  i18n: cfg.i18n || {},
669
+ ui: cfg.ui || {},
635
670
  };
636
671
  return out;
637
672
  }
@@ -807,6 +842,72 @@ class PraxisListConfigEditor {
807
842
  this.markDirty();
808
843
  this.verify();
809
844
  }
845
+ onUiChanged() {
846
+ this.markDirty();
847
+ this.verify();
848
+ }
849
+ addUiSortRow() {
850
+ this.uiSortRows = [...(this.uiSortRows || []), { label: '', field: '', dir: 'desc' }];
851
+ this.onUiSortRowsChanged();
852
+ }
853
+ removeUiSortRow(i) {
854
+ const arr = [...(this.uiSortRows || [])];
855
+ arr.splice(i, 1);
856
+ this.uiSortRows = arr;
857
+ this.onUiSortRowsChanged();
858
+ }
859
+ onUiSortRowsChanged() {
860
+ // Map rows to config.ui.sortOptions
861
+ const opts = (this.uiSortRows || [])
862
+ .filter(r => (r.field || '').trim())
863
+ .map(r => ({ label: (r.label || '').trim() || `${r.field},${r.dir || 'desc'}`, value: `${r.field},${r.dir || 'desc'}` }));
864
+ this.working = produce(this.working, (draft) => {
865
+ (draft.ui ||= {}).sortOptions = opts;
866
+ });
867
+ this.onUiChanged();
868
+ }
869
+ isUiSortRowDuplicate(index) {
870
+ const rows = this.uiSortRows || [];
871
+ const key = `${rows[index]?.field || ''},${rows[index]?.dir || 'desc'}`.trim();
872
+ if (!key || !rows[index]?.field)
873
+ return false;
874
+ let count = 0;
875
+ for (const r of rows) {
876
+ const k = `${r?.field || ''},${r?.dir || 'desc'}`.trim();
877
+ if (k === key)
878
+ count++;
879
+ }
880
+ return count > 1;
881
+ }
882
+ isIconColorDuplicate(index) {
883
+ const arr = this.iconColorMapEntries || [];
884
+ const key = (arr[index]?.key || '').trim();
885
+ if (!key)
886
+ return false;
887
+ let count = 0;
888
+ for (const e of arr) {
889
+ if ((e?.key || '').trim() === key)
890
+ count++;
891
+ }
892
+ return count > 1;
893
+ }
894
+ hydrateUiEditorFromConfig() {
895
+ const ui = (this.working.ui ||= {});
896
+ const arr = ui.sortOptions || [];
897
+ const rows = [];
898
+ for (const op of arr) {
899
+ if (typeof op === 'string') {
900
+ const [f, d] = op.split(',');
901
+ rows.push({ label: op, field: f, dir: d || 'asc' });
902
+ }
903
+ else if (op && typeof op === 'object') {
904
+ const v = String(op.value || '');
905
+ const [f, d] = v.split(',');
906
+ rows.push({ label: op.label, field: f, dir: d || 'asc' });
907
+ }
908
+ }
909
+ this.uiSortRows = rows;
910
+ }
810
911
  // Legacy method retained to avoid breaking references; no-op now.
811
912
  loadFieldsIfNeeded() { }
812
913
  updateSortConfig() {
@@ -828,8 +929,10 @@ class PraxisListConfigEditor {
828
929
  if (!slot?.field)
829
930
  return undefined;
830
931
  const base = '${item.' + slot.field + '}';
831
- if (slot.type === 'text')
832
- return { type: 'text', expr: base, class: slot.class, style: slot.style };
932
+ if (slot.type === 'text') {
933
+ const pipe = (slot.extraPipe || '').trim();
934
+ return { type: 'text', expr: pipe ? base + '|' + pipe : base, class: slot.class, style: slot.style };
935
+ }
833
936
  if (slot.type === 'chip')
834
937
  return { type: 'chip', expr: base, class: slot.class, style: slot.style, color: slot.chipColor, variant: slot.chipVariant };
835
938
  if (slot.type === 'rating')
@@ -852,6 +955,11 @@ class PraxisListConfigEditor {
852
955
  param = ':' + style;
853
956
  return { type: 'date', expr: param ? base + '|' + param : base, class: slot.class, style: slot.style };
854
957
  }
958
+ if (slot.type === 'icon') {
959
+ const pipe = (slot.extraPipe || '').trim();
960
+ const expr = pipe ? base + '|' + pipe : base;
961
+ return { type: 'icon', expr, class: slot.class, style: slot.style };
962
+ }
855
963
  return undefined;
856
964
  };
857
965
  const p = build(this.mappingPrimary);
@@ -900,6 +1008,22 @@ class PraxisListConfigEditor {
900
1008
  t.metaPlacement = this.mappingMeta.placement;
901
1009
  }
902
1010
  t.metaPrefixIcon = (this.mappingMetaPrefixIcon || '').trim() || undefined;
1011
+ // Status overlay and icon color map
1012
+ t.statusPosition = this.statusPosition || undefined;
1013
+ if (this.iconColorMapEntries && this.iconColorMapEntries.length) {
1014
+ const map = {};
1015
+ for (const e of this.iconColorMapEntries) {
1016
+ const k = (e.key || '').trim();
1017
+ if (!k)
1018
+ continue;
1019
+ if (e.color)
1020
+ map[k] = e.color;
1021
+ }
1022
+ t.iconColorMap = Object.keys(map).length ? map : undefined;
1023
+ }
1024
+ else {
1025
+ t.iconColorMap = undefined;
1026
+ }
903
1027
  // Apply features
904
1028
  if (this.features && this.features.length) {
905
1029
  t.features = this.features
@@ -945,6 +1069,37 @@ class PraxisListConfigEditor {
945
1069
  this.mappingDirty = true;
946
1070
  this.isDirty$.next(true);
947
1071
  }
1072
+ // Icon color map editor helpers
1073
+ addIconColorEntry() {
1074
+ this.iconColorMapEntries = [...(this.iconColorMapEntries || []), { key: '', color: undefined }];
1075
+ this.onIconColorMapChanged();
1076
+ }
1077
+ removeIconColorEntry(i) {
1078
+ const arr = [...(this.iconColorMapEntries || [])];
1079
+ arr.splice(i, 1);
1080
+ this.iconColorMapEntries = arr;
1081
+ this.onIconColorMapChanged();
1082
+ }
1083
+ onIconColorMapChanged() {
1084
+ // Normalize: last value wins, remove vazios
1085
+ const map = new Map();
1086
+ for (const e of (this.iconColorMapEntries || [])) {
1087
+ const k = (e?.key || '').trim();
1088
+ if (!k)
1089
+ continue;
1090
+ map.set(k, e?.color || undefined);
1091
+ }
1092
+ this.iconColorMapEntries = Array.from(map.entries()).map(([key, color]) => ({ key, color }));
1093
+ this.mappingDirty = true;
1094
+ this.isDirty$.next(true);
1095
+ }
1096
+ // Pipe assistant helper: sets extraPipe for a given mapping and marks dirty
1097
+ setPipe(target, spec) {
1098
+ if (!target)
1099
+ return;
1100
+ target.extraPipe = spec;
1101
+ this.onMappingChanged();
1102
+ }
948
1103
  // ===============
949
1104
  // Expr helpers (Features): click-to-insert fields and literals
950
1105
  // ===============
@@ -1023,6 +1178,8 @@ class PraxisListConfigEditor {
1023
1178
  Object.assign(base, parseCurrency(param));
1024
1179
  if (templ.primary.type === 'date')
1025
1180
  Object.assign(base, parseDate(param));
1181
+ if (templ.primary.type === 'text')
1182
+ base.extraPipe = param;
1026
1183
  this.mappingPrimary = base;
1027
1184
  }
1028
1185
  if (templ.secondary) {
@@ -1032,6 +1189,8 @@ class PraxisListConfigEditor {
1032
1189
  Object.assign(base, parseCurrency(param));
1033
1190
  if (templ.secondary.type === 'date')
1034
1191
  Object.assign(base, parseDate(param));
1192
+ if (templ.secondary.type === 'text')
1193
+ base.extraPipe = param;
1035
1194
  this.mappingSecondary = base;
1036
1195
  }
1037
1196
  if (templ.meta) {
@@ -1041,6 +1200,8 @@ class PraxisListConfigEditor {
1041
1200
  Object.assign(base, parseCurrency(param));
1042
1201
  if (templ.meta.type === 'date')
1043
1202
  Object.assign(base, parseDate(param));
1203
+ if (templ.meta.type === 'text')
1204
+ base.extraPipe = param;
1044
1205
  this.mappingMeta = base;
1045
1206
  if (templ.metaPlacement)
1046
1207
  this.mappingMeta.placement = templ.metaPlacement;
@@ -1068,12 +1229,27 @@ class PraxisListConfigEditor {
1068
1229
  Object.assign(base, parseCurrency(param));
1069
1230
  if (templ.trailing.type === 'date')
1070
1231
  Object.assign(base, parseDate(param));
1232
+ if (templ.trailing.type === 'icon' || templ.trailing.type === 'text')
1233
+ base.extraPipe = param;
1071
1234
  this.mappingTrailing = base;
1072
1235
  if (templ.trailing?.color)
1073
1236
  this.mappingTrailing.chipColor = templ.trailing.color;
1074
1237
  if (templ.trailing?.variant)
1075
1238
  this.mappingTrailing.chipVariant = templ.trailing.variant;
1076
1239
  }
1240
+ // Overlay position and icon colors
1241
+ this.statusPosition = templ?.statusPosition;
1242
+ const icm = templ?.iconColorMap;
1243
+ this.iconColorMapEntries = Object.entries(icm || {}).map(([k, v]) => ({ key: k, color: v }));
1244
+ if ((this.mappingTrailing?.field || '').toLowerCase() === 'status' && (!this.iconColorMapEntries || this.iconColorMapEntries.length === 0)) {
1245
+ this.iconColorMapEntries = [
1246
+ { key: 'PLANEJADA', color: 'primary' },
1247
+ { key: 'EM_ANDAMENTO', color: 'accent' },
1248
+ { key: 'PAUSADA', color: 'accent' },
1249
+ { key: 'CONCLUIDA', color: 'primary' },
1250
+ { key: 'FALHOU', color: 'warn' },
1251
+ ];
1252
+ }
1077
1253
  if (templ.leading?.type === 'icon' && typeof templ.leading.expr === 'string') {
1078
1254
  this.mappingLeading = { type: 'icon', icon: templ.leading.expr };
1079
1255
  }
@@ -1117,8 +1293,27 @@ class PraxisListConfigEditor {
1117
1293
  const compareBy = (this.working?.selection?.compareBy || '').trim();
1118
1294
  if (compareBy && this.fields.length && !this.fields.includes(compareBy))
1119
1295
  valid = false;
1296
+ // Duplicatas em opções de ordenação (UI) tornam inválido
1297
+ if (valid && this.working?.ui?.showSort) {
1298
+ const rows = this.uiSortRows || [];
1299
+ const seen = new Map();
1300
+ for (const r of rows) {
1301
+ const k = `${(r?.field || '').trim()},${(r?.dir || 'desc').trim()}`;
1302
+ if (!r?.field)
1303
+ continue;
1304
+ seen.set(k, (seen.get(k) || 0) + 1);
1305
+ }
1306
+ for (const [, count] of seen) {
1307
+ if (count > 1) {
1308
+ valid = false;
1309
+ break;
1310
+ }
1311
+ }
1312
+ }
1120
1313
  this.isValid$.next(valid);
1121
1314
  }
1315
+ // UI editor hydration already called in ctor
1316
+ // Ensure UI.sortOptions are kept when saving via Settings Panel
1122
1317
  inferFromFields() {
1123
1318
  if (!this.fields?.length)
1124
1319
  return;
@@ -1216,6 +1411,52 @@ class PraxisListConfigEditor {
1216
1411
  </mat-select>
1217
1412
  </mat-form-field>
1218
1413
  </div>
1414
+ <mat-divider class="my-8"></mat-divider>
1415
+ <div class="subtitle">UI (Buscar / Ordenar / Rodapé)</div>
1416
+ <div class="g g-auto-220 gap-12 ai-end">
1417
+ <mat-slide-toggle [(ngModel)]="working.ui.showSearch" (ngModelChange)="onUiChanged()">Mostrar busca</mat-slide-toggle>
1418
+ <mat-slide-toggle [(ngModel)]="working.ui.showSort" (ngModelChange)="onUiChanged()">Mostrar ordenar</mat-slide-toggle>
1419
+ <mat-slide-toggle [(ngModel)]="working.ui.showRange" (ngModelChange)="onUiChanged()">Mostrar faixa X–Y de Total</mat-slide-toggle>
1420
+ </div>
1421
+ <div class="g g-auto-220 gap-12 ai-end mt-12" *ngIf="working.ui?.showSearch">
1422
+ <mat-form-field appearance="outline">
1423
+ <mat-label>Campo para buscar</mat-label>
1424
+ <mat-select [(ngModel)]="working.ui.searchField" (ngModelChange)="onUiChanged()">
1425
+ <mat-option *ngFor="let f of fields" [value]="f">{{ f }}</mat-option>
1426
+ </mat-select>
1427
+ </mat-form-field>
1428
+ <mat-form-field appearance="outline">
1429
+ <mat-label>Placeholder</mat-label>
1430
+ <input matInput [(ngModel)]="working.ui.searchPlaceholder" (ngModelChange)="onUiChanged()" placeholder="ex.: Buscar por título" />
1431
+ </mat-form-field>
1432
+ </div>
1433
+ <div class="mt-12" *ngIf="working.ui?.showSort">
1434
+ <div class="g g-1-auto ai-center gap-8">
1435
+ <div class="muted">Opções de ordenação (rótulo → campo+direção)</div>
1436
+ <button mat-flat-button color="primary" (click)="addUiSortRow()">Adicionar opção</button>
1437
+ </div>
1438
+ <div class="g g-auto-220 gap-12 ai-end mt-12" *ngFor="let r of uiSortRows; let i = index">
1439
+ <mat-form-field appearance="outline">
1440
+ <mat-label>Rótulo</mat-label>
1441
+ <input matInput [(ngModel)]="r.label" (ngModelChange)="onUiSortRowsChanged()" placeholder="ex.: Mais recentes" />
1442
+ </mat-form-field>
1443
+ <mat-form-field appearance="outline">
1444
+ <mat-label>Campo</mat-label>
1445
+ <mat-select [(ngModel)]="r.field" (ngModelChange)="onUiSortRowsChanged()">
1446
+ <mat-option *ngFor="let f of fields" [value]="f">{{ f }}</mat-option>
1447
+ </mat-select>
1448
+ </mat-form-field>
1449
+ <mat-form-field appearance="outline">
1450
+ <mat-label>Direção</mat-label>
1451
+ <mat-select [(ngModel)]="r.dir" (ngModelChange)="onUiSortRowsChanged()">
1452
+ <mat-option value="desc">Descendente</mat-option>
1453
+ <mat-option value="asc">Ascendente</mat-option>
1454
+ </mat-select>
1455
+ </mat-form-field>
1456
+ <div class="error" *ngIf="isUiSortRowDuplicate(i)">Opção duplicada (campo+direção)</div>
1457
+ <div class="flex-end"><button mat-button color="warn" (click)="removeUiSortRow(i)">Remover</button></div>
1458
+ </div>
1459
+ </div>
1219
1460
  </div>
1220
1461
  </mat-tab>
1221
1462
  <mat-tab label="Ações">
@@ -1381,6 +1622,18 @@ class PraxisListConfigEditor {
1381
1622
  </mat-select>
1382
1623
  </mat-form-field>
1383
1624
  </ng-container>
1625
+ <mat-form-field appearance="outline" *ngIf="mappingPrimary.type==='text'">
1626
+ <mat-label>Pipe extra (ex.: map:K=V)</mat-label>
1627
+ <input matInput [(ngModel)]="mappingPrimary.extraPipe" (ngModelChange)="onMappingChanged()" placeholder="ex.: map:PLANEJADA=Planejada" />
1628
+ <button mat-icon-button matSuffix [matMenuTriggerFor]="pipeMenuPrimary" aria-label="Assistente de pipes" matTooltip="Assistente de pipes"><mat-icon>build</mat-icon></button>
1629
+ </mat-form-field>
1630
+ <mat-menu #pipeMenuPrimary="matMenu">
1631
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'map:KEY=VALUE,KEY2=VALUE2')"><mat-icon>tune</mat-icon><span>Map: KEY=VALUE…</span></button>
1632
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'number:pt-BR')"><mat-icon>push_pin</mat-icon><span>Número: pt-BR</span></button>
1633
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'number:pt-BR:compact')"><mat-icon>format_list_numbered</mat-icon><span>Número: compacto</span></button>
1634
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'bool:Sim:Não')"><mat-icon>check_circle</mat-icon><span>Bool: Sim/Não</span></button>
1635
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'date:pt-BR:short')"><mat-icon>schedule</mat-icon><span>Data: curto</span></button>
1636
+ </mat-menu>
1384
1637
  <mat-form-field appearance="outline">
1385
1638
  <mat-label>Classe CSS</mat-label>
1386
1639
  <input matInput [(ngModel)]="mappingPrimary.class" (ngModelChange)="onMappingChanged()" />
@@ -1436,6 +1689,18 @@ class PraxisListConfigEditor {
1436
1689
  </mat-select>
1437
1690
  </mat-form-field>
1438
1691
  </ng-container>
1692
+ <mat-form-field appearance="outline" *ngIf="mappingSecondary.type==='text'">
1693
+ <mat-label>Pipe extra (ex.: number:pt-BR:compact)</mat-label>
1694
+ <input matInput [(ngModel)]="mappingSecondary.extraPipe" (ngModelChange)="onMappingChanged()" placeholder="ex.: number:pt-BR:compact" />
1695
+ <button mat-icon-button matSuffix [matMenuTriggerFor]="pipeMenuSecondary" aria-label="Assistente de pipes" matTooltip="Assistente de pipes"><mat-icon>build</mat-icon></button>
1696
+ </mat-form-field>
1697
+ <mat-menu #pipeMenuSecondary="matMenu">
1698
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'map:KEY=VALUE,KEY2=VALUE2')"><mat-icon>tune</mat-icon><span>Map: KEY=VALUE…</span></button>
1699
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'number:pt-BR')"><mat-icon>push_pin</mat-icon><span>Número: pt-BR</span></button>
1700
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'number:pt-BR:compact')"><mat-icon>format_list_numbered</mat-icon><span>Número: compacto</span></button>
1701
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'bool:Sim:Não')"><mat-icon>check_circle</mat-icon><span>Bool: Sim/Não</span></button>
1702
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'date:pt-BR:short')"><mat-icon>schedule</mat-icon><span>Data: curto</span></button>
1703
+ </mat-menu>
1439
1704
  <mat-form-field appearance="outline">
1440
1705
  <mat-label>Classe CSS</mat-label>
1441
1706
  <input matInput [(ngModel)]="mappingSecondary.class" (ngModelChange)="onMappingChanged()" />
@@ -1530,6 +1795,18 @@ class PraxisListConfigEditor {
1530
1795
  </mat-select>
1531
1796
  </mat-form-field>
1532
1797
  </ng-container>
1798
+ <mat-form-field appearance="outline" *ngIf="mappingMeta.type==='text'">
1799
+ <mat-label>Pipe extra (ex.: map:K=V)</mat-label>
1800
+ <input matInput [(ngModel)]="mappingMeta.extraPipe" (ngModelChange)="onMappingChanged()" placeholder="ex.: map:ABERTO=Aberto,FECHADO=Fechado" />
1801
+ <button mat-icon-button matSuffix [matMenuTriggerFor]="pipeMenuMeta" aria-label="Assistente de pipes" matTooltip="Assistente de pipes"><mat-icon>build</mat-icon></button>
1802
+ </mat-form-field>
1803
+ <mat-menu #pipeMenuMeta="matMenu">
1804
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'map:KEY=VALUE,KEY2=VALUE2')"><mat-icon>tune</mat-icon><span>Map: KEY=VALUE…</span></button>
1805
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'number:pt-BR')"><mat-icon>push_pin</mat-icon><span>Número: pt-BR</span></button>
1806
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'number:pt-BR:compact')"><mat-icon>format_list_numbered</mat-icon><span>Número: compacto</span></button>
1807
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'bool:Sim:Não')"><mat-icon>check_circle</mat-icon><span>Bool: Sim/Não</span></button>
1808
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'date:pt-BR:short')"><mat-icon>schedule</mat-icon><span>Data: curto</span></button>
1809
+ </mat-menu>
1533
1810
  </div>
1534
1811
  </mat-expansion-panel>
1535
1812
  <mat-expansion-panel>
@@ -1550,8 +1827,19 @@ class PraxisListConfigEditor {
1550
1827
  <mat-option value="chip">Chip</mat-option>
1551
1828
  <mat-option value="currency">Moeda</mat-option>
1552
1829
  <mat-option value="date">Data</mat-option>
1830
+ <mat-option value="icon">Ícone</mat-option>
1553
1831
  </mat-select>
1554
1832
  </mat-form-field>
1833
+ <mat-form-field appearance="outline" *ngIf="mappingTrailing.type==='text' || mappingTrailing.type==='icon'">
1834
+ <mat-label>Pipe extra (ex.: map:KEY=icon,...)</mat-label>
1835
+ <input matInput [(ngModel)]="mappingTrailing.extraPipe" (ngModelChange)="onMappingChanged()" placeholder="ex.: map:PLANEJADA=event,EM_ANDAMENTO=play_circle" />
1836
+ <button mat-icon-button matSuffix [matMenuTriggerFor]="pipeMenuTrailing" aria-label="Assistente de pipes" matTooltip="Assistente de pipes"><mat-icon>build</mat-icon></button>
1837
+ </mat-form-field>
1838
+ <mat-menu #pipeMenuTrailing="matMenu">
1839
+ <button mat-menu-item (click)="setPipe(mappingTrailing, 'map:PLANEJADA=event,EM_ANDAMENTO=play_circle,PAUSADA=pause_circle,CONCLUIDA=check_circle,FALHOU=error')"><mat-icon>tune</mat-icon><span>Map: status→ícone</span></button>
1840
+ <button mat-menu-item (click)="setPipe(mappingTrailing, 'date:pt-BR:short')"><mat-icon>schedule</mat-icon><span>Data: curto</span></button>
1841
+ <button mat-menu-item (click)="setPipe(mappingTrailing, 'number:pt-BR')"><mat-icon>push_pin</mat-icon><span>Número: pt-BR</span></button>
1842
+ </mat-menu>
1555
1843
  <ng-container *ngIf="mappingTrailing.type==='chip'">
1556
1844
  <mat-form-field appearance="outline">
1557
1845
  <mat-label>Cor do chip</mat-label>
@@ -1603,6 +1891,44 @@ class PraxisListConfigEditor {
1603
1891
  <mat-label>Style inline</mat-label>
1604
1892
  <input matInput [(ngModel)]="mappingTrailing.style" (ngModelChange)="onMappingChanged()" />
1605
1893
  </mat-form-field>
1894
+ <mat-form-field appearance="outline">
1895
+ <mat-label>Posição do status (cards)</mat-label>
1896
+ <mat-select [(ngModel)]="statusPosition" (ngModelChange)="onMappingChanged()">
1897
+ <mat-option [value]="undefined">Inline</mat-option>
1898
+ <mat-option value="inline">Inline</mat-option>
1899
+ <mat-option value="top-right">Canto superior direito</mat-option>
1900
+ </mat-select>
1901
+ </mat-form-field>
1902
+ <div class="subtitle">Cores por status (ícone)</div>
1903
+ <div class="chips-row" *ngIf="(mappingTrailing?.field || '').toLowerCase()==='status'">
1904
+ <span class="muted">Sugestões:</span>
1905
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'PLANEJADA', color:'primary'}]); onIconColorMapChanged()">PLANEJADA</button>
1906
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'EM_ANDAMENTO', color:'accent'}]); onIconColorMapChanged()">EM_ANDAMENTO</button>
1907
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'PAUSADA', color:'accent'}]); onIconColorMapChanged()">PAUSADA</button>
1908
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'CONCLUIDA', color:'primary'}]); onIconColorMapChanged()">CONCLUIDA</button>
1909
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'FALHOU', color:'warn'}]); onIconColorMapChanged()">FALHOU</button>
1910
+ </div>
1911
+ <div class="g g-auto-220 gap-12 ai-end">
1912
+ <ng-container *ngFor="let ent of iconColorMapEntries; let i = index">
1913
+ <mat-form-field appearance="outline">
1914
+ <mat-label>Chave</mat-label>
1915
+ <input matInput [(ngModel)]="ent.key" (ngModelChange)="onIconColorMapChanged()" placeholder="ex.: EM_ANDAMENTO ou play_circle" />
1916
+ </mat-form-field>
1917
+ <mat-form-field appearance="outline">
1918
+ <mat-label>Cor</mat-label>
1919
+ <mat-select [(ngModel)]="ent.color" (ngModelChange)="onIconColorMapChanged()">
1920
+ <mat-option [value]="undefined">Default</mat-option>
1921
+ <mat-option value="primary">Primary</mat-option>
1922
+ <mat-option value="accent">Accent</mat-option>
1923
+ <mat-option value="warn">Warn</mat-option>
1924
+ </mat-select>
1925
+ </mat-form-field>
1926
+ <div class="error" *ngIf="isIconColorDuplicate(i)">Chave duplicada (consolidada)</div>
1927
+ <div class="flex-end"><button mat-button color="warn" (click)="removeIconColorEntry(i)">Remover</button></div>
1928
+ </ng-container>
1929
+ <div><button mat-flat-button color="primary" (click)="addIconColorEntry()">Adicionar cor</button></div>
1930
+ </div>
1931
+ <div class="muted">Entradas vazias são ignoradas e chaves repetidas são mescladas (última vence).</div>
1606
1932
  </div>
1607
1933
  </mat-expansion-panel>
1608
1934
  <mat-expansion-panel>
@@ -1863,7 +2189,7 @@ class PraxisListConfigEditor {
1863
2189
  </div>
1864
2190
  </mat-tab>
1865
2191
  </mat-tab-group>
1866
- `, isInline: true, styles: [".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;opacity:.82}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i3$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i3$1.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: i4$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.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: i6.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: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.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: "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: "component", type: i14.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i14.MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisListSkinPreviewComponent, selector: "praxis-list-skin-preview", inputs: ["config", "items", "theme"] }] });
2192
+ `, isInline: true, styles: [".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;opacity:.82}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--mdc-theme-error,#b00020);font-size:.85rem}.muted{opacity:.7}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i3$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i3$1.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: i4$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.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: i6.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: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.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: "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: "component", type: i14.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "component", type: i14.MatChipSet, selector: "mat-chip-set", inputs: ["disabled", "role", "tabIndex"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i15.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i15.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i15.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisListSkinPreviewComponent, selector: "praxis-list-skin-preview", inputs: ["config", "items", "theme"] }] });
1867
2193
  }
1868
2194
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisListConfigEditor, decorators: [{
1869
2195
  type: Component,
@@ -1882,6 +2208,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
1882
2208
  MatTooltipModule,
1883
2209
  MatDividerModule,
1884
2210
  MatChipsModule,
2211
+ MatMenuModule,
1885
2212
  PraxisIconDirective,
1886
2213
  PraxisListSkinPreviewComponent,
1887
2214
  ], template: `
@@ -1911,6 +2238,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
1911
2238
  </mat-select>
1912
2239
  </mat-form-field>
1913
2240
  </div>
2241
+ <mat-divider class="my-8"></mat-divider>
2242
+ <div class="subtitle">UI (Buscar / Ordenar / Rodapé)</div>
2243
+ <div class="g g-auto-220 gap-12 ai-end">
2244
+ <mat-slide-toggle [(ngModel)]="working.ui.showSearch" (ngModelChange)="onUiChanged()">Mostrar busca</mat-slide-toggle>
2245
+ <mat-slide-toggle [(ngModel)]="working.ui.showSort" (ngModelChange)="onUiChanged()">Mostrar ordenar</mat-slide-toggle>
2246
+ <mat-slide-toggle [(ngModel)]="working.ui.showRange" (ngModelChange)="onUiChanged()">Mostrar faixa X–Y de Total</mat-slide-toggle>
2247
+ </div>
2248
+ <div class="g g-auto-220 gap-12 ai-end mt-12" *ngIf="working.ui?.showSearch">
2249
+ <mat-form-field appearance="outline">
2250
+ <mat-label>Campo para buscar</mat-label>
2251
+ <mat-select [(ngModel)]="working.ui.searchField" (ngModelChange)="onUiChanged()">
2252
+ <mat-option *ngFor="let f of fields" [value]="f">{{ f }}</mat-option>
2253
+ </mat-select>
2254
+ </mat-form-field>
2255
+ <mat-form-field appearance="outline">
2256
+ <mat-label>Placeholder</mat-label>
2257
+ <input matInput [(ngModel)]="working.ui.searchPlaceholder" (ngModelChange)="onUiChanged()" placeholder="ex.: Buscar por título" />
2258
+ </mat-form-field>
2259
+ </div>
2260
+ <div class="mt-12" *ngIf="working.ui?.showSort">
2261
+ <div class="g g-1-auto ai-center gap-8">
2262
+ <div class="muted">Opções de ordenação (rótulo → campo+direção)</div>
2263
+ <button mat-flat-button color="primary" (click)="addUiSortRow()">Adicionar opção</button>
2264
+ </div>
2265
+ <div class="g g-auto-220 gap-12 ai-end mt-12" *ngFor="let r of uiSortRows; let i = index">
2266
+ <mat-form-field appearance="outline">
2267
+ <mat-label>Rótulo</mat-label>
2268
+ <input matInput [(ngModel)]="r.label" (ngModelChange)="onUiSortRowsChanged()" placeholder="ex.: Mais recentes" />
2269
+ </mat-form-field>
2270
+ <mat-form-field appearance="outline">
2271
+ <mat-label>Campo</mat-label>
2272
+ <mat-select [(ngModel)]="r.field" (ngModelChange)="onUiSortRowsChanged()">
2273
+ <mat-option *ngFor="let f of fields" [value]="f">{{ f }}</mat-option>
2274
+ </mat-select>
2275
+ </mat-form-field>
2276
+ <mat-form-field appearance="outline">
2277
+ <mat-label>Direção</mat-label>
2278
+ <mat-select [(ngModel)]="r.dir" (ngModelChange)="onUiSortRowsChanged()">
2279
+ <mat-option value="desc">Descendente</mat-option>
2280
+ <mat-option value="asc">Ascendente</mat-option>
2281
+ </mat-select>
2282
+ </mat-form-field>
2283
+ <div class="error" *ngIf="isUiSortRowDuplicate(i)">Opção duplicada (campo+direção)</div>
2284
+ <div class="flex-end"><button mat-button color="warn" (click)="removeUiSortRow(i)">Remover</button></div>
2285
+ </div>
2286
+ </div>
1914
2287
  </div>
1915
2288
  </mat-tab>
1916
2289
  <mat-tab label="Ações">
@@ -2076,6 +2449,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
2076
2449
  </mat-select>
2077
2450
  </mat-form-field>
2078
2451
  </ng-container>
2452
+ <mat-form-field appearance="outline" *ngIf="mappingPrimary.type==='text'">
2453
+ <mat-label>Pipe extra (ex.: map:K=V)</mat-label>
2454
+ <input matInput [(ngModel)]="mappingPrimary.extraPipe" (ngModelChange)="onMappingChanged()" placeholder="ex.: map:PLANEJADA=Planejada" />
2455
+ <button mat-icon-button matSuffix [matMenuTriggerFor]="pipeMenuPrimary" aria-label="Assistente de pipes" matTooltip="Assistente de pipes"><mat-icon>build</mat-icon></button>
2456
+ </mat-form-field>
2457
+ <mat-menu #pipeMenuPrimary="matMenu">
2458
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'map:KEY=VALUE,KEY2=VALUE2')"><mat-icon>tune</mat-icon><span>Map: KEY=VALUE…</span></button>
2459
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'number:pt-BR')"><mat-icon>push_pin</mat-icon><span>Número: pt-BR</span></button>
2460
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'number:pt-BR:compact')"><mat-icon>format_list_numbered</mat-icon><span>Número: compacto</span></button>
2461
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'bool:Sim:Não')"><mat-icon>check_circle</mat-icon><span>Bool: Sim/Não</span></button>
2462
+ <button mat-menu-item (click)="setPipe(mappingPrimary, 'date:pt-BR:short')"><mat-icon>schedule</mat-icon><span>Data: curto</span></button>
2463
+ </mat-menu>
2079
2464
  <mat-form-field appearance="outline">
2080
2465
  <mat-label>Classe CSS</mat-label>
2081
2466
  <input matInput [(ngModel)]="mappingPrimary.class" (ngModelChange)="onMappingChanged()" />
@@ -2131,6 +2516,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
2131
2516
  </mat-select>
2132
2517
  </mat-form-field>
2133
2518
  </ng-container>
2519
+ <mat-form-field appearance="outline" *ngIf="mappingSecondary.type==='text'">
2520
+ <mat-label>Pipe extra (ex.: number:pt-BR:compact)</mat-label>
2521
+ <input matInput [(ngModel)]="mappingSecondary.extraPipe" (ngModelChange)="onMappingChanged()" placeholder="ex.: number:pt-BR:compact" />
2522
+ <button mat-icon-button matSuffix [matMenuTriggerFor]="pipeMenuSecondary" aria-label="Assistente de pipes" matTooltip="Assistente de pipes"><mat-icon>build</mat-icon></button>
2523
+ </mat-form-field>
2524
+ <mat-menu #pipeMenuSecondary="matMenu">
2525
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'map:KEY=VALUE,KEY2=VALUE2')"><mat-icon>tune</mat-icon><span>Map: KEY=VALUE…</span></button>
2526
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'number:pt-BR')"><mat-icon>push_pin</mat-icon><span>Número: pt-BR</span></button>
2527
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'number:pt-BR:compact')"><mat-icon>format_list_numbered</mat-icon><span>Número: compacto</span></button>
2528
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'bool:Sim:Não')"><mat-icon>check_circle</mat-icon><span>Bool: Sim/Não</span></button>
2529
+ <button mat-menu-item (click)="setPipe(mappingSecondary, 'date:pt-BR:short')"><mat-icon>schedule</mat-icon><span>Data: curto</span></button>
2530
+ </mat-menu>
2134
2531
  <mat-form-field appearance="outline">
2135
2532
  <mat-label>Classe CSS</mat-label>
2136
2533
  <input matInput [(ngModel)]="mappingSecondary.class" (ngModelChange)="onMappingChanged()" />
@@ -2225,6 +2622,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
2225
2622
  </mat-select>
2226
2623
  </mat-form-field>
2227
2624
  </ng-container>
2625
+ <mat-form-field appearance="outline" *ngIf="mappingMeta.type==='text'">
2626
+ <mat-label>Pipe extra (ex.: map:K=V)</mat-label>
2627
+ <input matInput [(ngModel)]="mappingMeta.extraPipe" (ngModelChange)="onMappingChanged()" placeholder="ex.: map:ABERTO=Aberto,FECHADO=Fechado" />
2628
+ <button mat-icon-button matSuffix [matMenuTriggerFor]="pipeMenuMeta" aria-label="Assistente de pipes" matTooltip="Assistente de pipes"><mat-icon>build</mat-icon></button>
2629
+ </mat-form-field>
2630
+ <mat-menu #pipeMenuMeta="matMenu">
2631
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'map:KEY=VALUE,KEY2=VALUE2')"><mat-icon>tune</mat-icon><span>Map: KEY=VALUE…</span></button>
2632
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'number:pt-BR')"><mat-icon>push_pin</mat-icon><span>Número: pt-BR</span></button>
2633
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'number:pt-BR:compact')"><mat-icon>format_list_numbered</mat-icon><span>Número: compacto</span></button>
2634
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'bool:Sim:Não')"><mat-icon>check_circle</mat-icon><span>Bool: Sim/Não</span></button>
2635
+ <button mat-menu-item (click)="setPipe(mappingMeta, 'date:pt-BR:short')"><mat-icon>schedule</mat-icon><span>Data: curto</span></button>
2636
+ </mat-menu>
2228
2637
  </div>
2229
2638
  </mat-expansion-panel>
2230
2639
  <mat-expansion-panel>
@@ -2245,8 +2654,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
2245
2654
  <mat-option value="chip">Chip</mat-option>
2246
2655
  <mat-option value="currency">Moeda</mat-option>
2247
2656
  <mat-option value="date">Data</mat-option>
2657
+ <mat-option value="icon">Ícone</mat-option>
2248
2658
  </mat-select>
2249
2659
  </mat-form-field>
2660
+ <mat-form-field appearance="outline" *ngIf="mappingTrailing.type==='text' || mappingTrailing.type==='icon'">
2661
+ <mat-label>Pipe extra (ex.: map:KEY=icon,...)</mat-label>
2662
+ <input matInput [(ngModel)]="mappingTrailing.extraPipe" (ngModelChange)="onMappingChanged()" placeholder="ex.: map:PLANEJADA=event,EM_ANDAMENTO=play_circle" />
2663
+ <button mat-icon-button matSuffix [matMenuTriggerFor]="pipeMenuTrailing" aria-label="Assistente de pipes" matTooltip="Assistente de pipes"><mat-icon>build</mat-icon></button>
2664
+ </mat-form-field>
2665
+ <mat-menu #pipeMenuTrailing="matMenu">
2666
+ <button mat-menu-item (click)="setPipe(mappingTrailing, 'map:PLANEJADA=event,EM_ANDAMENTO=play_circle,PAUSADA=pause_circle,CONCLUIDA=check_circle,FALHOU=error')"><mat-icon>tune</mat-icon><span>Map: status→ícone</span></button>
2667
+ <button mat-menu-item (click)="setPipe(mappingTrailing, 'date:pt-BR:short')"><mat-icon>schedule</mat-icon><span>Data: curto</span></button>
2668
+ <button mat-menu-item (click)="setPipe(mappingTrailing, 'number:pt-BR')"><mat-icon>push_pin</mat-icon><span>Número: pt-BR</span></button>
2669
+ </mat-menu>
2250
2670
  <ng-container *ngIf="mappingTrailing.type==='chip'">
2251
2671
  <mat-form-field appearance="outline">
2252
2672
  <mat-label>Cor do chip</mat-label>
@@ -2298,6 +2718,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
2298
2718
  <mat-label>Style inline</mat-label>
2299
2719
  <input matInput [(ngModel)]="mappingTrailing.style" (ngModelChange)="onMappingChanged()" />
2300
2720
  </mat-form-field>
2721
+ <mat-form-field appearance="outline">
2722
+ <mat-label>Posição do status (cards)</mat-label>
2723
+ <mat-select [(ngModel)]="statusPosition" (ngModelChange)="onMappingChanged()">
2724
+ <mat-option [value]="undefined">Inline</mat-option>
2725
+ <mat-option value="inline">Inline</mat-option>
2726
+ <mat-option value="top-right">Canto superior direito</mat-option>
2727
+ </mat-select>
2728
+ </mat-form-field>
2729
+ <div class="subtitle">Cores por status (ícone)</div>
2730
+ <div class="chips-row" *ngIf="(mappingTrailing?.field || '').toLowerCase()==='status'">
2731
+ <span class="muted">Sugestões:</span>
2732
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'PLANEJADA', color:'primary'}]); onIconColorMapChanged()">PLANEJADA</button>
2733
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'EM_ANDAMENTO', color:'accent'}]); onIconColorMapChanged()">EM_ANDAMENTO</button>
2734
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'PAUSADA', color:'accent'}]); onIconColorMapChanged()">PAUSADA</button>
2735
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'CONCLUIDA', color:'primary'}]); onIconColorMapChanged()">CONCLUIDA</button>
2736
+ <button mat-stroked-button type="button" (click)="iconColorMapEntries = iconColorMapEntries.concat([{key:'FALHOU', color:'warn'}]); onIconColorMapChanged()">FALHOU</button>
2737
+ </div>
2738
+ <div class="g g-auto-220 gap-12 ai-end">
2739
+ <ng-container *ngFor="let ent of iconColorMapEntries; let i = index">
2740
+ <mat-form-field appearance="outline">
2741
+ <mat-label>Chave</mat-label>
2742
+ <input matInput [(ngModel)]="ent.key" (ngModelChange)="onIconColorMapChanged()" placeholder="ex.: EM_ANDAMENTO ou play_circle" />
2743
+ </mat-form-field>
2744
+ <mat-form-field appearance="outline">
2745
+ <mat-label>Cor</mat-label>
2746
+ <mat-select [(ngModel)]="ent.color" (ngModelChange)="onIconColorMapChanged()">
2747
+ <mat-option [value]="undefined">Default</mat-option>
2748
+ <mat-option value="primary">Primary</mat-option>
2749
+ <mat-option value="accent">Accent</mat-option>
2750
+ <mat-option value="warn">Warn</mat-option>
2751
+ </mat-select>
2752
+ </mat-form-field>
2753
+ <div class="error" *ngIf="isIconColorDuplicate(i)">Chave duplicada (consolidada)</div>
2754
+ <div class="flex-end"><button mat-button color="warn" (click)="removeIconColorEntry(i)">Remover</button></div>
2755
+ </ng-container>
2756
+ <div><button mat-flat-button color="primary" (click)="addIconColorEntry()">Adicionar cor</button></div>
2757
+ </div>
2758
+ <div class="muted">Entradas vazias são ignoradas e chaves repetidas são mescladas (última vence).</div>
2301
2759
  </div>
2302
2760
  </mat-expansion-panel>
2303
2761
  <mat-expansion-panel>
@@ -2558,7 +3016,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
2558
3016
  </div>
2559
3017
  </mat-tab>
2560
3018
  </mat-tab-group>
2561
- `, styles: [".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;opacity:.82}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}\n"] }]
3019
+ `, styles: [".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;opacity:.82}.chips-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.error{color:var(--mdc-theme-error,#b00020);font-size:.85rem}.muted{opacity:.7}\n"] }]
2562
3020
  }], ctorParameters: () => [{ type: undefined, decorators: [{
2563
3021
  type: Inject,
2564
3022
  args: [SETTINGS_PANEL_DATA]
@@ -2578,6 +3036,9 @@ class PraxisList {
2578
3036
  sections$;
2579
3037
  loading$;
2580
3038
  total$;
3039
+ page$;
3040
+ lastQuery = {};
3041
+ search$ = new Subject();
2581
3042
  get layoutLines() {
2582
3043
  return this.config?.layout?.lines ?? 2;
2583
3044
  }
@@ -2599,8 +3060,18 @@ class PraxisList {
2599
3060
  this.sections$ = this.data.groupedStream();
2600
3061
  this.loading$ = this.data.loading$;
2601
3062
  this.total$ = this.data.total$;
3063
+ this.page$ = this.data.pageState$;
2602
3064
  this.setupSelectionBinding();
2603
3065
  this.applySkins();
3066
+ this.lastQuery = this.config?.dataSource?.query || {};
3067
+ this.search$.pipe(debounceTime$1(300), distinctUntilChanged$1()).subscribe((term) => {
3068
+ const field = this.config?.ui?.searchField;
3069
+ if (!field)
3070
+ return;
3071
+ const q = { ...(this.lastQuery || {}), [field]: term };
3072
+ this.lastQuery = q;
3073
+ this.data.setQuery(q);
3074
+ });
2604
3075
  }
2605
3076
  ngOnChanges(changes) {
2606
3077
  if (changes['config']) {
@@ -2615,8 +3086,10 @@ class PraxisList {
2615
3086
  this.sections$ = this.data.groupedStream();
2616
3087
  this.loading$ = this.data.loading$;
2617
3088
  this.total$ = this.data.total$;
3089
+ this.page$ = this.data.pageState$;
2618
3090
  this.setupSelectionBinding();
2619
3091
  this.applySkins();
3092
+ this.lastQuery = this.config?.dataSource?.query || {};
2620
3093
  }
2621
3094
  }
2622
3095
  applyPersistence() {
@@ -2664,7 +3137,8 @@ class PraxisList {
2664
3137
  trailing = (item) => this.evalSlot('trailing', item);
2665
3138
  sectionHeader = (key) => evaluateTemplate(this.config?.templating?.sectionHeader, { key })?.value || key;
2666
3139
  featureLabel(item, expr) {
2667
- return this.evalString(expr || '', { item });
3140
+ const out = evaluateTemplate({ type: 'text', expr: expr || '' }, item);
3141
+ return out?.value ?? '';
2668
3142
  }
2669
3143
  featuresVisible() {
2670
3144
  const t = this.config?.templating;
@@ -2755,6 +3229,31 @@ class PraxisList {
2755
3229
  if (isFinite(size) && size > 0)
2756
3230
  this.data.setPageSize(size);
2757
3231
  }
3232
+ onSortChange(val) {
3233
+ if (!val)
3234
+ return;
3235
+ this.data.setSort([val]);
3236
+ }
3237
+ onSearchInput(val) {
3238
+ this.search$.next(val || '');
3239
+ }
3240
+ sortOptionValue(op) {
3241
+ return typeof op === 'string' ? op : ((op && op.value) ? String(op.value) : '');
3242
+ }
3243
+ sortOptionLabel(op) {
3244
+ if (op && typeof op === 'object' && op.label)
3245
+ return String(op.label);
3246
+ const v = this.sortOptionValue(op);
3247
+ return v;
3248
+ }
3249
+ rangeStart(ps) {
3250
+ return (ps?.pageNumber || 0) * (ps?.pageSize || 0) + 1;
3251
+ }
3252
+ rangeEnd(currLen, ps, total) {
3253
+ const startIndex = (ps?.pageNumber || 0) * (ps?.pageSize || 0);
3254
+ const to = startIndex + (currLen || 0);
3255
+ return Math.min(to, total || 0);
3256
+ }
2758
3257
  onEditorApplied(newCfg) {
2759
3258
  this.config = newCfg;
2760
3259
  // Optionally infer templates from backend schema if not defined
@@ -2922,17 +3421,8 @@ class PraxisList {
2922
3421
  return [spec.slice(0, idx).trim(), spec.slice(idx + 1).trim()];
2923
3422
  }
2924
3423
  evalString(expr, ctx) {
2925
- return expr.replace(/\$\{([^}]+)\}/g, (_, path) => {
2926
- try {
2927
- const value = path
2928
- .split('.')
2929
- .reduce((acc, k) => (acc == null ? undefined : acc[k]), ctx);
2930
- return value == null ? '' : String(value);
2931
- }
2932
- catch {
2933
- return '';
2934
- }
2935
- });
3424
+ const res = evaluateTemplate({ type: 'text', expr }, ctx?.item ?? ctx);
3425
+ return res?.value ?? '';
2936
3426
  }
2937
3427
  parseTwoParams(param) {
2938
3428
  if (!param)
@@ -2994,7 +3484,7 @@ class PraxisList {
2994
3484
  trackBySection = (_, s) => s?.key ?? _;
2995
3485
  trackByItem = (i, it) => this.config?.selection?.compareBy ? (it?.[this.config.selection.compareBy] ?? i) : (it?.id ?? i);
2996
3486
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisList, deps: [], target: i0.ɵɵFactoryTarget.Component });
2997
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: PraxisList, isStandalone: true, selector: "praxis-list", inputs: { config: "config", form: "form" }, outputs: { itemClick: "itemClick", actionClick: "actionClick", selectionChange: "selectionChange" }, providers: [GenericCrudService, ListDataService], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"praxis-list-root\"\n [ngClass]=\"skinClasses\"\n [attr.aria-label]=\"config.a11y?.ariaLabel || null\"\n [attr.aria-labelledby]=\"config.a11y?.ariaLabelledBy || null\"\n>\n @if (inlineCss) {\n <style [innerHTML]=\"inlineCss\"></style>\n }\n\n <!-- Skeleton while loading -->\n @if ((loading$ | async) && hasSkeleton()) {\n @if (isListVariant()) {\n <mat-list>\n @for (_ of skeletonItems(); track $index; let i = $index) {\n <mat-list-item>\n <div class=\"list-item-content\">\n <div class=\"skeleton skeleton-avatar\"></div>\n <div style=\"width:100%\">\n <div class=\"skeleton skeleton-line w-60\"></div>\n @if (layoutLines > 1) { <div class=\"skeleton skeleton-line w-40\"></div> }\n </div>\n <div class=\"skeleton skeleton-chip\"></div>\n </div>\n </mat-list-item>\n }\n </mat-list>\n } @else {\n <div class=\"cards-grid\">\n @for (_ of skeletonItems(); track $index; let i = $index) {\n <div class=\"item-card\">\n <div class=\"list-item-content\">\n <div class=\"skeleton skeleton-avatar\"></div>\n <div style=\"width:100%\">\n <div class=\"skeleton skeleton-line w-60\"></div>\n @if (layoutLines > 1) { <div class=\"skeleton skeleton-line w-40\"></div> }\n </div>\n <div class=\"skeleton skeleton-chip\"></div>\n </div>\n </div>\n }\n </div>\n }\n } @else {\n <ng-container *ngTemplateOutlet=\"notLoading\"></ng-container>\n }\n\n <ng-template #notLoading>\n <!-- Empty state -->\n @if (items$ | async; as all) {\n @if (all.length === 0) {\n <div class=\"section-header\">\n {{ (config.templating?.emptyState?.expr || 'Nenhum item dispon\u00EDvel') }}\n </div>\n }\n }\n\n <!-- List variant -->\n @if (isListVariant()) {\n <!-- Selection list -->\n @if (isSelectionEnabled()) {\n <mat-selection-list\n [multiple]=\"config.selection?.mode === 'multiple'\"\n [formControl]=\"boundControl\"\n (selectionChange)=\"onSelectionChange($event)\"\n >\n @for (section of sections$ | async; track trackBySection($index, section)) {\n @if (section.key) {\n <div class=\"section-header mat-subheader\">\n {{ sectionHeader(section.key) }}\n </div>\n }\n @for (item of section.items; track trackByItem($index, item); let i = $index) {\n <mat-list-option\n [value]=\"item\"\n (click)=\"onItemClick(item, i, section.key || undefined)\"\n >\n <div class=\"list-item-content\">\n @if (leading(item); as lead) {\n @switch (lead.type) {\n @case ('icon') {\n <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon>\n }\n @case ('image') {\n <div class=\"lead-image\" [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">\n <img [src]=\"lead.value\" [alt]=\"config.templating?.leading?.imageAlt || ''\" />\n @if (lead.badge?.value) {\n <mat-chip class=\"lead-badge\" [color]=\"lead.badge?.color || undefined\" [ngClass]=\"(((lead.badge?.variant || 'filled') === 'outlined') ? 'chip-outlined' : '')\">{{ lead.badge?.value }}</mat-chip>\n }\n </div>\n }\n }\n }\n @if (meta(item); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(item)?.class\" [style.cssText]=\"primary(item)?.style\">{{ primary(item)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(item)?.class\" [style.cssText]=\"secondary(item)?.style\">{{ secondary(item)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @if (config.templating?.metaPrefixIcon; as mpi) { <mat-icon [praxisIcon]=\"mpi\"></mat-icon> }\n {{ m.value }}\n </div>\n }\n @if (featuresVisible()) {\n @for (f of (config.templating?.features || []); track $index; let fi = $index) {\n <span class=\"feature\">\n @if (f.icon && featuresMode() !== 'labels-only') { <mat-icon [praxisIcon]=\"f.icon\"></mat-icon> }\n @if (featuresMode() !== 'icons-only') { <span [ngClass]=\"f.class\" [style.cssText]=\"f.style\">{{ featureLabel(item, f.expr) }}</span> }\n </span>\n }\n }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip [color]=\"m.color || undefined\" [ngClass]=\"((m.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon [praxisIcon]=\"starIcon(idx, m.value)\"></mat-icon> } }\n @default { <span>@if (config.templating?.metaPrefixIcon; as mpi2) { <mat-icon>{{ mpi2 }}</mat-icon> }{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(item); as tr) {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n @for (action of visibleActions(item); let aidx = $index; track action?.id ?? $index) {\n @if ((action.kind || 'icon') === 'icon') {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon [praxisIcon]=\"action.icon\"></mat-icon>\n </button>\n } @else {\n @if (action.buttonVariant === 'stroked') { <button mat-stroked-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (action.buttonVariant === 'raised') { <button mat-raised-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (!action.buttonVariant || action.buttonVariant === 'flat') { <button mat-flat-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n }\n }\n </div>\n </mat-list-option>\n }\n }\n </mat-selection-list>\n } @else {\n <ng-container *ngTemplateOutlet=\"readList\"></ng-container>\n }\n\n <!-- Read-only list -->\n <ng-template #readList>\n <mat-list>\n @for (section of sections$ | async; track trackBySection($index, section); let sidx = $index) {\n @if (section.key) {\n <div class=\"section-header mat-subheader\">\n {{ sectionHeader(section.key) }}\n </div>\n }\n @for (item of section.items; track trackByItem($index, item); let i = $index) {\n <mat-list-item (click)=\"onItemClick(item, i, section.key || undefined)\">\n <div class=\"list-item-content\">\n @if (leading(item); as lead) { <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon> }\n @if (meta(item); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(item)?.class\" [style.cssText]=\"primary(item)?.style\">{{ primary(item)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(item)?.class\" [style.cssText]=\"secondary(item)?.style\">{{ secondary(item)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) { <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">{{ m.value }}</div> }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip>{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon>{{ starIcon(idx, m.value) }}</mat-icon> } }\n @default { <span>{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(item); as tr) {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip>{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n @for (action of visibleActions(item); let aidx = $index; track action?.id ?? $index) {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n }\n </div>\n </mat-list-item>\n }\n @if (config.layout?.dividers === 'between') { <mat-divider></mat-divider> }\n }\n </mat-list>\n </ng-template>\n } @else {\n <ng-container *ngTemplateOutlet=\"cardsVariant\"></ng-container>\n }\n\n <!-- Cards variant -->\n <ng-template #cardsVariant>\n <div class=\"cards-grid\">\n @for (it of (items$ | async) ?? []; track $index; let i = $index) {\n <div class=\"item-card\" (click)=\"onItemClick(it, i)\">\n <div class=\"list-item-content\">\n @if (leading(it); as lead) {\n @switch (lead.type) {\n @case ('icon') { <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon> }\n @case ('image') {\n <div class=\"lead-image\" [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">\n <img [src]=\"lead.value\" [alt]=\"config.templating?.leading?.imageAlt || ''\" />\n @if (lead.badge?.value) { <mat-chip class=\"lead-badge\" [color]=\"lead.badge?.color || undefined\" [ngClass]=\"(((lead.badge?.variant || 'filled') === 'outlined') ? 'chip-outlined' : '')\">{{ lead.badge?.value }}</mat-chip> }\n </div>\n }\n }\n }\n @if (meta(it); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(it)?.class\" [style.cssText]=\"primary(it)?.style\">{{ primary(it)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(it)?.class\" [style.cssText]=\"secondary(it)?.style\">{{ secondary(it)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) { <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">{{ m.value }}</div> }\n @if (featuresVisible()) {\n @for (f of (config.templating?.features || []); track $index; let fi = $index) {\n <span class=\"feature\">\n @if (f.icon && featuresMode() !== 'labels-only') { <mat-icon>{{ f.icon }}</mat-icon> }\n @if (featuresMode() !== 'icons-only') { <span [ngClass]=\"f.class\" [style.cssText]=\"f.style\">{{ featureLabel(it, f.expr) }}</span> }\n </span>\n }\n }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip [color]=\"m.color || undefined\" [ngClass]=\"((m.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon>{{ starIcon(idx, m.value) }}</mat-icon> } }\n @default { <span>{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(it); as tr) {\n @if ((config.templating?.statusPosition || 'inline') === 'top-right' && (tr?.type === 'chip' || tr?.type === 'icon')) {\n <div class=\"status-overlay\">\n @if (tr?.type === 'chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @if (tr?.type === 'icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n </div>\n } @else {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n }\n </div>\n <div class=\"card-actions\" (click)=\"$event.stopPropagation()\">\n @for (action of visibleActions(it); let aidx = $index; track action?.id ?? $index) {\n @if ((action.kind || 'icon') === 'icon') {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n } @else {\n @if (action.buttonVariant === 'stroked') { <button mat-stroked-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (action.buttonVariant === 'raised') { <button mat-raised-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (!action.buttonVariant || action.buttonVariant === 'flat') { <button mat-flat-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n }\n }\n </div>\n </div>\n }\n </div>\n </ng-template>\n \n <!-- Simple pagination controls for remote data -->\n @if (config.dataSource?.resourcePath) {\n <div class=\"paginator\">\n <button mat-stroked-button color=\"primary\" (click)=\"prevPage()\">Anterior</button>\n <button mat-stroked-button color=\"primary\" (click)=\"nextPage()\">Pr\u00F3ximo</button>\n <mat-form-field style=\"width: 120px; margin-left: 8px;\" appearance=\"outline\">\n <mat-label>Tam. p\u00E1gina</mat-label>\n <mat-select (selectionChange)=\"setPageSize($event.value)\" [value]=\"config.layout?.pageSize || 10\">\n <mat-option [value]=\"6\">6</mat-option>\n <mat-option [value]=\"8\">8</mat-option>\n <mat-option [value]=\"12\">12</mat-option>\n <mat-option [value]=\"24\">24</mat-option>\n </mat-select>\n </mat-form-field>\n @if (total$ | async; as total) { <span class=\"muted\">Total: {{ total }}</span> }\n </div>\n }\n </ng-template>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.praxis-list-root{--p-list-radius: 1rem;--p-list-shadow: 0 6px 20px rgba(0, 0, 0, .08);--p-list-border: 1px solid rgba(0, 0, 0, .08);--p-list-blur: 8px;--p-list-grad-from: #8e72ff;--p-list-grad-to: #ffaa70;--p-list-grad-angle: 135deg}.skin-elevated .item-card{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:var(--p-list-radius);box-shadow:var(--p-list-shadow);border:var(--p-list-border)}.skin-pill-soft .item-card{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:calc(var(--p-list-radius) * 1.3);box-shadow:0 4px 16px #00000014;border:var(--p-list-border)}.skin-pill-soft .mat-mdc-list-item .list-item-content{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:calc(var(--p-list-radius) * 1.3);box-shadow:0 4px 16px #00000014;border:var(--p-list-border);color:var(--p-list-foreground, #1f2937)}.skin-gradient-tile .item-card,.skin-gradient-tile .mat-mdc-list-item .list-item-content{background:linear-gradient(var(--p-list-grad-angle),var(--p-list-grad-from),var(--p-list-grad-to));border-radius:var(--p-list-radius);color:#fff}.skin-glass .item-card{background:#ffffff2e;border-radius:var(--p-list-radius);border:1px solid rgba(255,255,255,.3);backdrop-filter:blur(var(--p-list-blur));-webkit-backdrop-filter:blur(var(--p-list-blur))}.skin-glass .mat-mdc-list-item .list-item-content{background:#ffffff2e;border-radius:var(--p-list-radius);border:1px solid rgba(255,255,255,.3);backdrop-filter:blur(var(--p-list-blur));-webkit-backdrop-filter:blur(var(--p-list-blur));color:var(--p-list-foreground, #111)}.density-compact .mat-mdc-list-item,.density-compact .item-card{--_item-padding: 4px 8px}.density-comfortable .mat-mdc-list-item,.density-comfortable .item-card{--_item-padding: 8px 12px}.list-item-content{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;padding:var(--_item-padding, 12px 16px)}.lead-image{position:relative;width:96px;height:72px;border-radius:12px;overflow:hidden}.lead-image img{width:100%;height:100%;object-fit:cover;display:block}.lead-image .lead-badge{position:absolute;left:8px;bottom:8px}.model-media .lead-image{width:136px;height:96px;border-radius:16px}.model-media .list-item-content{gap:16px}.model-hotel .lead-image{width:160px;height:110px;border-radius:18px}.primary{font-weight:600;color:var(--sicoob-tertiary-default, #001E24)}.secondary{opacity:.8}.tertiary{opacity:.68}.features{display:flex;flex-wrap:wrap;gap:12px;margin-top:6px;color:inherit}.feature{display:inline-flex;align-items:center;gap:6px}.meta{opacity:.9}.trailing{opacity:.9;margin-left:8px}.section-header{font-size:.85rem;opacity:.7;padding:8px 12px}.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.item-card{padding:12px;display:flex;flex-direction:column;min-height:160px;position:relative;cursor:pointer}.item-card:hover{box-shadow:0 10px 24px var(--sicoob-shadow-medium, rgba(0, 0, 0, .12));transform:translateY(-1px);transition:box-shadow .15s ease,transform .15s ease}.card-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px;padding:6px 8px 4px;border-top:1px solid var(--sicoob-stroke-medium, rgba(0, 0, 0, .08));margin-top:auto}.status-overlay{position:absolute;top:10px;right:10px;pointer-events:none}.status-overlay .mat-mdc-chip{pointer-events:auto}.leading-icon.mat-icon{width:36px;height:36px;line-height:36px;font-size:20px;display:inline-flex;align-items:center;justify-content:center;border-radius:999px;background:color-mix(in srgb,var(--sicoob-info-lightest, #f0f9ff),transparent 30%);color:var(--sicoob-info-default, #0090e0)}.mat-mdc-chip{--mdc-chip-container-height: 22px;font-size:12px}.meta .mat-icon{font-size:18px;height:18px;width:18px}.primary,.secondary{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.primary,.secondary{-webkit-line-clamp:1}.features{overflow:hidden}@keyframes shimmer{0%{background-position:-468px 0}to{background-position:468px 0}}.skeleton{animation-duration:1.2s;animation-fill-mode:forwards;animation-iteration-count:infinite;animation-name:shimmer;animation-timing-function:linear;background:#f6f7f8;background:linear-gradient(to right,#eee 8%,#ddd 18%,#eee 33%);background-size:800px 104px;position:relative}.skeleton-avatar{width:32px;height:32px;border-radius:50%}.skeleton-line{height:10px;margin:6px 0;border-radius:6px}.skeleton-chip{width:48px;height:18px;border-radius:999px}.chip-outlined.mat-mdc-chip{background:transparent!important;border:1px solid currentColor}.w-60{width:60%}.w-40{width:40%}.paginator{display:flex;align-items:center;gap:8px;padding:8px 4px}.muted{opacity:.7}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type: i3.MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: i3.MatSelectionList, selector: "mat-selection-list", inputs: ["color", "compareWith", "multiple", "hideSingleSelectionIndicator", "disabled"], outputs: ["selectionChange"], exportAs: ["matSelectionList"] }, { kind: "component", type: i3.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "component", type: i3.MatListOption, selector: "mat-list-option", inputs: ["togglePosition", "checkboxPosition", "color", "value", "selected"], outputs: ["selectedChange"], exportAs: ["matListOption"] }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i14.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.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: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3487
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: PraxisList, isStandalone: true, selector: "praxis-list", inputs: { config: "config", form: "form" }, outputs: { itemClick: "itemClick", actionClick: "actionClick", selectionChange: "selectionChange" }, providers: [GenericCrudService, ListDataService], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"praxis-list-root\"\n [ngClass]=\"skinClasses\"\n [attr.aria-label]=\"config.a11y?.ariaLabel || null\"\n [attr.aria-labelledby]=\"config.a11y?.ariaLabelledBy || null\"\n>\n @if (inlineCss) {\n <style [innerHTML]=\"inlineCss\"></style>\n }\n\n <!-- Skeleton while loading -->\n @if ((loading$ | async) && hasSkeleton()) {\n @if (isListVariant()) {\n <mat-list>\n @for (_ of skeletonItems(); track $index; let i = $index) {\n <mat-list-item>\n <div class=\"list-item-content\">\n <div class=\"skeleton skeleton-avatar\"></div>\n <div style=\"width:100%\">\n <div class=\"skeleton skeleton-line w-60\"></div>\n @if (layoutLines > 1) { <div class=\"skeleton skeleton-line w-40\"></div> }\n </div>\n <div class=\"skeleton skeleton-chip\"></div>\n </div>\n </mat-list-item>\n }\n </mat-list>\n } @else {\n <div class=\"cards-grid\">\n @for (_ of skeletonItems(); track $index; let i = $index) {\n <div class=\"item-card\">\n <div class=\"list-item-content\">\n <div class=\"skeleton skeleton-avatar\"></div>\n <div style=\"width:100%\">\n <div class=\"skeleton skeleton-line w-60\"></div>\n @if (layoutLines > 1) { <div class=\"skeleton skeleton-line w-40\"></div> }\n </div>\n <div class=\"skeleton skeleton-chip\"></div>\n </div>\n </div>\n }\n </div>\n }\n } @else {\n <ng-container *ngTemplateOutlet=\"notLoading\"></ng-container>\n }\n\n <ng-template #notLoading>\n <!-- Empty state -->\n @if (items$ | async; as all) {\n @if (all.length === 0) {\n <div class=\"section-header\">\n {{ (config.templating?.emptyState?.expr || 'Nenhum item dispon\u00EDvel') }}\n </div>\n }\n }\n\n <!-- List variant -->\n @if (isListVariant()) {\n <!-- Selection list -->\n @if (isSelectionEnabled()) {\n <mat-selection-list\n [multiple]=\"config.selection?.mode === 'multiple'\"\n [formControl]=\"boundControl\"\n (selectionChange)=\"onSelectionChange($event)\"\n >\n @for (section of sections$ | async; track trackBySection($index, section)) {\n @if (section.key) {\n <div class=\"section-header mat-subheader\">\n {{ sectionHeader(section.key) }}\n </div>\n }\n @for (item of section.items; track trackByItem($index, item); let i = $index) {\n <mat-list-option\n [value]=\"item\"\n (click)=\"onItemClick(item, i, section.key || undefined)\"\n >\n <div class=\"list-item-content\">\n @if (leading(item); as lead) {\n @switch (lead.type) {\n @case ('icon') {\n <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon>\n }\n @case ('image') {\n <div class=\"lead-image\" [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">\n <img [src]=\"lead.value\" [alt]=\"config.templating?.leading?.imageAlt || ''\" />\n @if (lead.badge?.value) {\n <mat-chip class=\"lead-badge\" [color]=\"lead.badge?.color || undefined\" [ngClass]=\"(((lead.badge?.variant || 'filled') === 'outlined') ? 'chip-outlined' : '')\">{{ lead.badge?.value }}</mat-chip>\n }\n </div>\n }\n }\n }\n @if (meta(item); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(item)?.class\" [style.cssText]=\"primary(item)?.style\">{{ primary(item)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(item)?.class\" [style.cssText]=\"secondary(item)?.style\">{{ secondary(item)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @if (config.templating?.metaPrefixIcon; as mpi) { <mat-icon [praxisIcon]=\"mpi\"></mat-icon> }\n {{ m.value }}\n </div>\n }\n @if (featuresVisible()) {\n @for (f of (config.templating?.features || []); track $index; let fi = $index) {\n <span class=\"feature\">\n @if (f.icon && featuresMode() !== 'labels-only') { <mat-icon [praxisIcon]=\"f.icon\"></mat-icon> }\n @if (featuresMode() !== 'icons-only') { <span [ngClass]=\"f.class\" [style.cssText]=\"f.style\">{{ featureLabel(item, f.expr) }}</span> }\n </span>\n }\n }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip [color]=\"m.color || undefined\" [ngClass]=\"((m.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon [praxisIcon]=\"starIcon(idx, m.value)\"></mat-icon> } }\n @default { <span>@if (config.templating?.metaPrefixIcon; as mpi2) { <mat-icon>{{ mpi2 }}</mat-icon> }{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(item); as tr) {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n @for (action of visibleActions(item); let aidx = $index; track action?.id ?? $index) {\n @if ((action.kind || 'icon') === 'icon') {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon [praxisIcon]=\"action.icon\"></mat-icon>\n </button>\n } @else {\n @if (action.buttonVariant === 'stroked') { <button mat-stroked-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (action.buttonVariant === 'raised') { <button mat-raised-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (!action.buttonVariant || action.buttonVariant === 'flat') { <button mat-flat-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n }\n }\n </div>\n </mat-list-option>\n }\n }\n </mat-selection-list>\n } @else {\n <ng-container *ngTemplateOutlet=\"readList\"></ng-container>\n }\n\n <!-- Read-only list -->\n <ng-template #readList>\n <mat-list>\n @for (section of sections$ | async; track trackBySection($index, section); let sidx = $index) {\n @if (section.key) {\n <div class=\"section-header mat-subheader\">\n {{ sectionHeader(section.key) }}\n </div>\n }\n @for (item of section.items; track trackByItem($index, item); let i = $index) {\n <mat-list-item (click)=\"onItemClick(item, i, section.key || undefined)\">\n <div class=\"list-item-content\">\n @if (leading(item); as lead) { <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon> }\n @if (meta(item); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(item)?.class\" [style.cssText]=\"primary(item)?.style\">{{ primary(item)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(item)?.class\" [style.cssText]=\"secondary(item)?.style\">{{ secondary(item)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) { <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">{{ m.value }}</div> }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip>{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon>{{ starIcon(idx, m.value) }}</mat-icon> } }\n @default { <span>{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(item); as tr) {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip>{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n @for (action of visibleActions(item); let aidx = $index; track action?.id ?? $index) {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n }\n </div>\n </mat-list-item>\n }\n @if (config.layout?.dividers === 'between') { <mat-divider></mat-divider> }\n }\n </mat-list>\n </ng-template>\n } @else {\n <ng-container *ngTemplateOutlet=\"cardsVariant\"></ng-container>\n }\n\n <!-- Cards variant -->\n <ng-template #cardsVariant>\n <div class=\"cards-grid\">\n @for (it of (items$ | async) ?? []; track $index; let i = $index) {\n <div class=\"item-card\" (click)=\"onItemClick(it, i)\">\n <div class=\"list-item-content\">\n @if (leading(it); as lead) {\n @switch (lead.type) {\n @case ('icon') { <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon> }\n @case ('image') {\n <div class=\"lead-image\" [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">\n <img [src]=\"lead.value\" [alt]=\"config.templating?.leading?.imageAlt || ''\" />\n @if (lead.badge?.value) { <mat-chip class=\"lead-badge\" [color]=\"lead.badge?.color || undefined\" [ngClass]=\"(((lead.badge?.variant || 'filled') === 'outlined') ? 'chip-outlined' : '')\">{{ lead.badge?.value }}</mat-chip> }\n </div>\n }\n }\n }\n @if (meta(it); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(it)?.class\" [style.cssText]=\"primary(it)?.style\">{{ primary(it)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(it)?.class\" [style.cssText]=\"secondary(it)?.style\">{{ secondary(it)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) { <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">{{ m.value }}</div> }\n @if (featuresVisible()) {\n @for (f of (config.templating?.features || []); track $index; let fi = $index) {\n <span class=\"feature\">\n @if (f.icon && featuresMode() !== 'labels-only') { <mat-icon>{{ f.icon }}</mat-icon> }\n @if (featuresMode() !== 'icons-only') { <span [ngClass]=\"f.class\" [style.cssText]=\"f.style\">{{ featureLabel(it, f.expr) }}</span> }\n </span>\n }\n }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip [color]=\"m.color || undefined\" [ngClass]=\"((m.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon>{{ starIcon(idx, m.value) }}</mat-icon> } }\n @default { <span>{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(it); as tr) {\n @if ((config.templating?.statusPosition || 'inline') === 'top-right' && (tr?.type === 'chip' || tr?.type === 'icon')) {\n <div class=\"status-overlay\">\n @if (tr?.type === 'chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @if (tr?.type === 'icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n </div>\n } @else {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n }\n </div>\n <div class=\"card-actions\" (click)=\"$event.stopPropagation()\">\n @for (action of visibleActions(it); let aidx = $index; track action?.id ?? $index) {\n @if ((action.kind || 'icon') === 'icon') {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n } @else {\n @if (action.buttonVariant === 'stroked') { <button mat-stroked-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (action.buttonVariant === 'raised') { <button mat-raised-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (!action.buttonVariant || action.buttonVariant === 'flat') { <button mat-flat-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n }\n }\n </div>\n </div>\n }\n </div>\n </ng-template>\n \n <!-- Simple pagination controls for remote data -->\n @if (config.dataSource?.resourcePath) {\n <div class=\"paginator\" style=\"flex-wrap: wrap; gap: 8px;\">\n <div style=\"display:flex; align-items:center; gap:8px; flex-wrap: wrap;\">\n <button mat-stroked-button color=\"primary\" (click)=\"prevPage()\">Anterior</button>\n <button mat-stroked-button color=\"primary\" (click)=\"nextPage()\">Pr\u00F3ximo</button>\n <mat-form-field style=\"width: 120px;\" appearance=\"outline\">\n <mat-label>Tam. p\u00E1gina</mat-label>\n <mat-select (selectionChange)=\"setPageSize($event.value)\" [value]=\"config.layout?.pageSize || 10\">\n <mat-option [value]=\"6\">6</mat-option>\n <mat-option [value]=\"8\">8</mat-option>\n <mat-option [value]=\"12\">12</mat-option>\n <mat-option [value]=\"24\">24</mat-option>\n </mat-select>\n </mat-form-field>\n @if (config.ui?.showSort) {\n <mat-form-field style=\"width: 180px;\" appearance=\"outline\">\n <mat-label>Ordenar</mat-label>\n <mat-select (selectionChange)=\"onSortChange($event.value)\">\n @for (op of (config.ui?.sortOptions || []); track $index) {\n <mat-option [value]=\"sortOptionValue(op)\">{{ sortOptionLabel(op) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n @if (config.ui?.showSearch && config.ui?.searchField) {\n <mat-form-field style=\"min-width: 220px;\" appearance=\"outline\">\n <mat-label>{{ config.ui?.searchPlaceholder || 'Buscar' }}</mat-label>\n <input matInput type=\"search\" (input)=\"onSearchInput($any($event.target).value)\" />\n </mat-form-field>\n }\n </div>\n <div style=\"display:flex; align-items:center; gap:8px; margin-left:auto;\">\n @if ((config.ui?.showRange ?? true)) {\n @if (page$ | async; as ps) {\n @if (total$ | async; as total) {\n @if (items$ | async; as curr) {\n <span class=\"muted\">{{ rangeStart(ps) }}\u2013{{ rangeEnd(curr?.length || 0, ps, total || 0) }} de {{ total || 0 }}</span>\n }\n }\n }\n } @else {\n @if (total$ | async; as total2) { <span class=\"muted\">Total: {{ total2 }}</span> }\n }\n </div>\n </div>\n }\n </ng-template>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.praxis-list-root{--p-list-radius: 1rem;--p-list-shadow: 0 6px 20px rgba(0, 0, 0, .08);--p-list-border: 1px solid rgba(0, 0, 0, .08);--p-list-blur: 8px;--p-list-grad-from: #8e72ff;--p-list-grad-to: #ffaa70;--p-list-grad-angle: 135deg}.skin-elevated .item-card{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:var(--p-list-radius);box-shadow:var(--p-list-shadow);border:var(--p-list-border)}.skin-pill-soft .item-card{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:calc(var(--p-list-radius) * 1.3);box-shadow:0 4px 16px #00000014;border:var(--p-list-border)}.skin-pill-soft .mat-mdc-list-item .list-item-content{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:calc(var(--p-list-radius) * 1.3);box-shadow:0 4px 16px #00000014;border:var(--p-list-border);color:var(--p-list-foreground, #1f2937)}.skin-gradient-tile .item-card,.skin-gradient-tile .mat-mdc-list-item .list-item-content{background:linear-gradient(var(--p-list-grad-angle),var(--p-list-grad-from),var(--p-list-grad-to));border-radius:var(--p-list-radius);color:#fff}.skin-glass .item-card{background:#ffffff2e;border-radius:var(--p-list-radius);border:1px solid rgba(255,255,255,.3);backdrop-filter:blur(var(--p-list-blur));-webkit-backdrop-filter:blur(var(--p-list-blur))}.skin-glass .mat-mdc-list-item .list-item-content{background:#ffffff2e;border-radius:var(--p-list-radius);border:1px solid rgba(255,255,255,.3);backdrop-filter:blur(var(--p-list-blur));-webkit-backdrop-filter:blur(var(--p-list-blur));color:var(--p-list-foreground, #111)}.density-compact .mat-mdc-list-item,.density-compact .item-card{--_item-padding: 4px 8px}.density-comfortable .mat-mdc-list-item,.density-comfortable .item-card{--_item-padding: 8px 12px}.list-item-content{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;padding:var(--_item-padding, 12px 16px)}.lead-image{position:relative;width:96px;height:72px;border-radius:12px;overflow:hidden}.lead-image img{width:100%;height:100%;object-fit:cover;display:block}.lead-image .lead-badge{position:absolute;left:8px;bottom:8px}.model-media .lead-image{width:136px;height:96px;border-radius:16px}.model-media .list-item-content{gap:16px}.model-hotel .lead-image{width:160px;height:110px;border-radius:18px}.primary{font-weight:600;color:var(--sicoob-tertiary-default, #001E24)}.secondary{opacity:.8}.tertiary{opacity:.68}.features{display:flex;flex-wrap:wrap;gap:12px;margin-top:6px;color:inherit}.feature{display:inline-flex;align-items:center;gap:6px}.meta{opacity:.9}.trailing{opacity:.9;margin-left:8px}.section-header{font-size:.85rem;opacity:.7;padding:8px 12px}.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.item-card{padding:12px;display:flex;flex-direction:column;min-height:160px;position:relative;cursor:pointer}.item-card:hover{box-shadow:0 10px 24px var(--sicoob-shadow-medium, rgba(0, 0, 0, .12));transform:translateY(-1px);transition:box-shadow .15s ease,transform .15s ease}.card-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px;padding:6px 8px 4px;border-top:1px solid var(--sicoob-stroke-medium, rgba(0, 0, 0, .08));margin-top:auto}.status-overlay{position:absolute;top:10px;right:10px;pointer-events:none}.status-overlay .mat-mdc-chip{pointer-events:auto}.leading-icon.mat-icon{width:36px;height:36px;line-height:36px;font-size:20px;display:inline-flex;align-items:center;justify-content:center;border-radius:999px;background:color-mix(in srgb,var(--sicoob-info-lightest, #f0f9ff),transparent 30%);color:var(--sicoob-info-default, #0090e0)}.mat-mdc-chip{--mdc-chip-container-height: 22px;font-size:12px}.meta .mat-icon{font-size:18px;height:18px;width:18px}.primary,.secondary{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.primary,.secondary{-webkit-line-clamp:1}.features{overflow:hidden}@keyframes shimmer{0%{background-position:-468px 0}to{background-position:468px 0}}.skeleton{animation-duration:1.2s;animation-fill-mode:forwards;animation-iteration-count:infinite;animation-name:shimmer;animation-timing-function:linear;background:#f6f7f8;background:linear-gradient(to right,#eee 8%,#ddd 18%,#eee 33%);background-size:800px 104px;position:relative}.skeleton-avatar{width:32px;height:32px;border-radius:50%}.skeleton-line{height:10px;margin:6px 0;border-radius:6px}.skeleton-chip{width:48px;height:18px;border-radius:999px}.chip-outlined.mat-mdc-chip{background:transparent!important;border:1px solid currentColor}.w-60{width:60%}.w-40{width:40%}.paginator{display:flex;align-items:center;gap:8px;padding:8px 4px}.muted{opacity:.7}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type: i3.MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: i3.MatSelectionList, selector: "mat-selection-list", inputs: ["color", "compareWith", "multiple", "hideSingleSelectionIndicator", "disabled"], outputs: ["selectionChange"], exportAs: ["matSelectionList"] }, { kind: "component", type: i3.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "component", type: i3.MatListOption, selector: "mat-list-option", inputs: ["togglePosition", "checkboxPosition", "color", "value", "selected"], outputs: ["selectedChange"], exportAs: ["matListOption"] }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i14.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["role", "id", "aria-label", "aria-description", "value", "color", "removable", "highlighted", "disableRipple", "disabled"], outputs: ["removed", "destroyed"], exportAs: ["matChip"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i7.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i7.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.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: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.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: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2998
3488
  }
2999
3489
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisList, decorators: [{
3000
3490
  type: Component,
@@ -3009,7 +3499,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
3009
3499
  MatButtonModule,
3010
3500
  MatFormFieldModule,
3011
3501
  MatSelectModule,
3012
- ], changeDetection: ChangeDetectionStrategy.OnPush, providers: [GenericCrudService, ListDataService], template: "<div\n class=\"praxis-list-root\"\n [ngClass]=\"skinClasses\"\n [attr.aria-label]=\"config.a11y?.ariaLabel || null\"\n [attr.aria-labelledby]=\"config.a11y?.ariaLabelledBy || null\"\n>\n @if (inlineCss) {\n <style [innerHTML]=\"inlineCss\"></style>\n }\n\n <!-- Skeleton while loading -->\n @if ((loading$ | async) && hasSkeleton()) {\n @if (isListVariant()) {\n <mat-list>\n @for (_ of skeletonItems(); track $index; let i = $index) {\n <mat-list-item>\n <div class=\"list-item-content\">\n <div class=\"skeleton skeleton-avatar\"></div>\n <div style=\"width:100%\">\n <div class=\"skeleton skeleton-line w-60\"></div>\n @if (layoutLines > 1) { <div class=\"skeleton skeleton-line w-40\"></div> }\n </div>\n <div class=\"skeleton skeleton-chip\"></div>\n </div>\n </mat-list-item>\n }\n </mat-list>\n } @else {\n <div class=\"cards-grid\">\n @for (_ of skeletonItems(); track $index; let i = $index) {\n <div class=\"item-card\">\n <div class=\"list-item-content\">\n <div class=\"skeleton skeleton-avatar\"></div>\n <div style=\"width:100%\">\n <div class=\"skeleton skeleton-line w-60\"></div>\n @if (layoutLines > 1) { <div class=\"skeleton skeleton-line w-40\"></div> }\n </div>\n <div class=\"skeleton skeleton-chip\"></div>\n </div>\n </div>\n }\n </div>\n }\n } @else {\n <ng-container *ngTemplateOutlet=\"notLoading\"></ng-container>\n }\n\n <ng-template #notLoading>\n <!-- Empty state -->\n @if (items$ | async; as all) {\n @if (all.length === 0) {\n <div class=\"section-header\">\n {{ (config.templating?.emptyState?.expr || 'Nenhum item dispon\u00EDvel') }}\n </div>\n }\n }\n\n <!-- List variant -->\n @if (isListVariant()) {\n <!-- Selection list -->\n @if (isSelectionEnabled()) {\n <mat-selection-list\n [multiple]=\"config.selection?.mode === 'multiple'\"\n [formControl]=\"boundControl\"\n (selectionChange)=\"onSelectionChange($event)\"\n >\n @for (section of sections$ | async; track trackBySection($index, section)) {\n @if (section.key) {\n <div class=\"section-header mat-subheader\">\n {{ sectionHeader(section.key) }}\n </div>\n }\n @for (item of section.items; track trackByItem($index, item); let i = $index) {\n <mat-list-option\n [value]=\"item\"\n (click)=\"onItemClick(item, i, section.key || undefined)\"\n >\n <div class=\"list-item-content\">\n @if (leading(item); as lead) {\n @switch (lead.type) {\n @case ('icon') {\n <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon>\n }\n @case ('image') {\n <div class=\"lead-image\" [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">\n <img [src]=\"lead.value\" [alt]=\"config.templating?.leading?.imageAlt || ''\" />\n @if (lead.badge?.value) {\n <mat-chip class=\"lead-badge\" [color]=\"lead.badge?.color || undefined\" [ngClass]=\"(((lead.badge?.variant || 'filled') === 'outlined') ? 'chip-outlined' : '')\">{{ lead.badge?.value }}</mat-chip>\n }\n </div>\n }\n }\n }\n @if (meta(item); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(item)?.class\" [style.cssText]=\"primary(item)?.style\">{{ primary(item)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(item)?.class\" [style.cssText]=\"secondary(item)?.style\">{{ secondary(item)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @if (config.templating?.metaPrefixIcon; as mpi) { <mat-icon [praxisIcon]=\"mpi\"></mat-icon> }\n {{ m.value }}\n </div>\n }\n @if (featuresVisible()) {\n @for (f of (config.templating?.features || []); track $index; let fi = $index) {\n <span class=\"feature\">\n @if (f.icon && featuresMode() !== 'labels-only') { <mat-icon [praxisIcon]=\"f.icon\"></mat-icon> }\n @if (featuresMode() !== 'icons-only') { <span [ngClass]=\"f.class\" [style.cssText]=\"f.style\">{{ featureLabel(item, f.expr) }}</span> }\n </span>\n }\n }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip [color]=\"m.color || undefined\" [ngClass]=\"((m.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon [praxisIcon]=\"starIcon(idx, m.value)\"></mat-icon> } }\n @default { <span>@if (config.templating?.metaPrefixIcon; as mpi2) { <mat-icon>{{ mpi2 }}</mat-icon> }{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(item); as tr) {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n @for (action of visibleActions(item); let aidx = $index; track action?.id ?? $index) {\n @if ((action.kind || 'icon') === 'icon') {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon [praxisIcon]=\"action.icon\"></mat-icon>\n </button>\n } @else {\n @if (action.buttonVariant === 'stroked') { <button mat-stroked-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (action.buttonVariant === 'raised') { <button mat-raised-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (!action.buttonVariant || action.buttonVariant === 'flat') { <button mat-flat-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n }\n }\n </div>\n </mat-list-option>\n }\n }\n </mat-selection-list>\n } @else {\n <ng-container *ngTemplateOutlet=\"readList\"></ng-container>\n }\n\n <!-- Read-only list -->\n <ng-template #readList>\n <mat-list>\n @for (section of sections$ | async; track trackBySection($index, section); let sidx = $index) {\n @if (section.key) {\n <div class=\"section-header mat-subheader\">\n {{ sectionHeader(section.key) }}\n </div>\n }\n @for (item of section.items; track trackByItem($index, item); let i = $index) {\n <mat-list-item (click)=\"onItemClick(item, i, section.key || undefined)\">\n <div class=\"list-item-content\">\n @if (leading(item); as lead) { <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon> }\n @if (meta(item); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(item)?.class\" [style.cssText]=\"primary(item)?.style\">{{ primary(item)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(item)?.class\" [style.cssText]=\"secondary(item)?.style\">{{ secondary(item)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) { <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">{{ m.value }}</div> }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip>{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon>{{ starIcon(idx, m.value) }}</mat-icon> } }\n @default { <span>{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(item); as tr) {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip>{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n @for (action of visibleActions(item); let aidx = $index; track action?.id ?? $index) {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n }\n </div>\n </mat-list-item>\n }\n @if (config.layout?.dividers === 'between') { <mat-divider></mat-divider> }\n }\n </mat-list>\n </ng-template>\n } @else {\n <ng-container *ngTemplateOutlet=\"cardsVariant\"></ng-container>\n }\n\n <!-- Cards variant -->\n <ng-template #cardsVariant>\n <div class=\"cards-grid\">\n @for (it of (items$ | async) ?? []; track $index; let i = $index) {\n <div class=\"item-card\" (click)=\"onItemClick(it, i)\">\n <div class=\"list-item-content\">\n @if (leading(it); as lead) {\n @switch (lead.type) {\n @case ('icon') { <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon> }\n @case ('image') {\n <div class=\"lead-image\" [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">\n <img [src]=\"lead.value\" [alt]=\"config.templating?.leading?.imageAlt || ''\" />\n @if (lead.badge?.value) { <mat-chip class=\"lead-badge\" [color]=\"lead.badge?.color || undefined\" [ngClass]=\"(((lead.badge?.variant || 'filled') === 'outlined') ? 'chip-outlined' : '')\">{{ lead.badge?.value }}</mat-chip> }\n </div>\n }\n }\n }\n @if (meta(it); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(it)?.class\" [style.cssText]=\"primary(it)?.style\">{{ primary(it)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(it)?.class\" [style.cssText]=\"secondary(it)?.style\">{{ secondary(it)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) { <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">{{ m.value }}</div> }\n @if (featuresVisible()) {\n @for (f of (config.templating?.features || []); track $index; let fi = $index) {\n <span class=\"feature\">\n @if (f.icon && featuresMode() !== 'labels-only') { <mat-icon>{{ f.icon }}</mat-icon> }\n @if (featuresMode() !== 'icons-only') { <span [ngClass]=\"f.class\" [style.cssText]=\"f.style\">{{ featureLabel(it, f.expr) }}</span> }\n </span>\n }\n }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip [color]=\"m.color || undefined\" [ngClass]=\"((m.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon>{{ starIcon(idx, m.value) }}</mat-icon> } }\n @default { <span>{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(it); as tr) {\n @if ((config.templating?.statusPosition || 'inline') === 'top-right' && (tr?.type === 'chip' || tr?.type === 'icon')) {\n <div class=\"status-overlay\">\n @if (tr?.type === 'chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @if (tr?.type === 'icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n </div>\n } @else {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n }\n </div>\n <div class=\"card-actions\" (click)=\"$event.stopPropagation()\">\n @for (action of visibleActions(it); let aidx = $index; track action?.id ?? $index) {\n @if ((action.kind || 'icon') === 'icon') {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n } @else {\n @if (action.buttonVariant === 'stroked') { <button mat-stroked-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (action.buttonVariant === 'raised') { <button mat-raised-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (!action.buttonVariant || action.buttonVariant === 'flat') { <button mat-flat-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n }\n }\n </div>\n </div>\n }\n </div>\n </ng-template>\n \n <!-- Simple pagination controls for remote data -->\n @if (config.dataSource?.resourcePath) {\n <div class=\"paginator\">\n <button mat-stroked-button color=\"primary\" (click)=\"prevPage()\">Anterior</button>\n <button mat-stroked-button color=\"primary\" (click)=\"nextPage()\">Pr\u00F3ximo</button>\n <mat-form-field style=\"width: 120px; margin-left: 8px;\" appearance=\"outline\">\n <mat-label>Tam. p\u00E1gina</mat-label>\n <mat-select (selectionChange)=\"setPageSize($event.value)\" [value]=\"config.layout?.pageSize || 10\">\n <mat-option [value]=\"6\">6</mat-option>\n <mat-option [value]=\"8\">8</mat-option>\n <mat-option [value]=\"12\">12</mat-option>\n <mat-option [value]=\"24\">24</mat-option>\n </mat-select>\n </mat-form-field>\n @if (total$ | async; as total) { <span class=\"muted\">Total: {{ total }}</span> }\n </div>\n }\n </ng-template>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.praxis-list-root{--p-list-radius: 1rem;--p-list-shadow: 0 6px 20px rgba(0, 0, 0, .08);--p-list-border: 1px solid rgba(0, 0, 0, .08);--p-list-blur: 8px;--p-list-grad-from: #8e72ff;--p-list-grad-to: #ffaa70;--p-list-grad-angle: 135deg}.skin-elevated .item-card{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:var(--p-list-radius);box-shadow:var(--p-list-shadow);border:var(--p-list-border)}.skin-pill-soft .item-card{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:calc(var(--p-list-radius) * 1.3);box-shadow:0 4px 16px #00000014;border:var(--p-list-border)}.skin-pill-soft .mat-mdc-list-item .list-item-content{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:calc(var(--p-list-radius) * 1.3);box-shadow:0 4px 16px #00000014;border:var(--p-list-border);color:var(--p-list-foreground, #1f2937)}.skin-gradient-tile .item-card,.skin-gradient-tile .mat-mdc-list-item .list-item-content{background:linear-gradient(var(--p-list-grad-angle),var(--p-list-grad-from),var(--p-list-grad-to));border-radius:var(--p-list-radius);color:#fff}.skin-glass .item-card{background:#ffffff2e;border-radius:var(--p-list-radius);border:1px solid rgba(255,255,255,.3);backdrop-filter:blur(var(--p-list-blur));-webkit-backdrop-filter:blur(var(--p-list-blur))}.skin-glass .mat-mdc-list-item .list-item-content{background:#ffffff2e;border-radius:var(--p-list-radius);border:1px solid rgba(255,255,255,.3);backdrop-filter:blur(var(--p-list-blur));-webkit-backdrop-filter:blur(var(--p-list-blur));color:var(--p-list-foreground, #111)}.density-compact .mat-mdc-list-item,.density-compact .item-card{--_item-padding: 4px 8px}.density-comfortable .mat-mdc-list-item,.density-comfortable .item-card{--_item-padding: 8px 12px}.list-item-content{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;padding:var(--_item-padding, 12px 16px)}.lead-image{position:relative;width:96px;height:72px;border-radius:12px;overflow:hidden}.lead-image img{width:100%;height:100%;object-fit:cover;display:block}.lead-image .lead-badge{position:absolute;left:8px;bottom:8px}.model-media .lead-image{width:136px;height:96px;border-radius:16px}.model-media .list-item-content{gap:16px}.model-hotel .lead-image{width:160px;height:110px;border-radius:18px}.primary{font-weight:600;color:var(--sicoob-tertiary-default, #001E24)}.secondary{opacity:.8}.tertiary{opacity:.68}.features{display:flex;flex-wrap:wrap;gap:12px;margin-top:6px;color:inherit}.feature{display:inline-flex;align-items:center;gap:6px}.meta{opacity:.9}.trailing{opacity:.9;margin-left:8px}.section-header{font-size:.85rem;opacity:.7;padding:8px 12px}.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.item-card{padding:12px;display:flex;flex-direction:column;min-height:160px;position:relative;cursor:pointer}.item-card:hover{box-shadow:0 10px 24px var(--sicoob-shadow-medium, rgba(0, 0, 0, .12));transform:translateY(-1px);transition:box-shadow .15s ease,transform .15s ease}.card-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px;padding:6px 8px 4px;border-top:1px solid var(--sicoob-stroke-medium, rgba(0, 0, 0, .08));margin-top:auto}.status-overlay{position:absolute;top:10px;right:10px;pointer-events:none}.status-overlay .mat-mdc-chip{pointer-events:auto}.leading-icon.mat-icon{width:36px;height:36px;line-height:36px;font-size:20px;display:inline-flex;align-items:center;justify-content:center;border-radius:999px;background:color-mix(in srgb,var(--sicoob-info-lightest, #f0f9ff),transparent 30%);color:var(--sicoob-info-default, #0090e0)}.mat-mdc-chip{--mdc-chip-container-height: 22px;font-size:12px}.meta .mat-icon{font-size:18px;height:18px;width:18px}.primary,.secondary{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.primary,.secondary{-webkit-line-clamp:1}.features{overflow:hidden}@keyframes shimmer{0%{background-position:-468px 0}to{background-position:468px 0}}.skeleton{animation-duration:1.2s;animation-fill-mode:forwards;animation-iteration-count:infinite;animation-name:shimmer;animation-timing-function:linear;background:#f6f7f8;background:linear-gradient(to right,#eee 8%,#ddd 18%,#eee 33%);background-size:800px 104px;position:relative}.skeleton-avatar{width:32px;height:32px;border-radius:50%}.skeleton-line{height:10px;margin:6px 0;border-radius:6px}.skeleton-chip{width:48px;height:18px;border-radius:999px}.chip-outlined.mat-mdc-chip{background:transparent!important;border:1px solid currentColor}.w-60{width:60%}.w-40{width:40%}.paginator{display:flex;align-items:center;gap:8px;padding:8px 4px}.muted{opacity:.7}\n"] }]
3502
+ MatInputModule,
3503
+ ], changeDetection: ChangeDetectionStrategy.OnPush, providers: [GenericCrudService, ListDataService], template: "<div\n class=\"praxis-list-root\"\n [ngClass]=\"skinClasses\"\n [attr.aria-label]=\"config.a11y?.ariaLabel || null\"\n [attr.aria-labelledby]=\"config.a11y?.ariaLabelledBy || null\"\n>\n @if (inlineCss) {\n <style [innerHTML]=\"inlineCss\"></style>\n }\n\n <!-- Skeleton while loading -->\n @if ((loading$ | async) && hasSkeleton()) {\n @if (isListVariant()) {\n <mat-list>\n @for (_ of skeletonItems(); track $index; let i = $index) {\n <mat-list-item>\n <div class=\"list-item-content\">\n <div class=\"skeleton skeleton-avatar\"></div>\n <div style=\"width:100%\">\n <div class=\"skeleton skeleton-line w-60\"></div>\n @if (layoutLines > 1) { <div class=\"skeleton skeleton-line w-40\"></div> }\n </div>\n <div class=\"skeleton skeleton-chip\"></div>\n </div>\n </mat-list-item>\n }\n </mat-list>\n } @else {\n <div class=\"cards-grid\">\n @for (_ of skeletonItems(); track $index; let i = $index) {\n <div class=\"item-card\">\n <div class=\"list-item-content\">\n <div class=\"skeleton skeleton-avatar\"></div>\n <div style=\"width:100%\">\n <div class=\"skeleton skeleton-line w-60\"></div>\n @if (layoutLines > 1) { <div class=\"skeleton skeleton-line w-40\"></div> }\n </div>\n <div class=\"skeleton skeleton-chip\"></div>\n </div>\n </div>\n }\n </div>\n }\n } @else {\n <ng-container *ngTemplateOutlet=\"notLoading\"></ng-container>\n }\n\n <ng-template #notLoading>\n <!-- Empty state -->\n @if (items$ | async; as all) {\n @if (all.length === 0) {\n <div class=\"section-header\">\n {{ (config.templating?.emptyState?.expr || 'Nenhum item dispon\u00EDvel') }}\n </div>\n }\n }\n\n <!-- List variant -->\n @if (isListVariant()) {\n <!-- Selection list -->\n @if (isSelectionEnabled()) {\n <mat-selection-list\n [multiple]=\"config.selection?.mode === 'multiple'\"\n [formControl]=\"boundControl\"\n (selectionChange)=\"onSelectionChange($event)\"\n >\n @for (section of sections$ | async; track trackBySection($index, section)) {\n @if (section.key) {\n <div class=\"section-header mat-subheader\">\n {{ sectionHeader(section.key) }}\n </div>\n }\n @for (item of section.items; track trackByItem($index, item); let i = $index) {\n <mat-list-option\n [value]=\"item\"\n (click)=\"onItemClick(item, i, section.key || undefined)\"\n >\n <div class=\"list-item-content\">\n @if (leading(item); as lead) {\n @switch (lead.type) {\n @case ('icon') {\n <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon>\n }\n @case ('image') {\n <div class=\"lead-image\" [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">\n <img [src]=\"lead.value\" [alt]=\"config.templating?.leading?.imageAlt || ''\" />\n @if (lead.badge?.value) {\n <mat-chip class=\"lead-badge\" [color]=\"lead.badge?.color || undefined\" [ngClass]=\"(((lead.badge?.variant || 'filled') === 'outlined') ? 'chip-outlined' : '')\">{{ lead.badge?.value }}</mat-chip>\n }\n </div>\n }\n }\n }\n @if (meta(item); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(item)?.class\" [style.cssText]=\"primary(item)?.style\">{{ primary(item)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(item)?.class\" [style.cssText]=\"secondary(item)?.style\">{{ secondary(item)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @if (config.templating?.metaPrefixIcon; as mpi) { <mat-icon [praxisIcon]=\"mpi\"></mat-icon> }\n {{ m.value }}\n </div>\n }\n @if (featuresVisible()) {\n @for (f of (config.templating?.features || []); track $index; let fi = $index) {\n <span class=\"feature\">\n @if (f.icon && featuresMode() !== 'labels-only') { <mat-icon [praxisIcon]=\"f.icon\"></mat-icon> }\n @if (featuresMode() !== 'icons-only') { <span [ngClass]=\"f.class\" [style.cssText]=\"f.style\">{{ featureLabel(item, f.expr) }}</span> }\n </span>\n }\n }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip [color]=\"m.color || undefined\" [ngClass]=\"((m.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon [praxisIcon]=\"starIcon(idx, m.value)\"></mat-icon> } }\n @default { <span>@if (config.templating?.metaPrefixIcon; as mpi2) { <mat-icon>{{ mpi2 }}</mat-icon> }{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(item); as tr) {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n @for (action of visibleActions(item); let aidx = $index; track action?.id ?? $index) {\n @if ((action.kind || 'icon') === 'icon') {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon [praxisIcon]=\"action.icon\"></mat-icon>\n </button>\n } @else {\n @if (action.buttonVariant === 'stroked') { <button mat-stroked-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (action.buttonVariant === 'raised') { <button mat-raised-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (!action.buttonVariant || action.buttonVariant === 'flat') { <button mat-flat-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n }\n }\n </div>\n </mat-list-option>\n }\n }\n </mat-selection-list>\n } @else {\n <ng-container *ngTemplateOutlet=\"readList\"></ng-container>\n }\n\n <!-- Read-only list -->\n <ng-template #readList>\n <mat-list>\n @for (section of sections$ | async; track trackBySection($index, section); let sidx = $index) {\n @if (section.key) {\n <div class=\"section-header mat-subheader\">\n {{ sectionHeader(section.key) }}\n </div>\n }\n @for (item of section.items; track trackByItem($index, item); let i = $index) {\n <mat-list-item (click)=\"onItemClick(item, i, section.key || undefined)\">\n <div class=\"list-item-content\">\n @if (leading(item); as lead) { <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon> }\n @if (meta(item); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(item)?.class\" [style.cssText]=\"primary(item)?.style\">{{ primary(item)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(item)?.class\" [style.cssText]=\"secondary(item)?.style\">{{ secondary(item)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) { <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">{{ m.value }}</div> }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip>{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon>{{ starIcon(idx, m.value) }}</mat-icon> } }\n @default { <span>{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(item); as tr) {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip>{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n @for (action of visibleActions(item); let aidx = $index; track action?.id ?? $index) {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, item, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n }\n </div>\n </mat-list-item>\n }\n @if (config.layout?.dividers === 'between') { <mat-divider></mat-divider> }\n }\n </mat-list>\n </ng-template>\n } @else {\n <ng-container *ngTemplateOutlet=\"cardsVariant\"></ng-container>\n }\n\n <!-- Cards variant -->\n <ng-template #cardsVariant>\n <div class=\"cards-grid\">\n @for (it of (items$ | async) ?? []; track $index; let i = $index) {\n <div class=\"item-card\" (click)=\"onItemClick(it, i)\">\n <div class=\"list-item-content\">\n @if (leading(it); as lead) {\n @switch (lead.type) {\n @case ('icon') { <mat-icon [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">{{ lead.value }}</mat-icon> }\n @case ('image') {\n <div class=\"lead-image\" [ngClass]=\"lead.class\" [style.cssText]=\"lead.style\">\n <img [src]=\"lead.value\" [alt]=\"config.templating?.leading?.imageAlt || ''\" />\n @if (lead.badge?.value) { <mat-chip class=\"lead-badge\" [color]=\"lead.badge?.color || undefined\" [ngClass]=\"(((lead.badge?.variant || 'filled') === 'outlined') ? 'chip-outlined' : '')\">{{ lead.badge?.value }}</mat-chip> }\n </div>\n }\n }\n }\n @if (meta(it); as m) {\n <div>\n <div class=\"primary\" [ngClass]=\"primary(it)?.class\" [style.cssText]=\"primary(it)?.style\">{{ primary(it)?.value }}</div>\n @if (layoutLines > 1) { <div class=\"secondary\" [ngClass]=\"secondary(it)?.class\" [style.cssText]=\"secondary(it)?.style\">{{ secondary(it)?.value }}</div> }\n @if ((config.templating?.metaPlacement === 'line' && (m?.type === 'text' || m?.type === 'date')) || (layoutLines > 2 && (m?.type === 'text' || m?.type === 'date'))) { <div class=\"tertiary\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">{{ m.value }}</div> }\n @if (featuresVisible()) {\n @for (f of (config.templating?.features || []); track $index; let fi = $index) {\n <span class=\"feature\">\n @if (f.icon && featuresMode() !== 'labels-only') { <mat-icon>{{ f.icon }}</mat-icon> }\n @if (featuresMode() !== 'icons-only') { <span [ngClass]=\"f.class\" [style.cssText]=\"f.style\">{{ featureLabel(it, f.expr) }}</span> }\n </span>\n }\n }\n </div>\n @if (!(((config.templating?.metaPlacement === 'line') || (layoutLines > 2)) && (m?.type === 'text' || m?.type === 'date'))) {\n <div class=\"meta\" [ngClass]=\"m.class\" [style.cssText]=\"m.style\">\n @switch (m.type) {\n @case ('chip') { <mat-chip [color]=\"m.color || undefined\" [ngClass]=\"((m.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ m.value }}</mat-chip> }\n @case ('rating') { @for (_ of [0,1,2,3,4]; track $index; let idx = $index) { <mat-icon>{{ starIcon(idx, m.value) }}</mat-icon> } }\n @default { <span>{{ m.value }}</span> }\n }\n </div>\n }\n @if (trailing(it); as tr) {\n @if ((config.templating?.statusPosition || 'inline') === 'top-right' && (tr?.type === 'chip' || tr?.type === 'icon')) {\n <div class=\"status-overlay\">\n @if (tr?.type === 'chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @if (tr?.type === 'icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n </div>\n } @else {\n <div class=\"trailing\" [ngClass]=\"tr.class\" [style.cssText]=\"tr.style\">\n @switch (tr.type) {\n @case ('chip') { <mat-chip [color]=\"tr.color || undefined\" [ngClass]=\"((tr.variant || 'filled') === 'outlined') ? 'chip-outlined' : ''\">{{ tr.value }}</mat-chip> }\n @case ('icon') { <mat-icon [praxisIcon]=\"tr.value\" [color]=\"tr.color || undefined\"></mat-icon> }\n @default { <span>{{ tr.value }}</span> }\n }\n </div>\n }\n }\n }\n </div>\n <div class=\"card-actions\" (click)=\"$event.stopPropagation()\">\n @for (action of visibleActions(it); let aidx = $index; track action?.id ?? $index) {\n @if ((action.kind || 'icon') === 'icon') {\n <button mat-icon-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n } @else {\n @if (action.buttonVariant === 'stroked') { <button mat-stroked-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (action.buttonVariant === 'raised') { <button mat-raised-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n @if (!action.buttonVariant || action.buttonVariant === 'flat') { <button mat-flat-button [color]=\"action.color || undefined\" (click)=\"onActionClick($event, action.id, it, i)\" [attr.aria-label]=\"action.label || action.id\">{{ action.label || action.id }}</button> }\n }\n }\n </div>\n </div>\n }\n </div>\n </ng-template>\n \n <!-- Simple pagination controls for remote data -->\n @if (config.dataSource?.resourcePath) {\n <div class=\"paginator\" style=\"flex-wrap: wrap; gap: 8px;\">\n <div style=\"display:flex; align-items:center; gap:8px; flex-wrap: wrap;\">\n <button mat-stroked-button color=\"primary\" (click)=\"prevPage()\">Anterior</button>\n <button mat-stroked-button color=\"primary\" (click)=\"nextPage()\">Pr\u00F3ximo</button>\n <mat-form-field style=\"width: 120px;\" appearance=\"outline\">\n <mat-label>Tam. p\u00E1gina</mat-label>\n <mat-select (selectionChange)=\"setPageSize($event.value)\" [value]=\"config.layout?.pageSize || 10\">\n <mat-option [value]=\"6\">6</mat-option>\n <mat-option [value]=\"8\">8</mat-option>\n <mat-option [value]=\"12\">12</mat-option>\n <mat-option [value]=\"24\">24</mat-option>\n </mat-select>\n </mat-form-field>\n @if (config.ui?.showSort) {\n <mat-form-field style=\"width: 180px;\" appearance=\"outline\">\n <mat-label>Ordenar</mat-label>\n <mat-select (selectionChange)=\"onSortChange($event.value)\">\n @for (op of (config.ui?.sortOptions || []); track $index) {\n <mat-option [value]=\"sortOptionValue(op)\">{{ sortOptionLabel(op) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n }\n @if (config.ui?.showSearch && config.ui?.searchField) {\n <mat-form-field style=\"min-width: 220px;\" appearance=\"outline\">\n <mat-label>{{ config.ui?.searchPlaceholder || 'Buscar' }}</mat-label>\n <input matInput type=\"search\" (input)=\"onSearchInput($any($event.target).value)\" />\n </mat-form-field>\n }\n </div>\n <div style=\"display:flex; align-items:center; gap:8px; margin-left:auto;\">\n @if ((config.ui?.showRange ?? true)) {\n @if (page$ | async; as ps) {\n @if (total$ | async; as total) {\n @if (items$ | async; as curr) {\n <span class=\"muted\">{{ rangeStart(ps) }}\u2013{{ rangeEnd(curr?.length || 0, ps, total || 0) }} de {{ total || 0 }}</span>\n }\n }\n }\n } @else {\n @if (total$ | async; as total2) { <span class=\"muted\">Total: {{ total2 }}</span> }\n }\n </div>\n </div>\n }\n </ng-template>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.praxis-list-root{--p-list-radius: 1rem;--p-list-shadow: 0 6px 20px rgba(0, 0, 0, .08);--p-list-border: 1px solid rgba(0, 0, 0, .08);--p-list-blur: 8px;--p-list-grad-from: #8e72ff;--p-list-grad-to: #ffaa70;--p-list-grad-angle: 135deg}.skin-elevated .item-card{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:var(--p-list-radius);box-shadow:var(--p-list-shadow);border:var(--p-list-border)}.skin-pill-soft .item-card{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:calc(var(--p-list-radius) * 1.3);box-shadow:0 4px 16px #00000014;border:var(--p-list-border)}.skin-pill-soft .mat-mdc-list-item .list-item-content{background:var(--mdc-elevated-card-container-color, var(--md-sys-color-surface));border-radius:calc(var(--p-list-radius) * 1.3);box-shadow:0 4px 16px #00000014;border:var(--p-list-border);color:var(--p-list-foreground, #1f2937)}.skin-gradient-tile .item-card,.skin-gradient-tile .mat-mdc-list-item .list-item-content{background:linear-gradient(var(--p-list-grad-angle),var(--p-list-grad-from),var(--p-list-grad-to));border-radius:var(--p-list-radius);color:#fff}.skin-glass .item-card{background:#ffffff2e;border-radius:var(--p-list-radius);border:1px solid rgba(255,255,255,.3);backdrop-filter:blur(var(--p-list-blur));-webkit-backdrop-filter:blur(var(--p-list-blur))}.skin-glass .mat-mdc-list-item .list-item-content{background:#ffffff2e;border-radius:var(--p-list-radius);border:1px solid rgba(255,255,255,.3);backdrop-filter:blur(var(--p-list-blur));-webkit-backdrop-filter:blur(var(--p-list-blur));color:var(--p-list-foreground, #111)}.density-compact .mat-mdc-list-item,.density-compact .item-card{--_item-padding: 4px 8px}.density-comfortable .mat-mdc-list-item,.density-comfortable .item-card{--_item-padding: 8px 12px}.list-item-content{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:12px;padding:var(--_item-padding, 12px 16px)}.lead-image{position:relative;width:96px;height:72px;border-radius:12px;overflow:hidden}.lead-image img{width:100%;height:100%;object-fit:cover;display:block}.lead-image .lead-badge{position:absolute;left:8px;bottom:8px}.model-media .lead-image{width:136px;height:96px;border-radius:16px}.model-media .list-item-content{gap:16px}.model-hotel .lead-image{width:160px;height:110px;border-radius:18px}.primary{font-weight:600;color:var(--sicoob-tertiary-default, #001E24)}.secondary{opacity:.8}.tertiary{opacity:.68}.features{display:flex;flex-wrap:wrap;gap:12px;margin-top:6px;color:inherit}.feature{display:inline-flex;align-items:center;gap:6px}.meta{opacity:.9}.trailing{opacity:.9;margin-left:8px}.section-header{font-size:.85rem;opacity:.7;padding:8px 12px}.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.item-card{padding:12px;display:flex;flex-direction:column;min-height:160px;position:relative;cursor:pointer}.item-card:hover{box-shadow:0 10px 24px var(--sicoob-shadow-medium, rgba(0, 0, 0, .12));transform:translateY(-1px);transition:box-shadow .15s ease,transform .15s ease}.card-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px;padding:6px 8px 4px;border-top:1px solid var(--sicoob-stroke-medium, rgba(0, 0, 0, .08));margin-top:auto}.status-overlay{position:absolute;top:10px;right:10px;pointer-events:none}.status-overlay .mat-mdc-chip{pointer-events:auto}.leading-icon.mat-icon{width:36px;height:36px;line-height:36px;font-size:20px;display:inline-flex;align-items:center;justify-content:center;border-radius:999px;background:color-mix(in srgb,var(--sicoob-info-lightest, #f0f9ff),transparent 30%);color:var(--sicoob-info-default, #0090e0)}.mat-mdc-chip{--mdc-chip-container-height: 22px;font-size:12px}.meta .mat-icon{font-size:18px;height:18px;width:18px}.primary,.secondary{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.primary,.secondary{-webkit-line-clamp:1}.features{overflow:hidden}@keyframes shimmer{0%{background-position:-468px 0}to{background-position:468px 0}}.skeleton{animation-duration:1.2s;animation-fill-mode:forwards;animation-iteration-count:infinite;animation-name:shimmer;animation-timing-function:linear;background:#f6f7f8;background:linear-gradient(to right,#eee 8%,#ddd 18%,#eee 33%);background-size:800px 104px;position:relative}.skeleton-avatar{width:32px;height:32px;border-radius:50%}.skeleton-line{height:10px;margin:6px 0;border-radius:6px}.skeleton-chip{width:48px;height:18px;border-radius:999px}.chip-outlined.mat-mdc-chip{background:transparent!important;border:1px solid currentColor}.w-60{width:60%}.w-40{width:40%}.paginator{display:flex;align-items:center;gap:8px;padding:8px 4px}.muted{opacity:.7}\n"] }]
3013
3504
  }], propDecorators: { config: [{
3014
3505
  type: Input
3015
3506
  }], form: [{