@praxisui/tabs 8.0.0-beta.2 → 8.0.0-beta.20

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.
@@ -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)
@@ -2691,19 +2776,26 @@ class PraxisTabs {
2691
2776
  return id ? `tabs:${id}` : null;
2692
2777
  }
2693
2778
  syncSelectionFromConfig() {
2694
- this.groupLoaded.clear();
2695
- this.navLoaded.clear();
2696
2779
  const tabsLength = this.config?.tabs?.length ?? 0;
2697
2780
  const linksLength = this.config?.nav?.links?.length ?? 0;
2698
2781
  const groupIndex = this.clampIndex(this.config?.group?.selectedIndex, tabsLength);
2699
2782
  const navIndex = this.clampIndex(this.config?.nav?.selectedIndex, linksLength);
2700
2783
  this.selectedIndexSignal.set(groupIndex);
2701
2784
  this.currentNavIndex.set(navIndex);
2785
+ this.pruneLoadedIndexes(this.groupLoaded, tabsLength);
2786
+ this.pruneLoadedIndexes(this.navLoaded, linksLength);
2702
2787
  if (tabsLength > 0)
2703
2788
  this.groupLoaded.add(groupIndex);
2704
2789
  if (linksLength > 0)
2705
2790
  this.navLoaded.add(navIndex);
2706
2791
  }
2792
+ pruneLoadedIndexes(indexes, size) {
2793
+ for (const index of Array.from(indexes)) {
2794
+ if (index < 0 || index >= size) {
2795
+ indexes.delete(index);
2796
+ }
2797
+ }
2798
+ }
2707
2799
  persistConfig(config) {
2708
2800
  const key = this.storageKey();
2709
2801
  if (!key || !config)
@@ -2764,10 +2856,16 @@ class PraxisTabs {
2764
2856
  return !this.isLazy() || this.navLoaded.has(index) || this.currentNavIndex() === index;
2765
2857
  }
2766
2858
  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);
2859
+ const hasTabs = this.visibleTabEntries().length > 0;
2860
+ const hasLinks = this.visibleNavLinkEntries().length > 0;
2769
2861
  return !(hasTabs || hasLinks);
2770
2862
  }
2863
+ trackVisibleNavLink(index, entry) {
2864
+ return entry.link.id || `${entry.link.label || 'nav-link'}:${entry.index ?? index}`;
2865
+ }
2866
+ trackVisibleTab(index, entry) {
2867
+ return entry.tab.id || entry.tab.textLabel || `tab:${entry.index ?? index}`;
2868
+ }
2771
2869
  trackNavLink(index, link) {
2772
2870
  return link.id || `${link.label || 'nav-link'}:${index}`;
2773
2871
  }
@@ -2952,7 +3050,7 @@ class PraxisTabs {
2952
3050
  return JSON.parse(JSON.stringify(widget));
2953
3051
  }
