@praxisui/tabs 8.0.0-beta.11 → 8.0.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,6 +75,7 @@ Inputs
75
75
  - `componentInstanceId?: string` Opcional para desambiguar múltiplas instâncias com o mesmo `tabsId` na mesma rota.
76
76
  - `form?: FormGroup` FormGroup opcional para campos dinâmicos declarados em `content`.
77
77
  - `context?: any` Contexto propagado a widgets internos (via `DynamicWidgetLoader`).
78
+ - `selectedIndex?: number` Índice ativo controlado por composição; não reemite `selectedIndexChange`.
78
79
  - `enableCustomization?: boolean` Exibe botão de edição quando verdadeiro (abre o editor).
79
80
 
80
81
  Outputs
@@ -83,6 +84,38 @@ Outputs
83
84
  - `focusChange, animationDone, indexFocused, selectFocusedIndex` Eventos nativos do Angular Material.
84
85
  - `widgetEvent: WidgetEventEnvelope` Bridge avançada/legado para transporte de eventos dos widgets internos com contexto da aba/link. Para conexões novas de widgets internos, use `composition.links` com `component-port + nestedPath`.
85
86
 
87
+ ## Uso Controlado por Composição
88
+
89
+ `selectedIndex` é a porta pública para controlar a aba ativa a partir de estado externo ou de outro componente. Quando esse input é aplicado, o componente atualiza a aba ativa sem reemitir `selectedIndexChange`, evitando ciclos entre `state -> selectedIndex` e `selectedIndexChange -> state`.
90
+
91
+ Padrão recomendado para páginas dinâmicas:
92
+
93
+ - grave a seleção do usuário com `selectedIndexChange -> state`;
94
+ - projete o estado de volta com `state -> selectedIndex`;
95
+ - declare `selectedIndex` depois de `config` em `bindingOrder`, para que a configuração seja carregada antes da seleção controlada;
96
+ - não persista `inputs.selectedIndex` estático em recipes quando a seleção vem de `composition.links`.
97
+
98
+ Quando existe configuração persistida por `tabsId`, o valor controlado por `selectedIndex` vence a restauração local depois do carregamento da config. Essa projeção controlada não grava uma nova preferência no storage; somente interações diretas do usuário persistem seleção. Isso mantém o estado canônico da composição como fonte de verdade da navegação ativa sem transformar navegação transitória em configuração salva. Em modo `nav`, o índice controla `nav.links`; em modo `group`, controla `tabs`.
99
+
100
+ Exemplo canônico:
101
+
102
+ ```json
103
+ {
104
+ "links": [
105
+ {
106
+ "id": "tabs.selectedIndexChange->state.navigation.activeTabIndex",
107
+ "from": { "kind": "component-port", "ref": { "widget": "workspaceTabs", "port": "selectedIndexChange" } },
108
+ "to": { "kind": "state", "ref": { "path": "navigation.activeTabIndex", "layer": "values", "write": true } }
109
+ },
110
+ {
111
+ "id": "state.navigation.activeTabIndex->tabs.selectedIndex",
112
+ "from": { "kind": "state", "ref": { "path": "navigation.activeTabIndex", "layer": "values" } },
113
+ "to": { "kind": "component-port", "ref": { "widget": "workspaceTabs", "port": "selectedIndex" } }
114
+ }
115
+ ]
116
+ }
117
+ ```
118
+
86
119
  Persistência
87
120
  - Quando `tabsId` é fornecido, a configuração é salva/recuperada em `AsyncConfigStorage` na chave `tabs:<component_id>`.
88
121
  - O `component_id` é derivado via `ComponentKeyService` (inclui rota, tipo de componente, `tabsId` e `componentInstanceId` quando informado).
@@ -110,8 +143,8 @@ export interface TabsMetadata {
110
143
  behavior?: { lazyLoad?: boolean; closeable?: boolean; reorderable?: boolean };
111
144
  accessibility?: { highContrast?: boolean; reduceMotion?: boolean };
112
145
  group?: { alignTabs?: 'start' | 'center' | 'end'; headerPosition?: 'above'|'below'; selectedIndex?: number; dynamicHeight?: boolean; disableRipple?: boolean; disablePagination?: boolean; fitInkBarToContent?: boolean; stretchTabs?: boolean; color?: 'primary'|'accent'|'warn'; backgroundColor?: 'primary'|'accent'|'warn'|undefined; animationDuration?: string; ariaLabel?: string; ariaLabelledby?: string; };
113
- tabs?: Array<{ id?: string; textLabel?: string; disabled?: boolean; labelClass?: string|string[]; bodyClass?: string|string[]; content?: any[]; widgets?: WidgetDefinition[] }>;
114
- nav?: { links: Array<{ id?: string; label: string; disabled?: boolean; content?: any[]; widgets?: WidgetDefinition[] }>; selectedIndex?: number; disableRipple?: boolean; disablePagination?: boolean; fitInkBarToContent?: boolean; stretchTabs?: boolean; color?: 'primary'|'accent'|'warn'; backgroundColor?: 'primary'|'accent'|'warn'|undefined; animationDuration?: string; ariaLabel?: string; ariaLabelledby?: string };
146
+ tabs?: Array<{ id?: string; textLabel?: string; icon?: string; disabled?: boolean; visible?: boolean; labelClass?: string|string[]; bodyClass?: string|string[]; content?: any[]; widgets?: WidgetDefinition[] }>;
147
+ nav?: { links: Array<{ id?: string; label: string; icon?: string; disabled?: boolean; visible?: boolean; content?: any[]; widgets?: WidgetDefinition[] }>; selectedIndex?: number; disableRipple?: boolean; disablePagination?: boolean; fitInkBarToContent?: boolean; stretchTabs?: boolean; color?: 'primary'|'accent'|'warn'; backgroundColor?: 'primary'|'accent'|'warn'|undefined; animationDuration?: string; ariaLabel?: string; ariaLabelledby?: string };
115
148
  }
