@praxisui/tabs 8.0.0-beta.1 → 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,13 +75,46 @@ 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
81
82
  - `selectedIndexChange: number` Índice selecionado atualizado (ambos modos).
82
83
  - `selectedTabChange: MatTabChangeEvent` Evento nativo do MatTabGroup.
83
84
  - `focusChange, animationDone, indexFocused, selectFocusedIndex` Eventos nativos do Angular Material.
84
- - `widgetEvent: { tabId?, tabIndex?, linkId?, linkIndex?, sourceId, output?, payload? }` Reemissão de eventos dos widgets internos com contexto da aba/link.
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`.
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
+ ```
85
118
 
86
119
  Persistência
87
120
  - Quando `tabsId` é fornecido, a configuração é salva/recuperada em `AsyncConfigStorage` na chave `tabs:<component_id>`.
@@ -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
 
@@ -146,18 +179,49 @@ Quick Setup
146
179
  - `openQuickSetup()` abre `TabsQuickSetupComponent` para criação rápida de abas/links.
147
180
  - `applied$`/`saved$` aplicam a configuração ao componente.
148
181
 
182
+ ## Agentic Authoring
183
+
184
+ `@praxisui/tabs` publica `PRAXIS_TABS_AUTHORING_MANIFEST` para orientar edições assistidas por IA sobre `TabsMetadata`.
185
+
186
+ - **Editable targets:** `tab`, `tabLabel`, `tabIcon`, `tabContent`, `activeTab`, `visibility`, `disabledState` e `layout`.
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`.
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.
190
+ - **Registry projection:** o manifesto é exportado no `public-api` e projetado em `components['praxis-tabs'].authoringManifest` pelo AI Registry.
191
+
149
192
  ## Eventos e Conexões
150
193
 
151
- `widgetEvent` reemite eventos de widgets internos com contexto da origem, permitindo conexões no Builder/Graph:
194
+ Para conexoes canonicas de widgets internos, use `composition.links` com endpoint `component-port + nestedPath`.
195
+
196
+ Exemplo de output de uma lista dentro da primeira tab:
197
+
198
+ ```json
199
+ {
200
+ "kind": "component-port",
201
+ "ref": {
202
+ "widget": "tabs-widget",
203
+ "nestedPath": [
204
+ { "kind": "tab", "id": "employees-list", "index": 0 },
205
+ { "kind": "widget", "key": "employees-list", "componentType": "praxis-list" }
206
+ ],
207
+ "port": "itemClick",
208
+ "direction": "output"
209
+ }
210
+ }
211
+ ```
212
+
213
+ Regras:
214
+
215
+ - `ref.widget` e a instancia top-level de `praxis-tabs`;
216
+ - `nestedPath` e relativo a essa instancia e deve terminar em `kind: "widget"` com `key` estavel;
217
+ - `ref.port` e a porta real do widget filho;
218
+ - inputs para filhos nested devem atualizar a configuracao declarativa do filho, nao depender de dot-path publico sobre `config`.
219
+
220
+ `widgetEvent` continua existindo como bridge avancada/legado para transporte de eventos internos com contexto da origem:
152
221
 
153
222
  - Forma do evento: `{ tabId?, tabIndex?, linkId?, linkIndex?, sourceId, output?, payload }`.
154
- - De um componente interno para fora:
155
- - From: `{ widget: '<key do tabs>', output: 'widgetEvent' }`
156
- - Map (exemplo tabela interna): `payload.payload.id`
157
- - To: `<widget externo>.<input>`
158
- - De fora para um componente interno (dot-path):
159
- - Grupo: `inputs.config.tabs[<idx>].widgets[<widx>].inputs.<input>`
160
- - Nav: `inputs.config.nav.links[<idx>].widgets[<widx>].inputs.<input>`
223
+ - Nao use `widgetEvent` como caminho principal para authoring novo de nested ports.
224
+ - Nao use dot-path de `config.tabs[].widgets[]` como contrato publico para novos links nested.
161
225
 
162
226
  ## Lazy Load
163
227
 
@@ -167,7 +231,7 @@ Quick Setup
167
231
  ## Exemplo Mínimo
168
232
 
169
233
  ```html