2954
3052
  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: `
3053
+ 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
3054
  <div
2957
3055
  class="praxis-tabs-root"
2958
3056
  [class.density-compact]="config?.appearance?.density === 'compact'"
@@ -2995,13 +3093,13 @@ class PraxisTabs {
2995
3093
  cdkDropList
2996
3094
  cdkDropListOrientation="horizontal"
2997
3095
  [cdkDropListDisabled]="!config?.behavior?.reorderable"
2998
- (cdkDropListDropped)="onNavDrop($event)"
3096
+ (cdkDropListDropped)="onVisibleNavDrop($event)"
2999
3097
  [disablePagination]="config?.nav?.disablePagination"
3000
3098
  [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
3001
3099
  [mat-stretch-tabs]="config?.nav?.stretchTabs"
3002
3100
  [color]="config?.nav?.color"
3003
3101
  [backgroundColor]="config?.nav?.backgroundColor"
3004
- [selectedIndex]="currentNavIndex()"
3102
+ [selectedIndex]="selectedVisibleNavIndex()"
3005
3103
  [attr.aria-label]="config?.nav?.ariaLabel || config?.group?.ariaLabel || null"
3006
3104
  [attr.aria-labelledby]="config?.nav?.ariaLabelledby || config?.group?.ariaLabelledby || null"
3007
3105
  [animationDuration]="effectiveAnimationDuration()"
@@ -3010,21 +3108,22 @@ class PraxisTabs {
3010
3108
  >
3011
3109
  <a
3012
3110
  mat-tab-link
3013
- *ngFor="let link of config?.nav?.links; let i = index; trackBy: trackNavLink"
3111
+ *ngFor="let entry of visibleNavLinkEntries(); let i = index; trackBy: trackVisibleNavLink"
3014
3112
  cdkDrag
3015
3113
  [cdkDragDisabled]="!config?.behavior?.reorderable"
3016
3114
  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)"
3115
+ [active]="getNavActive(entry.index)"
3116
+ [disabled]="entry.link.disabled"
3117
+ [disableRipple]="config?.nav?.disableRipple || entry.link.disableRipple"
3118
+ [fitInkBarToContent]="entry.link.fitInkBarToContent || false"
3119
+ [id]="entry.link.id || ''"
3120
+ (click)="onNavClick(entry.index)"
3023
3121
  >
3024
3122
  <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
3025
3123
  <mat-icon fontIcon="drag_indicator"></mat-icon>
3026
3124
  </span>
3027
- {{ link.label }}
3125
+ <mat-icon *ngIf="entry.link.icon" class="tab-label-icon" [praxisIcon]="entry.link.icon"></mat-icon>
3126
+ {{ entry.link.label }}
3028
3127
  </a>
3029
3128
  </nav>
3030
3129
 
@@ -3074,7 +3173,7 @@ class PraxisTabs {
3074
3173
  [fitInkBarToContent]="config?.group?.fitInkBarToContent"
3075
3174
  [headerPosition]="config?.group?.headerPosition ?? 'above'"
3076
3175
  [preserveContent]="config?.group?.preserveContent"
3077
- [selectedIndex]="selectedIndexSignal()"
3176
+ [selectedIndex]="selectedVisibleTabIndex()"
3078
3177
  [mat-stretch-tabs]="config?.group?.stretchTabs"
3079
3178
  [mat-align-tabs]="config?.group?.alignTabs || 'start'"
3080
3179
  [color]="config?.group?.color"
@@ -3084,26 +3183,27 @@ class PraxisTabs {
3084
3183
  [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
3085
3184
  (animationDone)="animationDone.emit()"
3086
3185
  (focusChange)="focusChange.emit($event)"
3087
- (selectedIndexChange)="onSelectedIndexChange($event)"
3186
+ (selectedIndexChange)="onVisibleTabIndexChange($event)"
3088
3187
  (selectedTabChange)="selectedTabChange.emit($event)"
3089
3188
  class="praxis-tabs-group"
3090
3189
  >
3091
3190
  <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"
3191
+ *ngFor="let entry of visibleTabEntries(); let i = index; trackBy: trackVisibleTab"
3192
+ [disabled]="entry.tab.disabled"
3193
+ [labelClass]="entry.tab.labelClass ?? ''"
3194
+ [bodyClass]="entry.tab.bodyClass ?? ''"
3195
+ [id]="entry.tab.id || ''"
3196
+ [attr.aria-label]="entry.tab.ariaLabel || null"
3197
+ [attr.aria-labelledby]="entry.tab.ariaLabelledby || null"
3099
3198
  >
3100
3199
  <ng-template mat-tab-label>
3101
- <span>{{ tab.textLabel }}</span>
3200
+ <mat-icon *ngIf="entry.tab.icon" class="tab-label-icon" [praxisIcon]="entry.tab.icon"></mat-icon>
3201
+ <span>{{ entry.tab.textLabel }}</span>
3102
3202
  <button
3103
3203
  *ngIf="config?.behavior?.closeable"
3104
3204
  mat-icon-button
3105
3205
  type="button"
3106
- (click)="closeTab(i); $event.stopPropagation()"
3206
+ (click)="closeTab(entry.index); $event.stopPropagation()"
3107
3207
  (keydown.enter)="$event.stopPropagation()"
3108
3208
  (keydown.space)="$event.stopPropagation()"
3109
3209
  [attr.aria-label]="t('chrome.closeTab', 'Fechar aba')"
@@ -3114,10 +3214,10 @@ class PraxisTabs {
3114
3214
  <button
3115
3215
  mat-icon-button
3116
3216
  type="button"
3117
- (click)="moveTab(i, -1); $event.stopPropagation()"
3217
+ (click)="moveTab(entry.index, -1); $event.stopPropagation()"
3118
3218
  (keydown.enter)="$event.stopPropagation()"
3119
3219
  (keydown.space)="$event.stopPropagation()"
3120
- [disabled]="i===0"
3220
+ [disabled]="entry.index===0"
3121
3221
  [attr.aria-label]="t('chrome.moveTabLeft', 'Mover aba para esquerda')"
3122
3222
  >
3123
3223
  <mat-icon fontIcon="arrow_back"></mat-icon>
@@ -3125,10 +3225,10 @@ class PraxisTabs {
3125
3225
  <button
3126
3226
  mat-icon-button
3127
3227
  type="button"
3128
- (click)="moveTab(i, 1); $event.stopPropagation()"
3228
+ (click)="moveTab(entry.index, 1); $event.stopPropagation()"
3129
3229
  (keydown.enter)="$event.stopPropagation()"
3130
3230
  (keydown.space)="$event.stopPropagation()"
3131
- [disabled]="i===(config?.tabs?.length||1)-1"
3231
+ [disabled]="entry.index===(config?.tabs?.length||1)-1"
3132
3232
  [attr.aria-label]="t('chrome.moveTabRight', 'Mover aba para direita')"
3133
3233
  >
3134
3234
  <mat-icon fontIcon="arrow_forward"></mat-icon>
@@ -3137,20 +3237,20 @@ class PraxisTabs {
3137
3237
  </ng-template>
3138
3238
 
3139
3239
  <ng-template matTabContent>
3140
- <ng-container *ngIf="(tab.content?.length || tab.widgets?.length) && groupContentReady(i); else emptyTab">
3141
- <ng-container *ngIf="tab.content && form">
3240
+ <ng-container *ngIf="(entry.tab.content?.length || entry.tab.widgets?.length) && groupContentReady(entry.index); else emptyTab">
3241
+ <ng-container *ngIf="entry.tab.content && form">
3142
3242
  <ng-container
3143
3243
  dynamicFieldLoader
3144
- [fields]="tab.content || []"
3244
+ [fields]="entry.tab.content || []"
3145
3245
  [formGroup]="form!"
3146
3246
  ></ng-container>
3147
3247
  </ng-container>
3148
- <ng-container *ngIf="tab.widgets?.length">
3149
- <ng-container *ngFor="let w of tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3248
+ <ng-container *ngIf="entry.tab.widgets?.length">
3249
+ <ng-container *ngFor="let w of entry.tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3150
3250
  <ng-container
3151
3251
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3152
3252
  [context]="context || {}"
3153
- (widgetEvent)="emitWidgetEvent(tabEventPath(tab.id, i), $event)"
3253
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
3154
3254
  ></ng-container>
3155
3255
  </ng-container>
3156
3256
  </ng-container>
@@ -3191,7 +3291,7 @@ class PraxisTabs {
3191
3291
  <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
3192
3292
  </button>
3193
3293
  </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 });
3294
+ `, 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
3295
  }