116
149
  ```
117
150
 
@@ -153,6 +186,7 @@ Quick Setup
153
186
  - **Editable targets:** `tab`, `tabLabel`, `tabIcon`, `tabContent`, `activeTab`, `visibility`, `disabledState` e `layout`.
154
187
  - **Operation families:** `tab.add`, `tab.remove`, `tab.label.set`, `tab.icon.set`, `tab.order.set`, `tab.disabled.set`, `tab.visible.set`, `tab.active.set`, `layout.variant.set` e `tab.content.set`.
155
188
  - **Validation:** ids de abas/links devem ser estáveis e únicos, remoção destrutiva exige confirmação, `group` e `nav` não devem virar modos primários concorrentes, e o round-trip precisa preservar ordem, ids e selected index.
189
+ - **Runtime/editor parity:** `tabs[].icon`, `tabs[].visible`, `nav.links[].icon` e `nav.links[].visible` são campos canônicos editáveis; itens com `visible: false` não são renderizados na navegação.
156
190
  - **Registry projection:** o manifesto é exportado no `public-api` e projetado em `components['praxis-tabs'].authoringManifest` pelo AI Registry.
157
191
 
158
192
  ## Eventos e Conexões
@@ -784,6 +784,7 @@ class PraxisTabsConfigEditor {
784
784
  this.editedConfig.tabs.push({
785
785
  id: `tab${(this.editedConfig.tabs.length + 1)}`,
786
786
  textLabel: this.t('defaults.newTabLabel', 'New Tab'),
787
+ visible: true,
787
788
  });
788
789
  this.onAppearanceChange();
789
790
  }
@@ -859,6 +860,7 @@ class PraxisTabsConfigEditor {
859
860
  this.nav.links.push({
860
861
  id: `link${this.nav.links.length + 1}`,
861
862
  label: this.t('defaults.newLinkLabel', 'New Link'),
863
+ visible: true,
862
864
  });
863
865
  this.onAppearanceChange();
864
866
  }
@@ -1293,6 +1295,9 @@ class PraxisTabsConfigEditor {
1293
1295
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1294
1296
  <input matInput [(ngModel)]="tab.textLabel" (ngModelChange)="onAppearanceChange()" />
1295
1297
  </mat-form-field>
1298
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1299
+ <input matInput [(ngModel)]="tab.icon" (ngModelChange)="onAppearanceChange()" />
1300
+ </mat-form-field>
1296
1301
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.labelClass', 'Classe do rotulo') }}</mat-label>
1297
1302
  <input matInput [(ngModel)]="tab.labelClass" (ngModelChange)="onAppearanceChange()" />
1298
1303
  </mat-form-field>
@@ -1306,7 +1311,10 @@ class PraxisTabsConfigEditor {
1306
1311
  <input matInput [(ngModel)]="tab.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
1307
1312
  </mat-form-field>
1308
1313
  </div>
1309
- <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1314
+ <div class="editor-row">
1315
+ <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1316
+ <mat-slide-toggle [ngModel]="tab.visible !== false" (ngModelChange)="tab.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1317
+ </div>
1310
1318
 
1311
1319
  <!-- Widgets (componentes dinâmicos) -->
1312
1320
  <div class="editor-divider editor-grid">
@@ -1384,9 +1392,13 @@ class PraxisTabsConfigEditor {
1384
1392
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1385
1393
  <input matInput [(ngModel)]="l.label" (ngModelChange)="onAppearanceChange()" />
1386
1394
  </mat-form-field>
1395
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1396
+ <input matInput [(ngModel)]="l.icon" (ngModelChange)="onAppearanceChange()" />
1397
+ </mat-form-field>
1387
1398
  </div>
1388
1399
  <div class="editor-row">
1389
1400
  <mat-slide-toggle [(ngModel)]="l.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.linkDisabled', 'Desativado') }}</mat-slide-toggle>
1401
+ <mat-slide-toggle [ngModel]="l.visible !== false" (ngModelChange)="l.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1390
1402
  <mat-slide-toggle [(ngModel)]="l.disableRipple" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disableRipple', 'Sem ripple') }}</mat-slide-toggle>
1391
1403
  <mat-slide-toggle [(ngModel)]="l.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.fitInkBarToContent', 'Indicador ajustado ao conteudo') }}</mat-slide-toggle>
1392
1404
  </div>
@@ -1797,6 +1809,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1797
1809
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1798
1810
  <input matInput [(ngModel)]="tab.textLabel" (ngModelChange)="onAppearanceChange()" />
1799
1811
  </mat-form-field>
1812
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1813
+ <input matInput [(ngModel)]="tab.icon" (ngModelChange)="onAppearanceChange()" />
1814
+ </mat-form-field>
1800
1815
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.labelClass', 'Classe do rotulo') }}</mat-label>
1801
1816
  <input matInput [(ngModel)]="tab.labelClass" (ngModelChange)="onAppearanceChange()" />
1802
1817
  </mat-form-field>
@@ -1810,7 +1825,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1810
1825
  <input matInput [(ngModel)]="tab.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
1811
1826
  </mat-form-field>
1812
1827
  </div>
1813
- <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1828
+ <div class="editor-row">
1829
+ <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1830
+ <mat-slide-toggle [ngModel]="tab.visible !== false" (ngModelChange)="tab.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1831
+ </div>
1814
1832
 
1815
1833
  <!-- Widgets (componentes dinâmicos) -->
1816
1834
  <div class="editor-divider editor-grid">
@@ -1888,9 +1906,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1888
1906
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1889
1907
  <input matInput [(ngModel)]="l.label" (ngModelChange)="onAppearanceChange()" />
1890
1908
  </mat-form-field>
1909
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1910
+ <input matInput [(ngModel)]="l.icon" (ngModelChange)="onAppearanceChange()" />
1911
+ </mat-form-field>
1891
1912
  </div>
1892
1913
  <div class="editor-row">
1893
1914
  <mat-slide-toggle [(ngModel)]="l.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.linkDisabled', 'Desativado') }}</mat-slide-toggle>
1915
+ <mat-slide-toggle [ngModel]="l.visible !== false" (ngModelChange)="l.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1894
1916
  <mat-slide-toggle [(ngModel)]="l.disableRipple" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disableRipple', 'Sem ripple') }}</mat-slide-toggle>
1895
1917
  <mat-slide-toggle [(ngModel)]="l.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.fitInkBarToContent', 'Indicador ajustado ao conteudo') }}</mat-slide-toggle>
1896
1918
  </div>
@@ -2372,6 +2394,12 @@ class PraxisTabs {
2372
2394
  config = null;
2373
2395
  tabsId;
2374
2396
  componentInstanceId;
2397
+ set selectedIndex(index) {
2398
+ if (index == null)
2399
+ return;
2400
+ this.controlledSelectedIndex = index;
2401
+ this.applySelectedIndex(index, false, false);
2402
+ }
2375
2403
  enableCustomization = false;
2376
2404
  form = null;
2377
2405
  context = null;
@@ -2388,6 +2416,7 @@ class PraxisTabs {
2388
2416
  selectedIndexSignal = signal(0, ...(ngDevMode ? [{ debugName: "selectedIndexSignal" }] : []));
2389
2417
  groupLoaded = new Set();
2390
2418
  navLoaded = new Set();
2419
+ controlledSelectedIndex;
2391
2420
  destroy$ = new Subject();
2392
2421
  widgetDefinitionCache = new WeakMap();
2393
2422
  ngOnInit() {
@@ -2400,6 +2429,7 @@ class PraxisTabs {
2400
2429
  this.config = stored;
2401
2430
  }
2402
2431
  this.syncSelectionFromConfig();
2432
+ this.reapplyControlledSelectedIndex();
2403
2433
  });
2404
2434
  }
2405
2435
  }
@@ -2409,6 +2439,7 @@ class PraxisTabs {
2409
2439
  this.syncSelectionFromConfig();
2410
2440
  // Persist when tabsId provided
2411
2441
  this.persistConfig(this.config);
2442
+ this.reapplyControlledSelectedIndex();
2412
2443
  }
2413
2444
  }
2414
2445
  ngOnDestroy() {
@@ -2427,23 +2458,39 @@ class PraxisTabs {
2427
2458
  getNavActive(i) {
2428
2459
  return this.currentNavIndex() === i;
2429
2460
  }
2461
+ visibleNavLinkEntries() {
2462
+ return (this.config?.nav?.links ?? [])
2463
+ .map((link, index) => ({ link, index }))
2464
+ .filter((entry) => entry.link.visible !== false);
2465
+ }
2466
+ visibleTabEntries() {
2467
+ return (this.config?.tabs ?? [])
2468
+ .map((tab, index) => ({ tab, index }))
2469
+ .filter((entry) => entry.tab.visible !== false);
2470
+ }
2471
+ selectedVisibleNavIndex() {
2472
+ const entries = this.visibleNavLinkEntries();
2473
+ const index = entries.findIndex((entry) => entry.index === this.currentNavIndex());
2474
+ return index >= 0 ? index : 0;
2475
+ }
2476
+ selectedVisibleTabIndex() {
2477
+ const entries = this.visibleTabEntries();
2478
+ const index = entries.findIndex((entry) => entry.index === this.selectedIndexSignal());
2479
+ return index >= 0 ? index : 0;
2480
+ }
2481
+ onVisibleTabIndexChange(index) {
2482
+ const entry = this.visibleTabEntries()[index];
2483
+ if (!entry)
2484
+ return;
2485
+ this.onSelectedIndexChange(entry.index);
2486
+ }
2430
2487
  onNavClick(i) {
2431
2488
  if (!this.config?.nav?.links?.length)
2432
2489
  return;
2433
2490
  const linksCount = this.config.nav.links.length;
2434
2491
  if (i < 0 || i >= linksCount)
2435
2492
  return;
2436
- this.currentNavIndex.set(i);
2437
- this.config = produce(this.config, (draft) => {
2438
- if (!draft.nav)
2439
- return;
2440
- draft.nav.selectedIndex = i;
2441
- });
2442
- this.persistConfig(this.config);
2443
- // Lazy: mark as loaded
2444
- this.navLoaded.add(i);
2445
- // Emit as index change for consumers to track
2446
- this.selectedIndexChange.emit(i);
2493
+ this.applySelectedIndex(i, true);
2447
2494
  }
2448
2495
  onNavDrop(event) {
2449
2496
  if (!this.config?.nav?.links)
@@ -2491,10 +2538,39 @@ class PraxisTabs {
2491
2538
  });
2492
2539
  this.persistConfig(this.config);
2493
2540
  }
2541
+ onVisibleNavDrop(event) {
2542
+ const entries = this.visibleNavLinkEntries();
2543
+ const previous = entries[event.previousIndex];
2544
+ const current = entries[event.currentIndex];
2545
+ if (!previous || !current)
2546
+ return;
2547
+ this.onNavDrop({
2548
+ ...event,
2549
+ previousIndex: previous.index,
2550
+ currentIndex: current.index,
2551
+ });
2552
+ }
2494
2553
  onSelectedIndexChange(index) {
2554
+ this.applySelectedIndex(index, true);
2555
+ }
2556
+ applySelectedIndex(index, emit, persist = true) {
2557
+ if (this.isNavMode() && this.config) {
2558
+ const selected = this.clampIndex(index, this.config?.nav?.links?.length ?? 0);
2559
+ this.currentNavIndex.set(selected);
2560
+ this.config = produce(this.config, (draft) => {
2561
+ draft.nav.selectedIndex = selected;
2562
+ });
2563
+ if (persist) {
2564
+ this.persistConfig(this.config);
2565
+ }
2566
+ this.navLoaded.add(selected);
2567
+ if (emit) {
2568
+ this.selectedIndexChange.emit(selected);
2569
+ }
2570
+ return;
2571
+ }
2495
2572
  const selected = this.clampIndex(index, this.config?.tabs?.length ?? 0);
2496
2573
  this.selectedIndexSignal.set(selected);
2497
- // Update config immutably
2498
2574
  if (this.config) {
2499
2575
  this.config = produce(this.config, (draft) => {
2500
2576
  if (!draft.group) {
@@ -2504,11 +2580,20 @@ class PraxisTabs {
2504
2580
  draft.group.selectedIndex = selected;
2505
2581
  }
2506
2582
  });
2507
- this.persistConfig(this.config);
2583
+ if (persist) {
2584
+ this.persistConfig(this.config);
2585
+ }
2508
2586
  }
2509
- // Lazy: mark as loaded
2510
2587
  this.groupLoaded.add(selected);
2511
- this.selectedIndexChange.emit(selected);
2588
+ if (emit) {
2589
+ this.selectedIndexChange.emit(selected);
2590
+ }
2591
+ }
2592
+ reapplyControlledSelectedIndex() {
2593
+ if (this.controlledSelectedIndex == null) {
2594
+ return;
2595
+ }
2596
+ this.applySelectedIndex(this.controlledSelectedIndex, false, false);
2512
2597
  }
2513
2598
  closeTab(index) {
2514
2599
  if (!this.config?.tabs)
@@ -2764,10 +2849,16 @@ class PraxisTabs {
2764
2849
  return !this.isLazy() || this.navLoaded.has(index) || this.currentNavIndex() === index;
2765
2850
  }
2766
2851
  isEmptyGlobal() {
2767
- const hasTabs = !!(this.config?.tabs && this.config.tabs.length > 0);
2768
- const hasLinks = !!(this.config?.nav?.links && this.config.nav.links.length > 0);
2852
+ const hasTabs = this.visibleTabEntries().length > 0;
2853
+ const hasLinks = this.visibleNavLinkEntries().length > 0;
2769
2854
  return !(hasTabs || hasLinks);
2770
2855
  }
2856
+ trackVisibleNavLink(index, entry) {
2857
+ return entry.link.id || `${entry.link.label || 'nav-link'}:${entry.index ?? index}`;
2858
+ }
2859
+ trackVisibleTab(index, entry) {
2860
+ return entry.tab.id || entry.tab.textLabel || `tab:${entry.index ?? index}`;
2861
+ }
2771
2862
  trackNavLink(index, link) {
2772
2863
  return link.id || `${link.label || 'nav-link'}:${index}`;
2773
2864
  }
@@ -2952,7 +3043,7 @@ class PraxisTabs {
2952
3043
  return JSON.parse(JSON.stringify(widget));
2953
3044
  }
2954
3045
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
2955
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisTabs, isStandalone: true, selector: "praxis-tabs", inputs: { config: "config", tabsId: "tabsId", componentInstanceId: "componentInstanceId", enableCustomization: "enableCustomization", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", widgetEvent: "widgetEvent" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], usesOnChanges: true, ngImport: i0, template: `
3046
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisTabs, isStandalone: true, selector: "praxis-tabs", inputs: { config: "config", tabsId: "tabsId", componentInstanceId: "componentInstanceId", selectedIndex: "selectedIndex", enableCustomization: "enableCustomization", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", widgetEvent: "widgetEvent" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], usesOnChanges: true, ngImport: i0, template: `
2956
3047
  <div
2957
3048
  class="praxis-tabs-root"
2958
3049
  [class.density-compact]="config?.appearance?.density === 'compact'"
@@ -2995,13 +3086,13 @@ class PraxisTabs {
2995
3086
  cdkDropList
2996
3087
  cdkDropListOrientation="horizontal"
2997
3088
  [cdkDropListDisabled]="!config?.behavior?.reorderable"
2998
- (cdkDropListDropped)="onNavDrop($event)"
3089
+ (cdkDropListDropped)="onVisibleNavDrop($event)"
2999
3090
  [disablePagination]="config?.nav?.disablePagination"
3000
3091
  [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
3001
3092
  [mat-stretch-tabs]="config?.nav?.stretchTabs"
3002
3093
  [color]="config?.nav?.color"
3003
3094
  [backgroundColor]="config?.nav?.backgroundColor"
3004
- [selectedIndex]="currentNavIndex()"
3095
+ [selectedIndex]="selectedVisibleNavIndex()"
3005
3096
  [attr.aria-label]="config?.nav?.ariaLabel || config?.group?.ariaLabel || null"
3006
3097
  [attr.aria-labelledby]="config?.nav?.ariaLabelledby || config?.group?.ariaLabelledby || null"
3007
3098
  [animationDuration]="effectiveAnimationDuration()"
@@ -3010,21 +3101,22 @@ class PraxisTabs {
3010
3101
  >
3011
3102
  <a
3012
3103
  mat-tab-link
3013
- *ngFor="let link of config?.nav?.links; let i = index; trackBy: trackNavLink"
3104
+ *ngFor="let entry of visibleNavLinkEntries(); let i = index; trackBy: trackVisibleNavLink"
3014
3105
  cdkDrag
3015
3106
  [cdkDragDisabled]="!config?.behavior?.reorderable"
3016
3107
  cdkDragLockAxis="x"
3017
- [active]="getNavActive(i)"
3018
- [disabled]="link.disabled"
3019
- [disableRipple]="config?.nav?.disableRipple || link.disableRipple"
3020
- [fitInkBarToContent]="link.fitInkBarToContent || false"
3021
- [id]="link.id || ''"
3022
- (click)="onNavClick(i)"
3108
+ [active]="getNavActive(entry.index)"
3109
+ [disabled]="entry.link.disabled"
3110
+ [disableRipple]="config?.nav?.disableRipple || entry.link.disableRipple"
3111
+ [fitInkBarToContent]="entry.link.fitInkBarToContent || false"
3112
+ [id]="entry.link.id || ''"
3113
+ (click)="onNavClick(entry.index)"
3023
3114
  >
3024
3115
  <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
3025
3116
  <mat-icon fontIcon="drag_indicator"></mat-icon>
3026
3117
  </span>
3027
- {{ link.label }}
3118
+ <mat-icon *ngIf="entry.link.icon" class="tab-label-icon" [praxisIcon]="entry.link.icon"></mat-icon>
3119
+ {{ entry.link.label }}
3028
3120
  </a>
3029
3121
  </nav>
3030
3122
 
@@ -3074,7 +3166,7 @@ class PraxisTabs {
3074
3166
  [fitInkBarToContent]="config?.group?.fitInkBarToContent"
3075
3167
  [headerPosition]="config?.group?.headerPosition ?? 'above'"
3076
3168
  [preserveContent]="config?.group?.preserveContent"
3077
- [selectedIndex]="selectedIndexSignal()"
3169
+ [selectedIndex]="selectedVisibleTabIndex()"
3078
3170
  [mat-stretch-tabs]="config?.group?.stretchTabs"
3079
3171
  [mat-align-tabs]="config?.group?.alignTabs || 'start'"
3080
3172
  [color]="config?.group?.color"
@@ -3084,26 +3176,27 @@ class PraxisTabs {
3084
3176
  [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
3085
3177
  (animationDone)="animationDone.emit()"
3086
3178
  (focusChange)="focusChange.emit($event)"
3087
- (selectedIndexChange)="onSelectedIndexChange($event)"
3179
+ (selectedIndexChange)="onVisibleTabIndexChange($event)"
3088
3180
  (selectedTabChange)="selectedTabChange.emit($event)"
3089
3181
  class="praxis-tabs-group"
3090
3182
  >
3091
3183
  <mat-tab
3092
- *ngFor="let tab of config?.tabs; let i = index; trackBy: trackTab"
3093
- [disabled]="tab.disabled"
3094
- [labelClass]="tab.labelClass ?? ''"
3095
- [bodyClass]="tab.bodyClass ?? ''"
3096
- [id]="tab.id || ''"
3097
- [attr.aria-label]="tab.ariaLabel || null"
3098
- [attr.aria-labelledby]="tab.ariaLabelledby || null"
3184
+ *ngFor="let entry of visibleTabEntries(); let i = index; trackBy: trackVisibleTab"
3185
+ [disabled]="entry.tab.disabled"
3186
+ [labelClass]="entry.tab.labelClass ?? ''"
3187
+ [bodyClass]="entry.tab.bodyClass ?? ''"
3188
+ [id]="entry.tab.id || ''"
3189
+ [attr.aria-label]="entry.tab.ariaLabel || null"
3190
+ [attr.aria-labelledby]="entry.tab.ariaLabelledby || null"
3099
3191
  >
3100
3192
  <ng-template mat-tab-label>
3101
- <span>{{ tab.textLabel }}</span>
3193
+ <mat-icon *ngIf="entry.tab.icon" class="tab-label-icon" [praxisIcon]="entry.tab.icon"></mat-icon>
3194
+ <span>{{ entry.tab.textLabel }}</span>
3102
3195
  <button
3103
3196
  *ngIf="config?.behavior?.closeable"
3104
3197
  mat-icon-button
3105
3198
  type="button"
3106
- (click)="closeTab(i); $event.stopPropagation()"
3199
+ (click)="closeTab(entry.index); $event.stopPropagation()"
3107
3200
  (keydown.enter)="$event.stopPropagation()"
3108
3201
  (keydown.space)="$event.stopPropagation()"
3109
3202
  [attr.aria-label]="t('chrome.closeTab', 'Fechar aba')"
@@ -3114,10 +3207,10 @@ class PraxisTabs {
3114
3207
  <button
3115
3208
  mat-icon-button
3116
3209
  type="button"
3117
- (click)="moveTab(i, -1); $event.stopPropagation()"
3210
+ (click)="moveTab(entry.index, -1); $event.stopPropagation()"
3118
3211
  (keydown.enter)="$event.stopPropagation()"
3119
3212
  (keydown.space)="$event.stopPropagation()"
3120
- [disabled]="i===0"
3213
+ [disabled]="entry.index===0"
3121
3214
  [attr.aria-label]="t('chrome.moveTabLeft', 'Mover aba para esquerda')"
3122
3215
  >
3123
3216
  <mat-icon fontIcon="arrow_back"></mat-icon>
@@ -3125,10 +3218,10 @@ class PraxisTabs {
3125
3218
  <button
3126
3219
  mat-icon-button
3127
3220
  type="button"
3128
- (click)="moveTab(i, 1); $event.stopPropagation()"
3221
+ (click)="moveTab(entry.index, 1); $event.stopPropagation()"
3129
3222
  (keydown.enter)="$event.stopPropagation()"
3130
3223
  (keydown.space)="$event.stopPropagation()"
3131
- [disabled]="i===(config?.tabs?.length||1)-1"
3224
+ [disabled]="entry.index===(config?.tabs?.length||1)-1"
3132
3225
  [attr.aria-label]="t('chrome.moveTabRight', 'Mover aba para direita')"
3133
3226
  >
3134
3227
  <mat-icon fontIcon="arrow_forward"></mat-icon>
@@ -3137,20 +3230,20 @@ class PraxisTabs {
3137
3230
  </ng-template>
3138
3231
 
3139
3232
  <ng-template matTabContent>
3140
- <ng-container *ngIf="(tab.content?.length || tab.widgets?.length) && groupContentReady(i); else emptyTab">
3141
- <ng-container *ngIf="tab.content && form">
3233
+ <ng-container *ngIf="(entry.tab.content?.length || entry.tab.widgets?.length) && groupContentReady(entry.index); else emptyTab">
3234
+ <ng-container *ngIf="entry.tab.content && form">
3142
3235
  <ng-container
3143
3236
  dynamicFieldLoader
3144
- [fields]="tab.content || []"
3237
+ [fields]="entry.tab.content || []"
3145
3238
  [formGroup]="form!"
3146
3239
  ></ng-container>
3147
3240
  </ng-container>
3148
- <ng-container *ngIf="tab.widgets?.length">
3149
- <ng-container *ngFor="let w of tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3241
+ <ng-container *ngIf="entry.tab.widgets?.length">
3242
+ <ng-container *ngFor="let w of entry.tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3150
3243
  <ng-container
3151
3244
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3152
3245
  [context]="context || {}"
3153
- (widgetEvent)="emitWidgetEvent(tabEventPath(tab.id, i), $event)"
3246
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
3154
3247
  ></ng-container>
3155
3248
  </ng-container>
3156
3249
  </ng-container>
@@ -3191,7 +3284,7 @@ class PraxisTabs {
3191
3284
  <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
3192
3285
  </button>
3193
3286
  </div>
3194
- `, isInline: true, styles: [".praxis-tabs-root{position:relative;display:block}.praxis-tabs-group.align-start .mat-mdc-tab-header{justify-content:flex-start}.praxis-tabs-group.align-center .mat-mdc-tab-header{justify-content:center}.praxis-tabs-group.align-end .mat-mdc-tab-header{justify-content:flex-end}.density-compact .mat-mdc-tab-body-content{padding:8px}.density-comfortable .mat-mdc-tab-body-content{padding:16px}.density-spacious .mat-mdc-tab-body-content{padding:24px}.tabs-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.edit-fab-secondary{right:56px}.tab-empty{padding:16px;color:var(--md-sys-color-on-surface-variant);font-style:italic}.high-contrast{filter:contrast(1.2)}.reduce-motion{--mat-animation-duration: 0ms}.drag-handle{display:inline-flex;align-items:center;vertical-align:middle;margin-right:4px;cursor:grab}:host-context(.pdx-gridster-item) .praxis-tabs-root{display:flex;flex-direction:column;height:100%;min-height:0}:host-context(.pdx-gridster-item) .praxis-tabs-group,:host-context(.pdx-gridster-item) .mat-mdc-tab-group{flex:1 1 auto;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-wrapper,:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{height:100%;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{overflow:auto}:host-context(.pdx-gridster-item) .praxis-tabnav-content{flex:1 1 auto;min-height:0;overflow:auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i3$1.MatTabContent, selector: "[matTabContent]" }, { kind: "directive", type: i3$1.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { 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: "component", type: i3$1.MatTabNav, selector: "[mat-tab-nav-bar]", inputs: ["fitInkBarToContent", "mat-stretch-tabs", "animationDuration", "backgroundColor", "disableRipple", "color", "tabPanel"], exportAs: ["matTabNavBar", "matTabNav"] }, { kind: "component", type: i3$1.MatTabNavPanel, selector: "mat-tab-nav-panel", inputs: ["id"], exportAs: ["matTabNavPanel"] }, { kind: "component", type: i3$1.MatTabLink, selector: "[mat-tab-link], [matTabLink]", inputs: ["active", "disabled", "disableRipple", "tabIndex", "id"], exportAs: ["matTabLink"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i7.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6$1.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6$1.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i10.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i10.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i10.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone"] }, { kind: "directive", type: DynamicFieldLoaderDirective, selector: "[dynamicFieldLoader]", inputs: ["fields", "formGroup", "enableExternalControlBinding", "itemTemplate", "debugTrace", "debugTraceLabel", "readonlyMode", "disabledMode", "presentationMode", "visible", "canvasMode"], outputs: ["componentsCreated", "fieldCreated", "fieldDestroyed", "renderError", "canvasMouseEnter", "canvasMouseLeave", "canvasClick"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "ownerWidgetKey", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent", "widgetDiagnostic"], exportAs: ["dynamicWidgetLoader"] }, { kind: "component", type: PraxisAiAssistantComponent, selector: "praxis-ai-assistant", inputs: ["adapter", "riskPolicy", "allowManualPatchEdit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3287
+ `, isInline: true, styles: [".praxis-tabs-root{position:relative;display:block}.praxis-tabs-group.align-start .mat-mdc-tab-header{justify-content:flex-start}.praxis-tabs-group.align-center .mat-mdc-tab-header{justify-content:center}.praxis-tabs-group.align-end .mat-mdc-tab-header{justify-content:flex-end}.density-compact .mat-mdc-tab-body-content{padding:8px}.density-comfortable .mat-mdc-tab-body-content{padding:16px}.density-spacious .mat-mdc-tab-body-content{padding:24px}.tabs-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.edit-fab-secondary{right:56px}.tab-empty{padding:16px;color:var(--md-sys-color-on-surface-variant);font-style:italic}.high-contrast{filter:contrast(1.2)}.reduce-motion{--mat-animation-duration: 0ms}.drag-handle{display:inline-flex;align-items:center;vertical-align:middle;margin-right:4px;cursor:grab}.tab-label-icon{font-size:18px;width:18px;height:18px;margin-right:6px;vertical-align:middle}:host-context(.pdx-gridster-item) .praxis-tabs-root{display:flex;flex-direction:column;height:100%;min-height:0}:host-context(.pdx-gridster-item) .praxis-tabs-group,:host-context(.pdx-gridster-item) .mat-mdc-tab-group{flex:1 1 auto;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-wrapper,:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{height:100%;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{overflow:auto}:host-context(.pdx-gridster-item) .praxis-tabnav-content{flex:1 1 auto;min-height:0;overflow:auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i3$1.MatTabContent, selector: "[matTabContent]" }, { kind: "directive", type: i3$1.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { 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: "component", type: i3$1.MatTabNav, selector: "[mat-tab-nav-bar]", inputs: ["fitInkBarToContent", "mat-stretch-tabs", "animationDuration", "backgroundColor", "disableRipple", "color", "tabPanel"], exportAs: ["matTabNavBar", "matTabNav"] }, { kind: "component", type: i3$1.MatTabNavPanel, selector: "mat-tab-nav-panel", inputs: ["id"], exportAs: ["matTabNavPanel"] }, { kind: "component", type: i3$1.MatTabLink, selector: "[mat-tab-link], [matTabLink]", inputs: ["active", "disabled", "disableRipple", "tabIndex", "id"], exportAs: ["matTabLink"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i7.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6$1.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6$1.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i10.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i10.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i10.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone"] }, { kind: "directive", type: DynamicFieldLoaderDirective, selector: "[dynamicFieldLoader]", inputs: ["fields", "formGroup", "enableExternalControlBinding", "itemTemplate", "debugTrace", "debugTraceLabel", "readonlyMode", "disabledMode", "presentationMode", "visible", "canvasMode"], outputs: ["componentsCreated", "fieldCreated", "fieldDestroyed", "renderError", "canvasMouseEnter", "canvasMouseLeave", "canvasClick"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "ownerWidgetKey", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent", "widgetDiagnostic"], exportAs: ["dynamicWidgetLoader"] }, { kind: "component", type: PraxisAiAssistantComponent, selector: "praxis-ai-assistant", inputs: ["adapter", "riskPolicy", "allowManualPatchEdit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3195
3288
  }
3196
3289
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabs, decorators: [{
3197
3290
  type: Component,
@@ -3251,13 +3344,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3251
3344
  cdkDropList
3252
3345
  cdkDropListOrientation="horizontal"
3253
3346
  [cdkDropListDisabled]="!config?.behavior?.reorderable"
3254
- (cdkDropListDropped)="onNavDrop($event)"
3347
+ (cdkDropListDropped)="onVisibleNavDrop($event)"
3255
3348
  [disablePagination]="config?.nav?.disablePagination"
3256
3349
  [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
3257
3350
  [mat-stretch-tabs]="config?.nav?.stretchTabs"
3258
3351
  [color]="config?.nav?.color"
3259
3352
  [backgroundColor]="config?.nav?.backgroundColor"
3260
- [selectedIndex]="currentNavIndex()"
3353
+ [selectedIndex]="selectedVisibleNavIndex()"
3261
3354
  [attr.aria-label]="config?.nav?.ariaLabel || config?.group?.ariaLabel || null"
3262
3355
  [attr.aria-labelledby]="config?.nav?.ariaLabelledby || config?.group?.ariaLabelledby || null"
3263
3356
  [animationDuration]="effectiveAnimationDuration()"
@@ -3266,21 +3359,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3266
3359
  >
3267
3360
  <a
3268
3361
  mat-tab-link
3269
- *ngFor="let link of config?.nav?.links; let i = index; trackBy: trackNavLink"
3362
+ *ngFor="let entry of visibleNavLinkEntries(); let i = index; trackBy: trackVisibleNavLink"
3270
3363
  cdkDrag
3271
3364
  [cdkDragDisabled]="!config?.behavior?.reorderable"
3272
3365
  cdkDragLockAxis="x"
3273
- [active]="getNavActive(i)"
3274
- [disabled]="link.disabled"
3275
- [disableRipple]="config?.nav?.disableRipple || link.disableRipple"
3276
- [fitInkBarToContent]="link.fitInkBarToContent || false"
3277
- [id]="link.id || ''"
3278
- (click)="onNavClick(i)"
3366
+ [active]="getNavActive(entry.index)"
3367
+ [disabled]="entry.link.disabled"
3368
+ [disableRipple]="config?.nav?.disableRipple || entry.link.disableRipple"
3369
+ [fitInkBarToContent]="entry.link.fitInkBarToContent || false"
3370
+ [id]="entry.link.id || ''"
3371
+ (click)="onNavClick(entry.index)"
3279
3372
  >
3280
3373
  <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
3281
3374
  <mat-icon fontIcon="drag_indicator"></mat-icon>
3282
3375
  </span>
3283
- {{ link.label }}
3376
+ <mat-icon *ngIf="entry.link.icon" class="tab-label-icon" [praxisIcon]="entry.link.icon"></mat-icon>
3377
+ {{ entry.link.label }}
3284
3378
  </a>
3285
3379
  </nav>
3286
3380
 
@@ -3330,7 +3424,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3330
3424
  [fitInkBarToContent]="config?.group?.fitInkBarToContent"
3331
3425
  [headerPosition]="config?.group?.headerPosition ?? 'above'"
3332
3426
  [preserveContent]="config?.group?.preserveContent"
3333
- [selectedIndex]="selectedIndexSignal()"
3427
+ [selectedIndex]="selectedVisibleTabIndex()"
3334
3428
  [mat-stretch-tabs]="config?.group?.stretchTabs"
3335
3429
  [mat-align-tabs]="config?.group?.alignTabs || 'start'"
3336
3430
  [color]="config?.group?.color"
@@ -3340,26 +3434,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3340
3434
  [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
3341
3435
  (animationDone)="animationDone.emit()"
3342
3436
  (focusChange)="focusChange.emit($event)"
3343
- (selectedIndexChange)="onSelectedIndexChange($event)"
3437
+ (selectedIndexChange)="onVisibleTabIndexChange($event)"
3344
3438
  (selectedTabChange)="selectedTabChange.emit($event)"
3345
3439
  class="praxis-tabs-group"
3346
3440
  >
3347
3441
  <mat-tab
3348
- *ngFor="let tab of config?.tabs; let i = index; trackBy: trackTab"
3349
- [disabled]="tab.disabled"
3350
- [labelClass]="tab.labelClass ?? ''"
3351
- [bodyClass]="tab.bodyClass ?? ''"
3352
- [id]="tab.id || ''"
3353
- [attr.aria-label]="tab.ariaLabel || null"
3354
- [attr.aria-labelledby]="tab.ariaLabelledby || null"
3442
+ *ngFor="let entry of visibleTabEntries(); let i = index; trackBy: trackVisibleTab"
3443
+ [disabled]="entry.tab.disabled"
3444
+ [labelClass]="entry.tab.labelClass ?? ''"
3445
+ [bodyClass]="entry.tab.bodyClass ?? ''"
3446
+ [id]="entry.tab.id || ''"
3447
+ [attr.aria-label]="entry.tab.ariaLabel || null"
3448
+ [attr.aria-labelledby]="entry.tab.ariaLabelledby || null"
3355
3449
  >
3356
3450
  <ng-template mat-tab-label>
3357
- <span>{{ tab.textLabel }}</span>
3451
+ <mat-icon *ngIf="entry.tab.icon" class="tab-label-icon" [praxisIcon]="entry.tab.icon"></mat-icon>
3452
+ <span>{{ entry.tab.textLabel }}</span>
3358
3453
  <button
3359
3454
  *ngIf="config?.behavior?.closeable"
3360
3455
  mat-icon-button
3361
3456
  type="button"
3362
- (click)="closeTab(i); $event.stopPropagation()"
3457
+ (click)="closeTab(entry.index); $event.stopPropagation()"
3363
3458
  (keydown.enter)="$event.stopPropagation()"
3364
3459
  (keydown.space)="$event.stopPropagation()"
3365
3460
  [attr.aria-label]="t('chrome.closeTab', 'Fechar aba')"
@@ -3370,10 +3465,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3370
3465
  <button
3371
3466
  mat-icon-button
3372
3467
  type="button"
3373
- (click)="moveTab(i, -1); $event.stopPropagation()"
3468
+ (click)="moveTab(entry.index, -1); $event.stopPropagation()"
3374
3469
  (keydown.enter)="$event.stopPropagation()"
3375
3470
  (keydown.space)="$event.stopPropagation()"
3376
- [disabled]="i===0"
3471
+ [disabled]="entry.index===0"
3377
3472
  [attr.aria-label]="t('chrome.moveTabLeft', 'Mover aba para esquerda')"
3378
3473
  >
3379
3474
  <mat-icon fontIcon="arrow_back"></mat-icon>
@@ -3381,10 +3476,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3381
3476
  <button
3382
3477
  mat-icon-button
3383
3478
  type="button"
3384
- (click)="moveTab(i, 1); $event.stopPropagation()"
3479
+ (click)="moveTab(entry.index, 1); $event.stopPropagation()"
3385
3480
  (keydown.enter)="$event.stopPropagation()"
3386
3481
  (keydown.space)="$event.stopPropagation()"
3387
- [disabled]="i===(config?.tabs?.length||1)-1"
3482
+ [disabled]="entry.index===(config?.tabs?.length||1)-1"
3388
3483
  [attr.aria-label]="t('chrome.moveTabRight', 'Mover aba para direita')"
3389
3484
  >
3390
3485
  <mat-icon fontIcon="arrow_forward"></mat-icon>
@@ -3393,20 +3488,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3393
3488
  </ng-template>
3394
3489
 
3395
3490
  <ng-template matTabContent>
3396
- <ng-container *ngIf="(tab.content?.length || tab.widgets?.length) && groupContentReady(i); else emptyTab">
3397
- <ng-container *ngIf="tab.content && form">
3491
+ <ng-container *ngIf="(entry.tab.content?.length || entry.tab.widgets?.length) && groupContentReady(entry.index); else emptyTab">
3492
+ <ng-container *ngIf="entry.tab.content && form">
3398
3493
  <ng-container
3399
3494
  dynamicFieldLoader
3400
- [fields]="tab.content || []"
3495
+ [fields]="entry.tab.content || []"
3401
3496
  [formGroup]="form!"
3402
3497
  ></ng-container>
3403
3498
  </ng-container>
3404
- <ng-container *ngIf="tab.widgets?.length">
3405
- <ng-container *ngFor="let w of tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3499
+ <ng-container *ngIf="entry.tab.widgets?.length">
3500
+ <ng-container *ngFor="let w of entry.tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3406
3501
  <ng-container
3407
3502
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3408
3503
  [context]="context || {}"
3409
- (widgetEvent)="emitWidgetEvent(tabEventPath(tab.id, i), $event)"
3504
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
3410
3505
  ></ng-container>
3411
3506
  </ng-container>
3412
3507
  </ng-container>
@@ -3447,7 +3542,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3447
3542
  <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
3448
3543
  </button>
3449
3544
  </div>
3450
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".praxis-tabs-root{position:relative;display:block}.praxis-tabs-group.align-start .mat-mdc-tab-header{justify-content:flex-start}.praxis-tabs-group.align-center .mat-mdc-tab-header{justify-content:center}.praxis-tabs-group.align-end .mat-mdc-tab-header{justify-content:flex-end}.density-compact .mat-mdc-tab-body-content{padding:8px}.density-comfortable .mat-mdc-tab-body-content{padding:16px}.density-spacious .mat-mdc-tab-body-content{padding:24px}.tabs-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.edit-fab-secondary{right:56px}.tab-empty{padding:16px;color:var(--md-sys-color-on-surface-variant);font-style:italic}.high-contrast{filter:contrast(1.2)}.reduce-motion{--mat-animation-duration: 0ms}.drag-handle{display:inline-flex;align-items:center;vertical-align:middle;margin-right:4px;cursor:grab}:host-context(.pdx-gridster-item) .praxis-tabs-root{display:flex;flex-direction:column;height:100%;min-height:0}:host-context(.pdx-gridster-item) .praxis-tabs-group,:host-context(.pdx-gridster-item) .mat-mdc-tab-group{flex:1 1 auto;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-wrapper,:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{height:100%;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{overflow:auto}:host-context(.pdx-gridster-item) .praxis-tabnav-content{flex:1 1 auto;min-height:0;overflow:auto}\n"] }]
3545
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".praxis-tabs-root{position:relative;display:block}.praxis-tabs-group.align-start .mat-mdc-tab-header{justify-content:flex-start}.praxis-tabs-group.align-center .mat-mdc-tab-header{justify-content:center}.praxis-tabs-group.align-end .mat-mdc-tab-header{justify-content:flex-end}.density-compact .mat-mdc-tab-body-content{padding:8px}.density-comfortable .mat-mdc-tab-body-content{padding:16px}.density-spacious .mat-mdc-tab-body-content{padding:24px}.tabs-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.edit-fab-secondary{right:56px}.tab-empty{padding:16px;color:var(--md-sys-color-on-surface-variant);font-style:italic}.high-contrast{filter:contrast(1.2)}.reduce-motion{--mat-animation-duration: 0ms}.drag-handle{display:inline-flex;align-items:center;vertical-align:middle;margin-right:4px;cursor:grab}.tab-label-icon{font-size:18px;width:18px;height:18px;margin-right:6px;vertical-align:middle}:host-context(.pdx-gridster-item) .praxis-tabs-root{display:flex;flex-direction:column;height:100%;min-height:0}:host-context(.pdx-gridster-item) .praxis-tabs-group,:host-context(.pdx-gridster-item) .mat-mdc-tab-group{flex:1 1 auto;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-wrapper,:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{height:100%;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{overflow:auto}:host-context(.pdx-gridster-item) .praxis-tabnav-content{flex:1 1 auto;min-height:0;overflow:auto}\n"] }]
3451
3546
  }], propDecorators: { config: [{
3452
3547
  type: Input
3453
3548
  }], tabsId: [{
@@ -3455,6 +3550,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3455
3550
  args: [{ required: true }]
3456
3551
  }], componentInstanceId: [{
3457
3552
  type: Input
3553
+ }], selectedIndex: [{
3554
+ type: Input
3458
3555
  }], enableCustomization: [{
3459
3556
  type: Input
3460
3557
  }], form: [{
@@ -3504,6 +3601,19 @@ const PRAXIS_TABS_PORTS = [
3504
3601
  description: 'Fragmento canonico de configuracao das tabs/nav e dos widgets internos.',
3505
3602
  exposure: { public: true, group: 'config' },
3506
3603
  },
3604
+ {
3605
+ id: 'selectedIndex',
3606
+ label: 'Indice selecionado',
3607
+ direction: 'input',
3608
+ semanticKind: 'value',
3609
+ schema: {
3610
+ id: 'number',
3611
+ kind: 'ts-type',
3612
+ ref: 'number',
3613
+ },
3614
+ description: 'Indice ativo de abas/nav para controle externo por composicao.',
3615
+ exposure: { public: true, group: 'state' },
3616
+ },
3507
3617
  {
3508
3618
  id: 'selectedIndexChange',
3509
3619
  label: 'Troca de indice selecionado',
@@ -3554,6 +3664,12 @@ const PRAXIS_TABS_COMPONENT_METADATA = {
3554
3664
  label: 'ID da instância',
3555
3665
  description: 'Identificador opcional para múltiplas instâncias na mesma rota',
3556
3666
  },
3667
+ {
3668
+ name: 'selectedIndex',
3669
+ type: 'number',
3670
+ label: 'Indice selecionado',
3671
+ description: 'Indice ativo de abas/nav controlavel externamente pela composicao',
3672
+ },
3557
3673
  {
3558
3674
  name: 'enableCustomization',
3559
3675
  type: 'boolean',
@@ -3718,11 +3834,11 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3718
3834
  editableTargets: [
3719
3835
  { kind: 'tab', resolver: 'tab-by-id-or-label', description: 'A group-mode tab in config.tabs[].' },
3720
3836
  { kind: 'tabLabel', resolver: 'tab-by-id-or-label', description: 'The text label of a group-mode tab.' },
3721
- { kind: 'tabIcon', resolver: 'tab-by-id-or-label', description: 'Icon metadata for a tab label when supported by the authoring document.' },
3722
- { kind: 'tabContent', resolver: 'tab-content-by-id', description: 'Dynamic fields or widgets hosted by a tab or nav link.' },
3837
+ { kind: 'tabIcon', resolver: 'tab-by-id-or-label', description: 'Icon metadata rendered in a group tab label.' },
3838
+ { kind: 'tabContent', resolver: 'tab-or-link-by-id', description: 'Dynamic fields or widgets hosted by a tab or nav link.' },
3723
3839
  { kind: 'activeTab', resolver: 'tab-index-or-id', description: 'Selected tab or nav link index.' },
3724
- { kind: 'visibility', resolver: 'tab-by-id-or-label', description: 'Authoring visibility flag used by tools before runtime projection.' },
3725
- { kind: 'disabledState', resolver: 'tab-by-id-or-label', description: 'Disabled state of a tab or nav link.' },
3840
+ { kind: 'visibility', resolver: 'tab-or-link-by-id', description: 'Runtime visibility flag for a group tab or nav link.' },
3841
+ { kind: 'disabledState', resolver: 'tab-or-link-by-id', description: 'Disabled state of a tab or nav link.' },
3726
3842
  { kind: 'layout', resolver: 'tabs-layout-config', description: 'Group/nav mode, header position, density, stretch and behavior settings.' },
3727
3843
  ],
3728
3844
  operations: [
@@ -3735,8 +3851,10 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3735
3851
  inputSchema: tabItemSchema,
3736
3852
  effects: [{ kind: 'append-unique', path: 'tabs[]', key: 'id' }],
3737
3853
  validators: ['tab-id-unique', 'tabs-mode-compatible', 'tab-content-valid'],
3738
- affectedPaths: ['tabs[]', 'group.selectedIndex'],
3739
- submissionImpact: false,
3854
+ destructive: false,
3855
+ requiresConfirmation: false,
3856
+ affectedPaths: ['tabs[]'],
3857
+ submissionImpact: 'config-only',
3740
3858
  preconditions: ['config-initialized'],
3741
3859
  },
3742
3860
  {
@@ -3751,12 +3869,28 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3751
3869
  replacementActiveTabId: { type: 'string' },
3752
3870
  },
3753
3871
  },
3754
- effects: [{ kind: 'remove-by-key', path: 'tabs[]', key: 'id' }],
3872
+ effects: [
3873
+ {
3874
+ kind: 'compile-domain-patch',
3875
+ handler: 'tabs.remove-tab-and-reselect',
3876
+ handlerContract: {
3877
+ reads: ['tabs[]', 'group.selectedIndex'],
3878
+ writes: ['tabs[]', 'group.selectedIndex'],
3879
+ identityKeys: ['tabs[].id'],
3880
+ inputSchema: {
3881
+ type: 'object',
3882
+ properties: { replacementActiveTabId: { type: 'string' } },
3883
+ },
3884
+ failureModes: ['target-tab-missing', 'replacement-tab-missing', 'confirmation-missing'],
3885
+ description: 'Removes the target tab by stable id and reselects a safe replacement when the active/default tab is removed.',
3886
+ },
3887
+ },
3888
+ ],
3755
3889
  destructive: true,
3756
3890
  requiresConfirmation: true,
3757
3891
  validators: ['tab-exists', 'active-tab-removal-safe', 'tab-content-removal-confirmed'],
3758
3892
  affectedPaths: ['tabs[]', 'group.selectedIndex'],
3759
- submissionImpact: false,
3893
+ submissionImpact: 'config-only',
3760
3894
  preconditions: ['config-initialized', 'target-tab-exists', 'confirmation-collected'],
3761
3895
  },
3762
3896
  {
@@ -3767,9 +3901,11 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3767
3901
  target: { kind: 'tabLabel', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3768
3902
  inputSchema: { type: 'object', required: ['textLabel'], properties: { textLabel: { type: 'string' } } },
3769
3903
  effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
3904
+ destructive: false,
3905
+ requiresConfirmation: false,
3770
3906
  validators: ['tab-exists', 'tab-label-valid'],
3771
3907
  affectedPaths: ['tabs[].textLabel'],
3772
- submissionImpact: false,
3908
+ submissionImpact: 'config-only',
3773
3909
  preconditions: ['config-initialized', 'target-tab-exists'],
3774
3910
  },
3775
3911
  {
@@ -3780,9 +3916,11 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3780
3916
  target: { kind: 'tabIcon', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3781
3917
  inputSchema: { type: 'object', required: ['icon'], properties: { icon: { type: 'string' } } },
3782
3918
  effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
3919
+ destructive: false,
3920
+ requiresConfirmation: false,
3783
3921
  validators: ['tab-exists', 'tab-icon-valid'],
3784
3922
  affectedPaths: ['tabs[].icon'],
3785
- submissionImpact: false,
3923
+ submissionImpact: 'visual-only',
3786
3924
  preconditions: ['config-initialized', 'target-tab-exists'],
3787
3925
  },
3788
3926
  {
@@ -3792,10 +3930,25 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3792
3930
  targetKind: 'tab',
3793
3931
  target: { kind: 'tab', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3794
3932
  inputSchema: { type: 'object', required: ['beforeTabId'], properties: { beforeTabId: { type: 'string' } } },
3795
- effects: [{ kind: 'reorder-by-key', path: 'tabs[]', key: 'id' }],
3933
+ effects: [
3934
+ {
3935
+ kind: 'compile-domain-patch',
3936
+ handler: 'tabs.reorder-tab-and-preserve-selection',
3937
+ handlerContract: {
3938
+ reads: ['tabs[]', 'group.selectedIndex'],
3939
+ writes: ['tabs[]', 'group.selectedIndex'],
3940
+ identityKeys: ['tabs[].id'],
3941
+ inputSchema: { type: 'object', required: ['beforeTabId'], properties: { beforeTabId: { type: 'string' } } },
3942
+ failureModes: ['target-tab-missing', 'before-tab-missing', 'unstable-tab-id'],
3943
+ description: 'Reorders tabs by stable id and remaps group.selectedIndex when the selected tab crosses positions.',
3944
+ },
3945
+ },
3946
+ ],
3947
+ destructive: false,
3948
+ requiresConfirmation: false,
3796
3949
  validators: ['tab-exists', 'tab-order-deterministic'],
3797
3950
  affectedPaths: ['tabs[]', 'group.selectedIndex'],
3798
- submissionImpact: false,
3951
+ submissionImpact: 'config-only',
3799
3952
  preconditions: ['config-initialized', 'target-tab-exists'],
3800
3953
  },
3801
3954
  {
@@ -3805,10 +3958,25 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3805
3958
  targetKind: 'disabledState',
3806
3959
  target: { kind: 'disabledState', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
3807
3960
  inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
3808
- effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
3961
+ effects: [
3962
+ {
3963
+ kind: 'compile-domain-patch',
3964
+ handler: 'tabs.set-tab-or-link-disabled',
3965
+ handlerContract: {
3966
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
3967
+ writes: ['tabs[].disabled', 'nav.links[].disabled'],
3968
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
3969
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
3970
+ failureModes: ['target-tab-or-link-missing', 'ambiguous-target', 'active-item-disabled-without-reselection'],
3971
+ description: 'Sets disabled on the resolved group tab or nav link without guessing between modes.',
3972
+ },
3973
+ },
3974
+ ],
3975
+ destructive: false,
3976
+ requiresConfirmation: false,
3809
3977
  validators: ['tab-or-link-exists', 'active-tab-disabled-safe'],
3810
3978
  affectedPaths: ['tabs[].disabled', 'nav.links[].disabled'],
3811
- submissionImpact: false,
3979
+ submissionImpact: 'config-only',
3812
3980
  preconditions: ['config-initialized', 'target-tab-or-link-exists'],
3813
3981
  },
3814
3982
  {
@@ -3818,10 +3986,25 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3818
3986
  targetKind: 'visibility',
3819
3987
  target: { kind: 'visibility', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
3820
3988
  inputSchema: { type: 'object', required: ['visible'], properties: { visible: { type: 'boolean' } } },
3821
- effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
3989
+ effects: [
3990
+ {
3991
+ kind: 'compile-domain-patch',
3992
+ handler: 'tabs.set-tab-or-link-visible',
3993
+ handlerContract: {
3994
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
3995
+ writes: ['tabs[].visible', 'nav.links[].visible'],
3996
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
3997
+ inputSchema: { type: 'object', required: ['visible'], properties: { visible: { type: 'boolean' } } },
3998
+ failureModes: ['target-tab-or-link-missing', 'ambiguous-target', 'active-item-hidden-without-reselection'],
3999
+ description: 'Sets visible on the resolved group tab or nav link and preserves deterministic visible-index mapping.',
4000
+ },
4001
+ },
4002
+ ],
4003
+ destructive: false,
4004
+ requiresConfirmation: false,
3822
4005
  validators: ['tab-or-link-exists', 'active-tab-visibility-safe'],
3823
4006
  affectedPaths: ['tabs[].visible', 'nav.links[].visible'],
3824
- submissionImpact: false,
4007
+ submissionImpact: 'config-only',
3825
4008
  preconditions: ['config-initialized', 'target-tab-or-link-exists'],
3826
4009
  },
3827
4010
  {
@@ -3831,10 +4014,25 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3831
4014
  targetKind: 'activeTab',
3832
4015
  target: { kind: 'activeTab', resolver: 'tab-index-or-id', ambiguityPolicy: 'fail', required: true },
3833
4016
  inputSchema: { type: 'object', required: ['selectedIndex'], properties: { selectedIndex: { type: 'number' }, tabId: { type: 'string' } } },
3834
- effects: [{ kind: 'set-value', path: 'group.selectedIndex' }],
4017
+ effects: [
4018
+ {
4019
+ kind: 'compile-domain-patch',
4020
+ handler: 'tabs.set-active-item',
4021
+ handlerContract: {
4022
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
4023
+ writes: ['group.selectedIndex', 'nav.selectedIndex'],
4024
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
4025
+ inputSchema: { type: 'object', required: ['selectedIndex'], properties: { selectedIndex: { type: 'number' }, tabId: { type: 'string' } } },
4026
+ failureModes: ['target-tab-or-link-missing', 'selected-index-out-of-range', 'hidden-or-disabled-target'],
4027
+ description: 'Sets the active index for the current primary mode using either selectedIndex or a resolved tab/link id.',
4028
+ },
4029
+ },
4030
+ ],
4031
+ destructive: false,
4032
+ requiresConfirmation: false,
3835
4033
  validators: ['active-tab-exists', 'selected-index-in-range'],
3836
4034
  affectedPaths: ['group.selectedIndex', 'nav.selectedIndex'],
3837
- submissionImpact: false,
4035
+ submissionImpact: 'config-only',
3838
4036
  preconditions: ['config-initialized', 'target-tab-or-link-exists'],
3839
4037
  },
3840
4038
  {
@@ -3856,9 +4054,11 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3856
4054
  },
3857
4055
  },
3858
4056
  effects: [{ kind: 'merge-object', path: 'appearance' }, { kind: 'merge-object', path: 'group' }, { kind: 'merge-object', path: 'nav' }, { kind: 'merge-object', path: 'behavior' }],
4057
+ destructive: false,
4058
+ requiresConfirmation: false,
3859
4059
  validators: ['tabs-mode-compatible', 'layout-values-valid', 'editor-runtime-round-trip'],
3860
4060
  affectedPaths: ['appearance.density', 'group.headerPosition', 'group.alignTabs', 'group.stretchTabs', 'nav.stretchTabs', 'behavior.lazyLoad'],
3861
- submissionImpact: false,
4061
+ submissionImpact: 'config-only',
3862
4062
  preconditions: ['config-initialized'],
3863
4063
  },
3864
4064
  {
@@ -3868,10 +4068,25 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3868
4068
  targetKind: 'tabContent',
3869
4069
  target: { kind: 'tabContent', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
3870
4070
  inputSchema: tabPatchSchema,
3871
- effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
4071
+ effects: [
4072
+ {
4073
+ kind: 'compile-domain-patch',
4074
+ handler: 'tabs.set-tab-or-link-content',
4075
+ handlerContract: {
4076
+ reads: ['tabs[]', 'nav.links[]'],
4077
+ writes: ['tabs[].content', 'tabs[].widgets', 'nav.links[].content', 'nav.links[].widgets'],
4078
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
4079
+ inputSchema: tabPatchSchema,
4080
+ failureModes: ['target-tab-or-link-missing', 'invalid-dynamic-field-content', 'invalid-widget-definition'],
4081
+ description: 'Updates content/widgets only on the resolved group tab or nav link while preserving nested widget identity.',
4082
+ },
4083
+ },
4084
+ ],
4085
+ destructive: false,
4086
+ requiresConfirmation: false,
3872
4087
  validators: ['tab-or-link-exists', 'tab-content-valid', 'widget-event-delegated'],
3873
4088
  affectedPaths: ['tabs[].content', 'tabs[].widgets', 'nav.links[].content', 'nav.links[].widgets'],
3874
- submissionImpact: false,
4089
+ submissionImpact: 'config-only',
3875
4090
  preconditions: ['config-initialized', 'target-tab-or-link-exists'],
3876
4091
  },
3877
4092
  ],
package/index.d.ts CHANGED
@@ -81,9 +81,11 @@ interface TabMetadata {
81
81
  ariaLabel?: string;
82
82
  ariaLabelledby?: string;
83
83
  textLabel?: string;
84
+ icon?: string;
84
85
  labelClass?: string | string[];
85
86
  bodyClass?: string | string[];
86
87
  disabled?: boolean;
88
+ visible?: boolean;
87
89
  content?: any[];
88
90
  widgets?: WidgetDefinition[];
89
91
  isActive?: boolean;
@@ -106,8 +108,10 @@ interface TabNavMetadata {
106
108
  interface TabLinkMetadata {
107
109
  id?: string;
108
110
  label: string;
111
+ icon?: string;
109
112
  active?: boolean;
110
113
  disabled?: boolean;
114
+ visible?: boolean;
111
115
  disableRipple?: boolean;
112
116
  fitInkBarToContent?: boolean;
113
117
  content?: any[];
@@ -140,6 +144,7 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
140
144
  config: TabsMetadata | null;
141
145
  tabsId: string;
142
146
  componentInstanceId?: string;
147
+ set selectedIndex(index: number | null | undefined);
143
148
  enableCustomization: boolean;
144
149
  form: FormGroup | null;
145
150
  context: Record<string, any> | null;
@@ -155,6 +160,7 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
155
160
  protected selectedIndexSignal: i0.WritableSignal<number>;
156
161
  private groupLoaded;
157
162
  private navLoaded;
163
+ private controlledSelectedIndex?;
158
164
  private readonly destroy$;
159
165
  private readonly widgetDefinitionCache;
160
166
  ngOnInit(): void;
@@ -163,9 +169,23 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
163
169
  isNavMode(): boolean;
164
170
  effectiveAnimationDuration(): string;
165
171
  getNavActive(i: number): boolean;
172
+ protected visibleNavLinkEntries(): Array<{
173
+ link: TabLinkMetadata;
174
+ index: number;
175
+ }>;
176
+ protected visibleTabEntries(): Array<{
177
+ tab: TabMetadata;
178
+ index: number;
179
+ }>;
180
+ protected selectedVisibleNavIndex(): number;
181
+ protected selectedVisibleTabIndex(): number;
182
+ protected onVisibleTabIndexChange(index: number): void;
166
183
  onNavClick(i: number): void;
167
184
  onNavDrop(event: CdkDragDrop<any>): void;
185
+ protected onVisibleNavDrop(event: CdkDragDrop<any>): void;
168
186
  onSelectedIndexChange(index: number): void;
187
+ private applySelectedIndex;
188
+ private reapplyControlledSelectedIndex;
169
189
  closeTab(index: number): void;
170
190
  moveTab(index: number, delta: number): void;
171
191
  openEditor(): void;
@@ -187,6 +207,14 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
187
207
  protected groupContentReady(index: number): boolean;
188
208
  protected navContentReady(index: number): boolean;
189
209
  protected isEmptyGlobal(): boolean;
210
+ protected trackVisibleNavLink(index: number, entry: {
211
+ link: TabLinkMetadata;
212
+ index: number;
213
+ }): string;
214
+ protected trackVisibleTab(index: number, entry: {
215
+ tab: TabMetadata;
216
+ index: number;
217
+ }): string;
190
218
  protected trackNavLink(index: number, link: TabLinkMetadata): string;
191
219
  protected trackTab(index: number, tab: TabMetadata): string;
192
220
  protected trackWidgetDefinition(index: number, widget: WidgetDefinition): string;
@@ -200,7 +228,7 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
200
228
  protected styleCss(): string | null;
201
229
  private cloneWidgetDefinition;
202
230
  static ɵfac: i0.ɵɵFactoryDeclaration<PraxisTabs, never>;
203
- static ɵcmp: i0.ɵɵComponentDeclaration<PraxisTabs, "praxis-tabs", never, { "config": { "alias": "config"; "required": false; }; "tabsId": { "alias": "tabsId"; "required": true; }; "componentInstanceId": { "alias": "componentInstanceId"; "required": false; }; "enableCustomization": { "alias": "enableCustomization"; "required": false; }; "form": { "alias": "form"; "required": false; }; "context": { "alias": "context"; "required": false; }; }, { "animationDone": "animationDone"; "focusChange": "focusChange"; "selectedIndexChange": "selectedIndexChange"; "selectedTabChange": "selectedTabChange"; "indexFocused": "indexFocused"; "selectFocusedIndex": "selectFocusedIndex"; "widgetEvent": "widgetEvent"; }, never, never, true, never>;
231
+ static ɵcmp: i0.ɵɵComponentDeclaration<PraxisTabs, "praxis-tabs", never, { "config": { "alias": "config"; "required": false; }; "tabsId": { "alias": "tabsId"; "required": true; }; "componentInstanceId": { "alias": "componentInstanceId"; "required": false; }; "selectedIndex": { "alias": "selectedIndex"; "required": false; }; "enableCustomization": { "alias": "enableCustomization"; "required": false; }; "form": { "alias": "form"; "required": false; }; "context": { "alias": "context"; "required": false; }; }, { "animationDone": "animationDone"; "focusChange": "focusChange"; "selectedIndexChange": "selectedIndexChange"; "selectedTabChange": "selectedTabChange"; "indexFocused": "indexFocused"; "selectFocusedIndex": "selectFocusedIndex"; "widgetEvent": "widgetEvent"; }, never, never, true, never>;
204
232
  }
205
233
 
206
234
  declare const PRAXIS_TABS_I18N_NAMESPACE = "praxisTabs";
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@praxisui/tabs",
3
- "version": "8.0.0-beta.11",
3
+ "version": "8.0.0-beta.12",
4
4
  "description": "Configurable tabs (group and nav) for Praxis UI with metadata-driven content and runtime editor.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^20.0.0",
7
7
  "@angular/core": "^20.0.0",
8
8
  "@angular/material": "^20.0.0",
9
9
  "@angular/cdk": "^20.0.0",
10
- "@praxisui/core": "^8.0.0-beta.11",
11
- "@praxisui/dynamic-fields": "^8.0.0-beta.11",
12
- "@praxisui/settings-panel": "^8.0.0-beta.11"
10
+ "@praxisui/core": "^8.0.0-beta.12",
11
+ "@praxisui/dynamic-fields": "^8.0.0-beta.12",
12
+ "@praxisui/settings-panel": "^8.0.0-beta.12"
13
13
  },
14
14
  "dependencies": {
15
15
  "tslib": "^2.3.0",