170
- <praxis-tabs [config]="tabsCfg" [tabsId]="'cliente-tabs'" [enableCustomization]="true" (widgetEvent)="onWidgetEvent($event)"></praxis-tabs>
234
+ <praxis-tabs [config]="tabsCfg" [tabsId]="'cliente-tabs'" [enableCustomization]="true"></praxis-tabs>
171
235
  ```
172
236
 
173
237
  ```ts
@@ -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',
@@ -3675,6 +3791,341 @@ function providePraxisTabsMetadata() {
3675
3791
  };
3676
3792
  }
3677
3793
 
3794
+ const tabItemSchema = {
3795
+ type: 'object',
3796
+ required: ['id', 'textLabel'],
3797
+ properties: {
3798
+ id: { type: 'string' },
3799
+ textLabel: { type: 'string' },
3800
+ icon: { type: 'string' },
3801
+ disabled: { type: 'boolean' },
3802
+ visible: { type: 'boolean', default: true },
3803
+ content: { type: 'array', items: { type: 'object' } },
3804
+ widgets: { type: 'array', items: { type: 'object' } },
3805
+ },
3806
+ };
3807
+ const tabPatchSchema = {
3808
+ type: 'object',
3809
+ minProperties: 1,
3810
+ properties: {
3811
+ id: { type: 'string' },
3812
+ textLabel: { type: 'string' },
3813
+ icon: { type: 'string' },
3814
+ disabled: { type: 'boolean' },
3815
+ visible: { type: 'boolean' },
3816
+ content: { type: 'array', items: { type: 'object' } },
3817
+ widgets: { type: 'array', items: { type: 'object' } },
3818
+ },
3819
+ };
3820
+ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3821
+ schemaVersion: '1.0.0',
3822
+ componentId: 'praxis-tabs',
3823
+ ownerPackage: '@praxisui/tabs',
3824
+ configSchemaId: 'TabsMetadata',
3825
+ manifestVersion: '1.0.0',
3826
+ runtimeInputs: [
3827
+ { name: 'config', type: 'TabsMetadata', description: 'Canonical tabs/nav configuration.' },
3828
+ { name: 'tabsId', type: 'string', description: 'Stable id used to derive persistence scope.' },
3829
+ { name: 'componentInstanceId', type: 'string', description: 'Optional instance discriminator for persistence scope.' },
3830
+ { name: 'form', type: 'FormGroup', description: 'FormGroup consumed by dynamic field content.' },
3831
+ { name: 'context', type: 'Record<string, any>', description: 'Context passed to nested widgets.' },
3832
+ { name: 'enableCustomization', type: 'boolean', description: 'Enables Settings Panel authoring surfaces.' },
3833
+ ],
3834
+ editableTargets: [
3835
+ { kind: 'tab', resolver: 'tab-by-id-or-label', description: 'A group-mode tab in config.tabs[].' },
3836
+ { kind: 'tabLabel', resolver: 'tab-by-id-or-label', description: 'The text label of a group-mode tab.' },
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.' },
3839
+ { kind: 'activeTab', resolver: 'tab-index-or-id', description: 'Selected tab or nav link index.' },
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.' },
3842
+ { kind: 'layout', resolver: 'tabs-layout-config', description: 'Group/nav mode, header position, density, stretch and behavior settings.' },
3843
+ ],
3844
+ operations: [
3845
+ {
3846
+ operationId: 'tab.add',
3847
+ title: 'Add tab',
3848
+ scope: 'global',
3849
+ targetKind: 'tab',
3850
+ target: { kind: 'tab', resolver: 'tabs-array', ambiguityPolicy: 'fail', required: false },
3851
+ inputSchema: tabItemSchema,
3852
+ effects: [{ kind: 'append-unique', path: 'tabs[]', key: 'id' }],
3853
+ validators: ['tab-id-unique', 'tabs-mode-compatible', 'tab-content-valid'],
3854
+ destructive: false,
3855
+ requiresConfirmation: false,
3856
+ affectedPaths: ['tabs[]'],
3857
+ submissionImpact: 'config-only',
3858
+ preconditions: ['config-initialized'],
3859
+ },
3860
+ {
3861
+ operationId: 'tab.remove',
3862
+ title: 'Remove tab',
3863
+ scope: 'layout',
3864
+ targetKind: 'tab',
3865
+ target: { kind: 'tab', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3866
+ inputSchema: {
3867
+ type: 'object',
3868
+ properties: {
3869
+ replacementActiveTabId: { type: 'string' },
3870
+ },
3871
+ },
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
+ ],
3889
+ destructive: true,
3890
+ requiresConfirmation: true,
3891
+ validators: ['tab-exists', 'active-tab-removal-safe', 'tab-content-removal-confirmed'],
3892
+ affectedPaths: ['tabs[]', 'group.selectedIndex'],
3893
+ submissionImpact: 'config-only',
3894
+ preconditions: ['config-initialized', 'target-tab-exists', 'confirmation-collected'],
3895
+ },
3896
+ {
3897
+ operationId: 'tab.label.set',
3898
+ title: 'Set tab label',
3899
+ scope: 'layout',
3900
+ targetKind: 'tabLabel',
3901
+ target: { kind: 'tabLabel', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3902
+ inputSchema: { type: 'object', required: ['textLabel'], properties: { textLabel: { type: 'string' } } },
3903
+ effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
3904
+ destructive: false,
3905
+ requiresConfirmation: false,
3906
+ validators: ['tab-exists', 'tab-label-valid'],
3907
+ affectedPaths: ['tabs[].textLabel'],
3908
+ submissionImpact: 'config-only',
3909
+ preconditions: ['config-initialized', 'target-tab-exists'],
3910
+ },
3911
+ {
3912
+ operationId: 'tab.icon.set',
3913
+ title: 'Set tab icon',
3914
+ scope: 'layout',
3915
+ targetKind: 'tabIcon',
3916
+ target: { kind: 'tabIcon', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3917
+ inputSchema: { type: 'object', required: ['icon'], properties: { icon: { type: 'string' } } },
3918
+ effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
3919
+ destructive: false,
3920
+ requiresConfirmation: false,
3921
+ validators: ['tab-exists', 'tab-icon-valid'],
3922
+ affectedPaths: ['tabs[].icon'],
3923
+ submissionImpact: 'visual-only',
3924
+ preconditions: ['config-initialized', 'target-tab-exists'],
3925
+ },
3926
+ {
3927
+ operationId: 'tab.order.set',
3928
+ title: 'Reorder tabs',
3929
+ scope: 'layout',
3930
+ targetKind: 'tab',
3931
+ target: { kind: 'tab', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3932
+ inputSchema: { type: 'object', required: ['beforeTabId'], properties: { beforeTabId: { type: 'string' } } },
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,
3949
+ validators: ['tab-exists', 'tab-order-deterministic'],
3950
+ affectedPaths: ['tabs[]', 'group.selectedIndex'],
3951
+ submissionImpact: 'config-only',
3952
+ preconditions: ['config-initialized', 'target-tab-exists'],
3953
+ },
3954
+ {
3955
+ operationId: 'tab.disabled.set',
3956
+ title: 'Set tab disabled state',
3957
+ scope: 'interaction',
3958
+ targetKind: 'disabledState',
3959
+ target: { kind: 'disabledState', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
3960
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
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,
3977
+ validators: ['tab-or-link-exists', 'active-tab-disabled-safe'],
3978
+ affectedPaths: ['tabs[].disabled', 'nav.links[].disabled'],
3979
+ submissionImpact: 'config-only',
3980
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
3981
+ },
3982
+ {
3983
+ operationId: 'tab.visible.set',
3984
+ title: 'Set tab visibility',
3985
+ scope: 'interaction',
3986
+ targetKind: 'visibility',
3987
+ target: { kind: 'visibility', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
3988
+ inputSchema: { type: 'object', required: ['visible'], properties: { visible: { type: 'boolean' } } },
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,
4005
+ validators: ['tab-or-link-exists', 'active-tab-visibility-safe'],
4006
+ affectedPaths: ['tabs[].visible', 'nav.links[].visible'],
4007
+ submissionImpact: 'config-only',
4008
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
4009
+ },
4010
+ {
4011
+ operationId: 'tab.active.set',
4012
+ title: 'Set active tab',
4013
+ scope: 'interaction',
4014
+ targetKind: 'activeTab',
4015
+ target: { kind: 'activeTab', resolver: 'tab-index-or-id', ambiguityPolicy: 'fail', required: true },
4016
+ inputSchema: { type: 'object', required: ['selectedIndex'], properties: { selectedIndex: { type: 'number' }, tabId: { type: 'string' } } },
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,
4033
+ validators: ['active-tab-exists', 'selected-index-in-range'],
4034
+ affectedPaths: ['group.selectedIndex', 'nav.selectedIndex'],
4035
+ submissionImpact: 'config-only',
4036
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
4037
+ },
4038
+ {
4039
+ operationId: 'layout.variant.set',
4040
+ title: 'Set tabs layout variant',
4041
+ scope: 'layout',
4042
+ targetKind: 'layout',
4043
+ target: { kind: 'layout', resolver: 'tabs-layout-config', ambiguityPolicy: 'fail', required: true },
4044
+ inputSchema: {
4045
+ type: 'object',
4046
+ required: ['mode'],
4047
+ properties: {
4048
+ mode: { enum: ['group', 'nav'] },
4049
+ density: { enum: ['compact', 'comfortable', 'spacious'] },
4050
+ headerPosition: { enum: ['above', 'below'] },
4051
+ alignTabs: { enum: ['start', 'center', 'end'] },
4052
+ stretchTabs: { type: 'boolean' },
4053
+ lazyLoad: { type: 'boolean' },
4054
+ },
4055
+ },
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,
4059
+ validators: ['tabs-mode-compatible', 'layout-values-valid', 'editor-runtime-round-trip'],
4060
+ affectedPaths: ['appearance.density', 'group.headerPosition', 'group.alignTabs', 'group.stretchTabs', 'nav.stretchTabs', 'behavior.lazyLoad'],
4061
+ submissionImpact: 'config-only',
4062
+ preconditions: ['config-initialized'],
4063
+ },
4064
+ {
4065
+ operationId: 'tab.content.set',
4066
+ title: 'Set tab content',
4067
+ scope: 'layout',
4068
+ targetKind: 'tabContent',
4069
+ target: { kind: 'tabContent', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
4070
+ inputSchema: tabPatchSchema,
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,
4087
+ validators: ['tab-or-link-exists', 'tab-content-valid', 'widget-event-delegated'],
4088
+ affectedPaths: ['tabs[].content', 'tabs[].widgets', 'nav.links[].content', 'nav.links[].widgets'],
4089
+ submissionImpact: 'config-only',
4090
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
4091
+ },
4092
+ ],
4093
+ validators: [
4094
+ { validatorId: 'tab-id-unique', level: 'error', code: 'PTABS001', description: 'Tab ids and nav link ids must be unique within their mode.' },
4095
+ { validatorId: 'tab-exists', level: 'error', code: 'PTABS002', description: 'Target tab must exist before applying the operation.' },
4096
+ { validatorId: 'tab-or-link-exists', level: 'error', code: 'PTABS003', description: 'Target must resolve to an existing group tab or nav link.' },
4097
+ { validatorId: 'active-tab-exists', level: 'error', code: 'PTABS004', description: 'Active tab or nav link selection must reference an existing item.' },
4098
+ { validatorId: 'selected-index-in-range', level: 'error', code: 'PTABS005', description: 'Selected index must be clamped to the target mode item count.' },
4099
+ { validatorId: 'active-tab-removal-safe', level: 'error', code: 'PTABS006', description: 'Removing the active/default tab requires confirmation or a replacement active tab.' },
4100
+ { validatorId: 'tab-content-removal-confirmed', level: 'error', code: 'PTABS007', description: 'Removing a tab or link with content/widgets is destructive and requires confirmation.' },
4101
+ { validatorId: 'tab-label-valid', level: 'error', code: 'PTABS008', description: 'Tab labels must be non-empty text values after localization/domain projection.' },
4102
+ { validatorId: 'tab-icon-valid', level: 'warning', code: 'PTABS009', description: 'Tab icon metadata must remain compatible with the icon directive and editor round-trip.' },
4103
+ { validatorId: 'tab-order-deterministic', level: 'error', code: 'PTABS010', description: 'Tab ordering must use stable ids, not transient array index as identity.' },
4104
+ { validatorId: 'tabs-mode-compatible', level: 'error', code: 'PTABS011', description: 'Authoring must resolve to one primary mode: group tabs or nav links.' },
4105
+ { validatorId: 'layout-values-valid', level: 'error', code: 'PTABS012', description: 'Layout values must match TabsMetadata enums and runtime bindings.' },
4106
+ { validatorId: 'editor-runtime-round-trip', level: 'error', code: 'PTABS013', description: 'Settings Panel, quick setup, JSON editor and runtime must preserve ids, order and selected index.' },
4107
+ { validatorId: 'active-tab-disabled-safe', level: 'warning', code: 'PTABS014', description: 'Disabling the active item should move selection or request explicit confirmation.' },
4108
+ { validatorId: 'active-tab-visibility-safe', level: 'warning', code: 'PTABS015', description: 'Hiding the active item should move selection or request explicit confirmation.' },
4109
+ { validatorId: 'tab-content-valid', level: 'error', code: 'PTABS016', description: 'Tab content must be valid DynamicFieldMetadata[] or WidgetDefinition[] and preserve nested widget identity.' },
4110
+ { validatorId: 'widget-event-delegated', level: 'info', code: 'PTABS017', description: 'Nested widget event paths remain delegated to the tabs runtime contract and are not redefined by authoring.' },
4111
+ ],
4112
+ roundTripRequirements: [
4113
+ 'Operations must preserve stable tab/link ids; array index may be used only as a resolver fallback, never as canonical identity.',
4114
+ 'Settings Panel, quick setup and JSON editor must round-trip through TabsAuthoringDocument without losing config or bindings.',
4115
+ 'Group and nav modes must remain mutually explicit; authoring cannot silently mix config.tabs and nav.links as competing primary modes.',
4116
+ 'Nested widget events remain delegated through widgetEvent path enrichment and component-port nestedPath semantics.',
4117
+ ],
4118
+ examples: [
4119
+ { id: 'add-overview-tab', request: 'Add an Overview tab before the details tab.', operationId: 'tab.add', params: { id: 'overview', textLabel: 'Overview' }, isPositive: true },
4120
+ { id: 'rename-tab', request: 'Rename the details tab to Account Details.', operationId: 'tab.label.set', target: 'details', params: { textLabel: 'Account Details' }, isPositive: true },
4121
+ { id: 'reorder-tabs', request: 'Move billing before overview.', operationId: 'tab.order.set', target: 'billing', params: { beforeTabId: 'overview' }, isPositive: true },
4122
+ { id: 'disable-tab', request: 'Disable the audit tab until the user has permission.', operationId: 'tab.disabled.set', target: 'audit', params: { disabled: true }, isPositive: true },
4123
+ { id: 'activate-tab', request: 'Open the documents tab by default.', operationId: 'tab.active.set', target: 'documents', params: { tabId: 'documents', selectedIndex: 2 }, isPositive: true },
4124
+ { id: 'reject-duplicate-tab-id', request: 'Add another tab with id overview.', operationId: 'tab.add', params: { id: 'overview', textLabel: 'Duplicate Overview' }, isPositive: false },
4125
+ { id: 'confirm-remove-content-tab', request: 'Remove the details tab that contains widgets.', operationId: 'tab.remove', target: 'details', params: { replacementActiveTabId: 'overview' }, isPositive: true },
4126
+ ],
4127
+ };
4128
+
3678
4129
  /*
3679
4130
  * Public API Surface of praxis-tabs
3680
4131
  */
@@ -3683,4 +4134,4 @@ function providePraxisTabsMetadata() {
3683
4134
  * Generated bundle index. Do not edit.
3684
4135
  */
3685
4136
 
3686
- export { PRAXIS_TABS_COMPONENT_METADATA, PRAXIS_TABS_I18N_CONFIG, PRAXIS_TABS_I18N_NAMESPACE, PraxisTabs, PraxisTabsConfigEditor, TABS_AI_CAPABILITIES, buildTabsApplyPlan, createPraxisTabsI18nConfig, createTabsAuthoringDocument, normalizeTabsAuthoringDocument, providePraxisTabsI18n, providePraxisTabsMetadata, serializeTabsAuthoringDocument, toCanonicalTabsConfig, validateTabsAuthoringDocument };
4137
+ export { PRAXIS_TABS_AUTHORING_MANIFEST, PRAXIS_TABS_COMPONENT_METADATA, PRAXIS_TABS_I18N_CONFIG, PRAXIS_TABS_I18N_NAMESPACE, PraxisTabs, PraxisTabsConfigEditor, TABS_AI_CAPABILITIES, buildTabsApplyPlan, createPraxisTabsI18nConfig, createTabsAuthoringDocument, normalizeTabsAuthoringDocument, providePraxisTabsI18n, providePraxisTabsMetadata, serializeTabsAuthoringDocument, toCanonicalTabsConfig, validateTabsAuthoringDocument };
package/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { OnInit, OnChanges, OnDestroy, EventEmitter, SimpleChanges, Provider } from '@angular/core';
3
3
  import { MatTabChangeEvent } from '@angular/material/tabs';
4
- import { AiCapability, WidgetDefinition, WidgetEventEnvelope, WidgetEventPathSegment, PraxisI18nConfig, ComponentDocMeta, ComponentMetadataRegistry, AiCapabilityCategory, AiValueKind, AiCapabilityCatalog } from '@praxisui/core';
4
+ import { AiCapability, WidgetDefinition, WidgetEventEnvelope, WidgetEventPathSegment, PraxisI18nConfig, ComponentDocMeta, ComponentMetadataRegistry, AiCapabilityCategory, AiValueKind, AiCapabilityCatalog, ComponentAuthoringManifest } from '@praxisui/core';
5
5
  import { FormGroup } from '@angular/forms';
6
6
  import { CdkDragDrop } from '@angular/cdk/drag-drop';
7
7
  import { BaseAiAdapter, PatchResult } from '@praxisui/ai';
@@ -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";
@@ -412,5 +440,7 @@ interface CapabilityCatalog extends AiCapabilityCatalog {
412
440
  }
413
441
  declare const TABS_AI_CAPABILITIES: CapabilityCatalog;
414
442
 
415
- export { PRAXIS_TABS_COMPONENT_METADATA, PRAXIS_TABS_I18N_CONFIG, PRAXIS_TABS_I18N_NAMESPACE, PraxisTabs, PraxisTabsConfigEditor, TABS_AI_CAPABILITIES, buildTabsApplyPlan, createPraxisTabsI18nConfig, createTabsAuthoringDocument, normalizeTabsAuthoringDocument, providePraxisTabsI18n, providePraxisTabsMetadata, serializeTabsAuthoringDocument, toCanonicalTabsConfig, validateTabsAuthoringDocument };
443
+ declare const PRAXIS_TABS_AUTHORING_MANIFEST: ComponentAuthoringManifest;
444
+
445
+ export { PRAXIS_TABS_AUTHORING_MANIFEST, PRAXIS_TABS_COMPONENT_METADATA, PRAXIS_TABS_I18N_CONFIG, PRAXIS_TABS_I18N_NAMESPACE, PraxisTabs, PraxisTabsConfigEditor, TABS_AI_CAPABILITIES, buildTabsApplyPlan, createPraxisTabsI18nConfig, createTabsAuthoringDocument, normalizeTabsAuthoringDocument, providePraxisTabsI18n, providePraxisTabsMetadata, serializeTabsAuthoringDocument, toCanonicalTabsConfig, validateTabsAuthoringDocument };
416
446
  export type { Capability, CapabilityCatalog, CapabilityCategory, TabGroupMetadata, TabLinkMetadata, TabMetadata, TabNavMetadata, TabsAccessibilityConfig, TabsAppearanceConfig, TabsApplyPlan, TabsAuthoringBindings, TabsAuthoringDocument, TabsBehaviorConfig, TabsBindingsDiff, TabsEditorDiagnostic, TabsEditorDiagnosticLevel, TabsEditorDocumentKind, TabsEventConfig, TabsMetadata, TabsRuntimeContext, TabsRuntimePlan, TabsStyleTokens, ValueKind };
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@praxisui/tabs",
3
- "version": "8.0.0-beta.1",
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.1",
11
- "@praxisui/dynamic-fields": "^8.0.0-beta.1",
12
- "@praxisui/settings-panel": "^8.0.0-beta.1"
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",