3196
3296
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabs, decorators: [{
3197
3297
  type: Component,
@@ -3251,13 +3351,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3251
3351
  cdkDropList
3252
3352
  cdkDropListOrientation="horizontal"
3253
3353
  [cdkDropListDisabled]="!config?.behavior?.reorderable"
3254
- (cdkDropListDropped)="onNavDrop($event)"
3354
+ (cdkDropListDropped)="onVisibleNavDrop($event)"
3255
3355
  [disablePagination]="config?.nav?.disablePagination"
3256
3356
  [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
3257
3357
  [mat-stretch-tabs]="config?.nav?.stretchTabs"
3258
3358
  [color]="config?.nav?.color"
3259
3359
  [backgroundColor]="config?.nav?.backgroundColor"
3260
- [selectedIndex]="currentNavIndex()"
3360
+ [selectedIndex]="selectedVisibleNavIndex()"
3261
3361
  [attr.aria-label]="config?.nav?.ariaLabel || config?.group?.ariaLabel || null"
3262
3362
  [attr.aria-labelledby]="config?.nav?.ariaLabelledby || config?.group?.ariaLabelledby || null"
3263
3363
  [animationDuration]="effectiveAnimationDuration()"
@@ -3266,21 +3366,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3266
3366
  >
3267
3367
  <a
3268
3368
  mat-tab-link
3269
- *ngFor="let link of config?.nav?.links; let i = index; trackBy: trackNavLink"
3369
+ *ngFor="let entry of visibleNavLinkEntries(); let i = index; trackBy: trackVisibleNavLink"
3270
3370
  cdkDrag
3271
3371
  [cdkDragDisabled]="!config?.behavior?.reorderable"
3272
3372
  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)"
3373
+ [active]="getNavActive(entry.index)"
3374
+ [disabled]="entry.link.disabled"
3375
+ [disableRipple]="config?.nav?.disableRipple || entry.link.disableRipple"
3376
+ [fitInkBarToContent]="entry.link.fitInkBarToContent || false"
3377
+ [id]="entry.link.id || ''"
3378
+ (click)="onNavClick(entry.index)"
3279
3379
  >
3280
3380
  <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
3281
3381
  <mat-icon fontIcon="drag_indicator"></mat-icon>
3282
3382
  </span>
3283
- {{ link.label }}
3383
+ <mat-icon *ngIf="entry.link.icon" class="tab-label-icon" [praxisIcon]="entry.link.icon"></mat-icon>
3384
+ {{ entry.link.label }}
3284
3385
  </a>
3285
3386
  </nav>
3286
3387
 
@@ -3330,7 +3431,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3330
3431
  [fitInkBarToContent]="config?.group?.fitInkBarToContent"
3331
3432
  [headerPosition]="config?.group?.headerPosition ?? 'above'"
3332
3433
  [preserveContent]="config?.group?.preserveContent"
3333
- [selectedIndex]="selectedIndexSignal()"
3434
+ [selectedIndex]="selectedVisibleTabIndex()"
3334
3435
  [mat-stretch-tabs]="config?.group?.stretchTabs"
3335
3436
  [mat-align-tabs]="config?.group?.alignTabs || 'start'"
3336
3437
  [color]="config?.group?.color"
@@ -3340,26 +3441,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3340
3441
  [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
3341
3442
  (animationDone)="animationDone.emit()"
3342
3443
  (focusChange)="focusChange.emit($event)"
3343
- (selectedIndexChange)="onSelectedIndexChange($event)"
3444
+ (selectedIndexChange)="onVisibleTabIndexChange($event)"
3344
3445
  (selectedTabChange)="selectedTabChange.emit($event)"
3345
3446
  class="praxis-tabs-group"
3346
3447
  >
3347
3448
  <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"
3449
+ *ngFor="let entry of visibleTabEntries(); let i = index; trackBy: trackVisibleTab"
3450
+ [disabled]="entry.tab.disabled"
3451
+ [labelClass]="entry.tab.labelClass ?? ''"
3452
+ [bodyClass]="entry.tab.bodyClass ?? ''"
3453
+ [id]="entry.tab.id || ''"
3454
+ [attr.aria-label]="entry.tab.ariaLabel || null"
3455
+ [attr.aria-labelledby]="entry.tab.ariaLabelledby || null"
3355
3456
  >
3356
3457
  <ng-template mat-tab-label>
3357
- <span>{{ tab.textLabel }}</span>
3458
+ <mat-icon *ngIf="entry.tab.icon" class="tab-label-icon" [praxisIcon]="entry.tab.icon"></mat-icon>
3459
+ <span>{{ entry.tab.textLabel }}</span>
3358
3460
  <button
3359
3461
  *ngIf="config?.behavior?.closeable"
3360
3462
  mat-icon-button
3361
3463
  type="button"
3362
- (click)="closeTab(i); $event.stopPropagation()"
3464
+ (click)="closeTab(entry.index); $event.stopPropagation()"
3363
3465
  (keydown.enter)="$event.stopPropagation()"
3364
3466
  (keydown.space)="$event.stopPropagation()"
3365
3467
  [attr.aria-label]="t('chrome.closeTab', 'Fechar aba')"
@@ -3370,10 +3472,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3370
3472
  <button
3371
3473
  mat-icon-button
3372
3474
  type="button"
3373
- (click)="moveTab(i, -1); $event.stopPropagation()"
3475
+ (click)="moveTab(entry.index, -1); $event.stopPropagation()"
3374
3476
  (keydown.enter)="$event.stopPropagation()"
3375
3477
  (keydown.space)="$event.stopPropagation()"
3376
- [disabled]="i===0"
3478
+ [disabled]="entry.index===0"
3377
3479
  [attr.aria-label]="t('chrome.moveTabLeft', 'Mover aba para esquerda')"
3378
3480
  >
3379
3481
  <mat-icon fontIcon="arrow_back"></mat-icon>
@@ -3381,10 +3483,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3381
3483
  <button
3382
3484
  mat-icon-button
3383
3485
  type="button"
3384
- (click)="moveTab(i, 1); $event.stopPropagation()"
3486
+ (click)="moveTab(entry.index, 1); $event.stopPropagation()"
3385
3487
  (keydown.enter)="$event.stopPropagation()"
3386
3488
  (keydown.space)="$event.stopPropagation()"
3387
- [disabled]="i===(config?.tabs?.length||1)-1"
3489
+ [disabled]="entry.index===(config?.tabs?.length||1)-1"
3388
3490
  [attr.aria-label]="t('chrome.moveTabRight', 'Mover aba para direita')"
3389
3491
  >
3390
3492
  <mat-icon fontIcon="arrow_forward"></mat-icon>
@@ -3393,20 +3495,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3393
3495
  </ng-template>
3394
3496
 
3395
3497
  <ng-template matTabContent>
3396
- <ng-container *ngIf="(tab.content?.length || tab.widgets?.length) && groupContentReady(i); else emptyTab">
3397
- <ng-container *ngIf="tab.content && form">
3498
+ <ng-container *ngIf="(entry.tab.content?.length || entry.tab.widgets?.length) && groupContentReady(entry.index); else emptyTab">
3499
+ <ng-container *ngIf="entry.tab.content && form">
3398
3500
  <ng-container
3399
3501
  dynamicFieldLoader
3400
- [fields]="tab.content || []"
3502
+ [fields]="entry.tab.content || []"
3401
3503
  [formGroup]="form!"
3402
3504
  ></ng-container>
3403
3505
  </ng-container>
3404
- <ng-container *ngIf="tab.widgets?.length">
3405
- <ng-container *ngFor="let w of tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3506
+ <ng-container *ngIf="entry.tab.widgets?.length">
3507
+ <ng-container *ngFor="let w of entry.tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3406
3508
  <ng-container
3407
3509
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3408
3510
  [context]="context || {}"
3409
- (widgetEvent)="emitWidgetEvent(tabEventPath(tab.id, i), $event)"
3511
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
3410
3512
  ></ng-container>
3411
3513
  </ng-container>
3412
3514
  </ng-container>
@@ -3447,7 +3549,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3447
3549
  <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
3448
3550
  </button>
3449
3551
  </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"] }]
3552
+ `, 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
3553
  }], propDecorators: { config: [{
3452
3554
  type: Input
3453
3555
  }], tabsId: [{
@@ -3455,6 +3557,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3455
3557
  args: [{ required: true }]
3456
3558
  }], componentInstanceId: [{
3457
3559
  type: Input
3560
+ }], selectedIndex: [{
3561
+ type: Input
3458
3562
  }], enableCustomization: [{
3459
3563
  type: Input
3460
3564
  }], form: [{
@@ -3504,6 +3608,19 @@ const PRAXIS_TABS_PORTS = [
3504
3608
  description: 'Fragmento canonico de configuracao das tabs/nav e dos widgets internos.',
3505
3609
  exposure: { public: true, group: 'config' },
3506
3610
  },
3611
+ {
3612
+ id: 'selectedIndex',
3613
+ label: 'Indice selecionado',
3614
+ direction: 'input',
3615
+ semanticKind: 'value',
3616
+ schema: {
3617
+ id: 'number',
3618
+ kind: 'ts-type',
3619
+ ref: 'number',
3620
+ },
3621
+ description: 'Indice ativo de abas/nav para controle externo por composicao.',
3622
+ exposure: { public: true, group: 'state' },
3623
+ },
3507
3624
  {
3508
3625
  id: 'selectedIndexChange',
3509
3626
  label: 'Troca de indice selecionado',
@@ -3554,6 +3671,12 @@ const PRAXIS_TABS_COMPONENT_METADATA = {
3554
3671
  label: 'ID da instância',
3555
3672
  description: 'Identificador opcional para múltiplas instâncias na mesma rota',
3556
3673
  },
3674
+ {
3675
+ name: 'selectedIndex',
3676
+ type: 'number',
3677
+ label: 'Indice selecionado',
3678
+ description: 'Indice ativo de abas/nav controlavel externamente pela composicao',
3679
+ },
3557
3680
  {
3558
3681
  name: 'enableCustomization',
3559
3682
  type: 'boolean',
@@ -3675,6 +3798,341 @@ function providePraxisTabsMetadata() {
3675
3798
  };
3676
3799
  }
3677
3800
 
3801
+ const tabItemSchema = {
3802
+ type: 'object',
3803
+ required: ['id', 'textLabel'],
3804
+ properties: {
3805
+ id: { type: 'string' },
3806
+ textLabel: { type: 'string' },
3807
+ icon: { type: 'string' },
3808
+ disabled: { type: 'boolean' },
3809
+ visible: { type: 'boolean', default: true },
3810
+ content: { type: 'array', items: { type: 'object' } },
3811
+ widgets: { type: 'array', items: { type: 'object' } },
3812
+ },
3813
+ };
3814
+ const tabPatchSchema = {
3815
+ type: 'object',
3816
+ minProperties: 1,
3817
+ properties: {
3818
+ id: { type: 'string' },
3819
+ textLabel: { type: 'string' },
3820
+ icon: { type: 'string' },
3821
+ disabled: { type: 'boolean' },
3822
+ visible: { type: 'boolean' },
3823
+ content: { type: 'array', items: { type: 'object' } },
3824
+ widgets: { type: 'array', items: { type: 'object' } },
3825
+ },
3826
+ };
3827
+ const PRAXIS_TABS_AUTHORING_MANIFEST = {
3828
+ schemaVersion: '1.0.0',
3829
+ componentId: 'praxis-tabs',
3830
+ ownerPackage: '@praxisui/tabs',
3831
+ configSchemaId: 'TabsMetadata',
3832
+ manifestVersion: '1.0.0',
3833
+ runtimeInputs: [
3834
+ { name: 'config', type: 'TabsMetadata', description: 'Canonical tabs/nav configuration.' },
3835
+ { name: 'tabsId', type: 'string', description: 'Stable id used to derive persistence scope.' },
3836
+ { name: 'componentInstanceId', type: 'string', description: 'Optional instance discriminator for persistence scope.' },
3837
+ { name: 'form', type: 'FormGroup', description: 'FormGroup consumed by dynamic field content.' },
3838
+ { name: 'context', type: 'Record<string, any>', description: 'Context passed to nested widgets.' },
3839
+ { name: 'enableCustomization', type: 'boolean', description: 'Enables Settings Panel authoring surfaces.' },
3840
+ ],
3841
+ editableTargets: [
3842
+ { kind: 'tab', resolver: 'tab-by-id-or-label', description: 'A group-mode tab in config.tabs[].' },
3843
+ { kind: 'tabLabel', resolver: 'tab-by-id-or-label', description: 'The text label of a group-mode tab.' },
3844
+ { kind: 'tabIcon', resolver: 'tab-by-id-or-label', description: 'Icon metadata rendered in a group tab label.' },
3845
+ { kind: 'tabContent', resolver: 'tab-or-link-by-id', description: 'Dynamic fields or widgets hosted by a tab or nav link.' },
3846
+ { kind: 'activeTab', resolver: 'tab-index-or-id', description: 'Selected tab or nav link index.' },
3847
+ { kind: 'visibility', resolver: 'tab-or-link-by-id', description: 'Runtime visibility flag for a group tab or nav link.' },
3848
+ { kind: 'disabledState', resolver: 'tab-or-link-by-id', description: 'Disabled state of a tab or nav link.' },
3849
+ { kind: 'layout', resolver: 'tabs-layout-config', description: 'Group/nav mode, header position, density, stretch and behavior settings.' },
3850
+ ],
3851
+ operations: [
3852
+ {
3853
+ operationId: 'tab.add',
3854
+ title: 'Add tab',
3855
+ scope: 'global',
3856
+ targetKind: 'tab',
3857
+ target: { kind: 'tab', resolver: 'tabs-array', ambiguityPolicy: 'fail', required: false },
3858
+ inputSchema: tabItemSchema,
3859
+ effects: [{ kind: 'append-unique', path: 'tabs[]', key: 'id' }],
3860
+ validators: ['tab-id-unique', 'tabs-mode-compatible', 'tab-content-valid'],
3861
+ destructive: false,
3862
+ requiresConfirmation: false,
3863
+ affectedPaths: ['tabs[]'],
3864
+ submissionImpact: 'config-only',
3865
+ preconditions: ['config-initialized'],
3866
+ },
3867
+ {
3868
+ operationId: 'tab.remove',
3869
+ title: 'Remove tab',
3870
+ scope: 'layout',
3871
+ targetKind: 'tab',
3872
+ target: { kind: 'tab', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3873
+ inputSchema: {
3874
+ type: 'object',
3875
+ properties: {
3876
+ replacementActiveTabId: { type: 'string' },
3877
+ },
3878
+ },
3879
+ effects: [
3880
+ {
3881
+ kind: 'compile-domain-patch',
3882
+ handler: 'tabs.remove-tab-and-reselect',
3883
+ handlerContract: {
3884
+ reads: ['tabs[]', 'group.selectedIndex'],
3885
+ writes: ['tabs[]', 'group.selectedIndex'],
3886
+ identityKeys: ['tabs[].id'],
3887
+ inputSchema: {
3888
+ type: 'object',
3889
+ properties: { replacementActiveTabId: { type: 'string' } },
3890
+ },
3891
+ failureModes: ['target-tab-missing', 'replacement-tab-missing', 'confirmation-missing'],
3892
+ description: 'Removes the target tab by stable id and reselects a safe replacement when the active/default tab is removed.',
3893
+ },
3894
+ },
3895
+ ],
3896
+ destructive: true,
3897
+ requiresConfirmation: true,
3898
+ validators: ['tab-exists', 'active-tab-removal-safe', 'tab-content-removal-confirmed'],
3899
+ affectedPaths: ['tabs[]', 'group.selectedIndex'],
3900
+ submissionImpact: 'config-only',
3901
+ preconditions: ['config-initialized', 'target-tab-exists', 'confirmation-collected'],
3902
+ },
3903
+ {
3904
+ operationId: 'tab.label.set',
3905
+ title: 'Set tab label',
3906
+ scope: 'layout',
3907
+ targetKind: 'tabLabel',
3908
+ target: { kind: 'tabLabel', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3909
+ inputSchema: { type: 'object', required: ['textLabel'], properties: { textLabel: { type: 'string' } } },
3910
+ effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
3911
+ destructive: false,
3912
+ requiresConfirmation: false,
3913
+ validators: ['tab-exists', 'tab-label-valid'],
3914
+ affectedPaths: ['tabs[].textLabel'],
3915
+ submissionImpact: 'config-only',
3916
+ preconditions: ['config-initialized', 'target-tab-exists'],
3917
+ },
3918
+ {
3919
+ operationId: 'tab.icon.set',
3920
+ title: 'Set tab icon',
3921
+ scope: 'layout',
3922
+ targetKind: 'tabIcon',
3923
+ target: { kind: 'tabIcon', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3924
+ inputSchema: { type: 'object', required: ['icon'], properties: { icon: { type: 'string' } } },
3925
+ effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
3926
+ destructive: false,
3927
+ requiresConfirmation: false,
3928
+ validators: ['tab-exists', 'tab-icon-valid'],
3929
+ affectedPaths: ['tabs[].icon'],
3930
+ submissionImpact: 'visual-only',
3931
+ preconditions: ['config-initialized', 'target-tab-exists'],
3932
+ },
3933
+ {
3934
+ operationId: 'tab.order.set',
3935
+ title: 'Reorder tabs',
3936
+ scope: 'layout',
3937
+ targetKind: 'tab',
3938
+ target: { kind: 'tab', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
3939
+ inputSchema: { type: 'object', required: ['beforeTabId'], properties: { beforeTabId: { type: 'string' } } },
3940
+ effects: [
3941
+ {
3942
+ kind: 'compile-domain-patch',
3943
+ handler: 'tabs.reorder-tab-and-preserve-selection',
3944
+ handlerContract: {
3945
+ reads: ['tabs[]', 'group.selectedIndex'],
3946
+ writes: ['tabs[]', 'group.selectedIndex'],
3947
+ identityKeys: ['tabs[].id'],
3948
+ inputSchema: { type: 'object', required: ['beforeTabId'], properties: { beforeTabId: { type: 'string' } } },
3949
+ failureModes: ['target-tab-missing', 'before-tab-missing', 'unstable-tab-id'],
3950
+ description: 'Reorders tabs by stable id and remaps group.selectedIndex when the selected tab crosses positions.',
3951
+ },
3952
+ },
3953
+ ],
3954
+ destructive: false,
3955
+ requiresConfirmation: false,
3956
+ validators: ['tab-exists', 'tab-order-deterministic'],
3957
+ affectedPaths: ['tabs[]', 'group.selectedIndex'],
3958
+ submissionImpact: 'config-only',
3959
+ preconditions: ['config-initialized', 'target-tab-exists'],
3960
+ },
3961
+ {
3962
+ operationId: 'tab.disabled.set',
3963
+ title: 'Set tab disabled state',
3964
+ scope: 'interaction',
3965
+ targetKind: 'disabledState',
3966
+ target: { kind: 'disabledState', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
3967
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
3968
+ effects: [
3969
+ {
3970
+ kind: 'compile-domain-patch',
3971
+ handler: 'tabs.set-tab-or-link-disabled',
3972
+ handlerContract: {
3973
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
3974
+ writes: ['tabs[].disabled', 'nav.links[].disabled'],
3975
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
3976
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
3977
+ failureModes: ['target-tab-or-link-missing', 'ambiguous-target', 'active-item-disabled-without-reselection'],
3978
+ description: 'Sets disabled on the resolved group tab or nav link without guessing between modes.',
3979
+ },
3980
+ },
3981
+ ],
3982
+ destructive: false,
3983
+ requiresConfirmation: false,
3984
+ validators: ['tab-or-link-exists', 'active-tab-disabled-safe'],
3985
+ affectedPaths: ['tabs[].disabled', 'nav.links[].disabled'],
3986
+ submissionImpact: 'config-only',
3987
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
3988
+ },
3989
+ {
3990
+ operationId: 'tab.visible.set',
3991
+ title: 'Set tab visibility',
3992
+ scope: 'interaction',
3993
+ targetKind: 'visibility',
3994
+ target: { kind: 'visibility', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
3995
+ inputSchema: { type: 'object', required: ['visible'], properties: { visible: { type: 'boolean' } } },
3996
+ effects: [
3997
+ {
3998
+ kind: 'compile-domain-patch',
3999
+ handler: 'tabs.set-tab-or-link-visible',
4000
+ handlerContract: {
4001
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
4002
+ writes: ['tabs[].visible', 'nav.links[].visible'],
4003
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
4004
+ inputSchema: { type: 'object', required: ['visible'], properties: { visible: { type: 'boolean' } } },
4005
+ failureModes: ['target-tab-or-link-missing', 'ambiguous-target', 'active-item-hidden-without-reselection'],
4006
+ description: 'Sets visible on the resolved group tab or nav link and preserves deterministic visible-index mapping.',
4007
+ },
4008
+ },
4009
+ ],
4010
+ destructive: false,
4011
+ requiresConfirmation: false,
4012
+ validators: ['tab-or-link-exists', 'active-tab-visibility-safe'],
4013
+ affectedPaths: ['tabs[].visible', 'nav.links[].visible'],
4014
+ submissionImpact: 'config-only',
4015
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
4016
+ },
4017
+ {
4018
+ operationId: 'tab.active.set',
4019
+ title: 'Set active tab',
4020
+ scope: 'interaction',
4021
+ targetKind: 'activeTab',
4022
+ target: { kind: 'activeTab', resolver: 'tab-index-or-id', ambiguityPolicy: 'fail', required: true },
4023
+ inputSchema: { type: 'object', required: ['selectedIndex'], properties: { selectedIndex: { type: 'number' }, tabId: { type: 'string' } } },
4024
+ effects: [
4025
+ {
4026
+ kind: 'compile-domain-patch',
4027
+ handler: 'tabs.set-active-item',
4028
+ handlerContract: {
4029
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
4030
+ writes: ['group.selectedIndex', 'nav.selectedIndex'],
4031
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
4032
+ inputSchema: { type: 'object', required: ['selectedIndex'], properties: { selectedIndex: { type: 'number' }, tabId: { type: 'string' } } },
4033
+ failureModes: ['target-tab-or-link-missing', 'selected-index-out-of-range', 'hidden-or-disabled-target'],
4034
+ description: 'Sets the active index for the current primary mode using either selectedIndex or a resolved tab/link id.',
4035
+ },
4036
+ },
4037
+ ],
4038
+ destructive: false,
4039
+ requiresConfirmation: false,
4040
+ validators: ['active-tab-exists', 'selected-index-in-range'],
4041
+ affectedPaths: ['group.selectedIndex', 'nav.selectedIndex'],
4042
+ submissionImpact: 'config-only',
4043
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
4044
+ },
4045
+ {
4046
+ operationId: 'layout.variant.set',
4047
+ title: 'Set tabs layout variant',
4048
+ scope: 'layout',
4049
+ targetKind: 'layout',
4050
+ target: { kind: 'layout', resolver: 'tabs-layout-config', ambiguityPolicy: 'fail', required: true },
4051
+ inputSchema: {
4052
+ type: 'object',
4053
+ required: ['mode'],
4054
+ properties: {
4055
+ mode: { enum: ['group', 'nav'] },
4056
+ density: { enum: ['compact', 'comfortable', 'spacious'] },
4057
+ headerPosition: { enum: ['above', 'below'] },
4058
+ alignTabs: { enum: ['start', 'center', 'end'] },
4059
+ stretchTabs: { type: 'boolean' },
4060
+ lazyLoad: { type: 'boolean' },
4061
+ },
4062
+ },
4063
+ effects: [{ kind: 'merge-object', path: 'appearance' }, { kind: 'merge-object', path: 'group' }, { kind: 'merge-object', path: 'nav' }, { kind: 'merge-object', path: 'behavior' }],
4064
+ destructive: false,
4065
+ requiresConfirmation: false,
4066
+ validators: ['tabs-mode-compatible', 'layout-values-valid', 'editor-runtime-round-trip'],
4067
+ affectedPaths: ['appearance.density', 'group.headerPosition', 'group.alignTabs', 'group.stretchTabs', 'nav.stretchTabs', 'behavior.lazyLoad'],
4068
+ submissionImpact: 'config-only',
4069
+ preconditions: ['config-initialized'],
4070
+ },
4071
+ {
4072
+ operationId: 'tab.content.set',
4073
+ title: 'Set tab content',
4074
+ scope: 'layout',
4075
+ targetKind: 'tabContent',
4076
+ target: { kind: 'tabContent', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
4077
+ inputSchema: tabPatchSchema,
4078
+ effects: [
4079
+ {
4080
+ kind: 'compile-domain-patch',
4081
+ handler: 'tabs.set-tab-or-link-content',
4082
+ handlerContract: {
4083
+ reads: ['tabs[]', 'nav.links[]'],
4084
+ writes: ['tabs[].content', 'tabs[].widgets', 'nav.links[].content', 'nav.links[].widgets'],
4085
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
4086
+ inputSchema: tabPatchSchema,
4087
+ failureModes: ['target-tab-or-link-missing', 'invalid-dynamic-field-content', 'invalid-widget-definition'],
4088
+ description: 'Updates content/widgets only on the resolved group tab or nav link while preserving nested widget identity.',
4089
+ },
4090
+ },
4091
+ ],
4092
+ destructive: false,
4093
+ requiresConfirmation: false,
4094
+ validators: ['tab-or-link-exists', 'tab-content-valid', 'widget-event-delegated'],
4095
+ affectedPaths: ['tabs[].content', 'tabs[].widgets', 'nav.links[].content', 'nav.links[].widgets'],
4096
+ submissionImpact: 'config-only',
4097
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
4098
+ },
4099
+ ],
4100
+ validators: [
4101
+ { validatorId: 'tab-id-unique', level: 'error', code: 'PTABS001', description: 'Tab ids and nav link ids must be unique within their mode.' },
4102
+ { validatorId: 'tab-exists', level: 'error', code: 'PTABS002', description: 'Target tab must exist before applying the operation.' },
4103
+ { validatorId: 'tab-or-link-exists', level: 'error', code: 'PTABS003', description: 'Target must resolve to an existing group tab or nav link.' },
4104
+ { validatorId: 'active-tab-exists', level: 'error', code: 'PTABS004', description: 'Active tab or nav link selection must reference an existing item.' },
4105
+ { validatorId: 'selected-index-in-range', level: 'error', code: 'PTABS005', description: 'Selected index must be clamped to the target mode item count.' },
4106
+ { validatorId: 'active-tab-removal-safe', level: 'error', code: 'PTABS006', description: 'Removing the active/default tab requires confirmation or a replacement active tab.' },
4107
+ { validatorId: 'tab-content-removal-confirmed', level: 'error', code: 'PTABS007', description: 'Removing a tab or link with content/widgets is destructive and requires confirmation.' },
4108
+ { validatorId: 'tab-label-valid', level: 'error', code: 'PTABS008', description: 'Tab labels must be non-empty text values after localization/domain projection.' },
4109
+ { validatorId: 'tab-icon-valid', level: 'warning', code: 'PTABS009', description: 'Tab icon metadata must remain compatible with the icon directive and editor round-trip.' },
4110
+ { validatorId: 'tab-order-deterministic', level: 'error', code: 'PTABS010', description: 'Tab ordering must use stable ids, not transient array index as identity.' },
4111
+ { validatorId: 'tabs-mode-compatible', level: 'error', code: 'PTABS011', description: 'Authoring must resolve to one primary mode: group tabs or nav links.' },
4112
+ { validatorId: 'layout-values-valid', level: 'error', code: 'PTABS012', description: 'Layout values must match TabsMetadata enums and runtime bindings.' },
4113
+ { 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.' },
4114
+ { validatorId: 'active-tab-disabled-safe', level: 'warning', code: 'PTABS014', description: 'Disabling the active item should move selection or request explicit confirmation.' },
4115
+ { validatorId: 'active-tab-visibility-safe', level: 'warning', code: 'PTABS015', description: 'Hiding the active item should move selection or request explicit confirmation.' },
4116
+ { validatorId: 'tab-content-valid', level: 'error', code: 'PTABS016', description: 'Tab content must be valid DynamicFieldMetadata[] or WidgetDefinition[] and preserve nested widget identity.' },
4117
+ { 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.' },
4118
+ ],
4119
+ roundTripRequirements: [
4120
+ 'Operations must preserve stable tab/link ids; array index may be used only as a resolver fallback, never as canonical identity.',
4121
+ 'Settings Panel, quick setup and JSON editor must round-trip through TabsAuthoringDocument without losing config or bindings.',
4122
+ 'Group and nav modes must remain mutually explicit; authoring cannot silently mix config.tabs and nav.links as competing primary modes.',
4123
+ 'Nested widget events remain delegated through widgetEvent path enrichment and component-port nestedPath semantics.',
4124
+ ],
4125
+ examples: [
4126
+ { id: 'add-overview-tab', request: 'Add an Overview tab before the details tab.', operationId: 'tab.add', params: { id: 'overview', textLabel: 'Overview' }, isPositive: true },
4127
+ { id: 'rename-tab', request: 'Rename the details tab to Account Details.', operationId: 'tab.label.set', target: 'details', params: { textLabel: 'Account Details' }, isPositive: true },
4128
+ { id: 'reorder-tabs', request: 'Move billing before overview.', operationId: 'tab.order.set', target: 'billing', params: { beforeTabId: 'overview' }, isPositive: true },
4129
+ { id: 'disable-tab', request: 'Disable the audit tab until the user has permission.', operationId: 'tab.disabled.set', target: 'audit', params: { disabled: true }, isPositive: true },
4130
+ { id: 'activate-tab', request: 'Open the documents tab by default.', operationId: 'tab.active.set', target: 'documents', params: { tabId: 'documents', selectedIndex: 2 }, isPositive: true },
4131
+ { id: 'reject-duplicate-tab-id', request: 'Add another tab with id overview.', operationId: 'tab.add', params: { id: 'overview', textLabel: 'Duplicate Overview' }, isPositive: false },
4132
+ { id: 'confirm-remove-content-tab', request: 'Remove the details tab that contains widgets.', operationId: 'tab.remove', target: 'details', params: { replacementActiveTabId: 'overview' }, isPositive: true },
4133
+ ],
4134
+ };
4135
+
3678
4136
  /*
3679
4137
  * Public API Surface of praxis-tabs
3680
4138
  */
@@ -3683,4 +4141,4 @@ function providePraxisTabsMetadata() {
3683
4141
  * Generated bundle index. Do not edit.
3684
4142
  */
3685
4143
 
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 };
4144
+ 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 };