@praxisui/tabs 8.0.0-beta.3 → 8.0.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Inject, Component, EventEmitter, signal, Output, Input, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { inject, Input, Inject, Component, ChangeDetectorRef, effect, EventEmitter, signal, Output, ChangeDetectionStrategy, ViewChild, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import { ActivatedRoute } from '@angular/router';
4
4
  import * as i1$1 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
@@ -14,7 +14,7 @@ import { providePraxisI18n, PraxisI18nService, PraxisIconDirective, providePraxi
14
14
  import * as i6$1 from '@angular/material/button';
15
15
  import { MatButtonModule } from '@angular/material/button';
16
16
  import * as i3 from '@angular/forms';
17
- import { FormsModule, ReactiveFormsModule } from '@angular/forms';
17
+ import { FormsModule, FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
18
18
  import { DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';
19
19
  import { SETTINGS_PANEL_DATA, SettingsPanelService } from '@praxisui/settings-panel';
20
20
  import * as i5 from '@angular/material/form-field';
@@ -25,11 +25,11 @@ import * as i9 from '@angular/material/slide-toggle';
25
25
  import { MatSlideToggleModule } from '@angular/material/slide-toggle';
26
26
  import * as i10 from '@angular/cdk/drag-drop';
27
27
  import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
28
- import { BehaviorSubject, Subject } from 'rxjs';
28
+ import { BehaviorSubject, firstValueFrom, Subject, Subscription } from 'rxjs';
29
29
  import { produce } from 'immer';
30
30
  import { MatSnackBar } from '@angular/material/snack-bar';
31
- import { take, takeUntil } from 'rxjs/operators';
32
- import { BaseAiAdapter, PraxisAiAssistantComponent } from '@praxisui/ai';
31
+ import { takeUntil, take } from 'rxjs/operators';
32
+ import { BaseAiAdapter, shouldRoutePromptToGovernedDecision, AiBackendApiService, PraxisAssistantSessionRegistryService, PraxisAssistantTurnOrchestratorService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
33
33
 
34
34
  const DOCUMENT_KIND = 'praxis.tabs.editor';
35
35
  const DOCUMENT_VERSION = 1;
@@ -375,6 +375,13 @@ const PRAXIS_TABS_PT_BR = {
375
375
  'quickSetup.mode.nav': 'Navegação por links',
376
376
  'quickSetup.fields.addLabelPlaceholder': 'Ex.: Dados Gerais',
377
377
  'quickSetup.hints.emptyLabels': 'Adicione um ou mais rótulos para criar as abas.',
378
+ 'ai.review.ready': 'Plano de edição das abas pronto para revisar.',
379
+ 'ai.review.unsupportedPlan': 'O componentEditPlan das abas não possui operações suportadas para aplicar.',
380
+ 'ai.review.addTab': 'Adicionar aba: {{label}}.',
381
+ 'ai.review.renameTab': 'Renomear aba para: {{label}}.',
382
+ 'ai.review.singleAdjustment': 'Revisar ajuste da aba: {{label}}.',
383
+ 'ai.review.multipleAdjustments': 'Revisar {{count}} ajustes nas abas: {{items}}',
384
+ 'ai.review.multipleAdjustmentsShort': 'Revisar {{count}} ajustes nas abas.',
378
385
  };
379
386
  const PRAXIS_TABS_EN_US = {
380
387
  'settings.title': 'Configure tabs',
@@ -513,6 +520,13 @@ const PRAXIS_TABS_EN_US = {
513
520
  'quickSetup.mode.nav': 'Link navigation',
514
521
  'quickSetup.fields.addLabelPlaceholder': 'e.g. General Data',
515
522
  'quickSetup.hints.emptyLabels': 'Add one or more labels to create the tabs.',
523
+ 'ai.review.ready': 'Tabs edit plan ready to review.',
524
+ 'ai.review.unsupportedPlan': 'The tabs componentEditPlan does not contain supported operations to apply.',
525
+ 'ai.review.addTab': 'Add tab: {{label}}.',
526
+ 'ai.review.renameTab': 'Rename tab to: {{label}}.',
527
+ 'ai.review.singleAdjustment': 'Review tab adjustment: {{label}}.',
528
+ 'ai.review.multipleAdjustments': 'Review {{count}} tab adjustments: {{items}}',
529
+ 'ai.review.multipleAdjustmentsShort': 'Review {{count}} tab adjustments.',
516
530
  };
517
531
  function createPraxisTabsI18nConfig() {
518
532
  return {
@@ -533,6 +547,12 @@ function providePraxisTabsI18n() {
533
547
  class PraxisTabsConfigEditor {
534
548
  registry;
535
549
  i18n = inject(PraxisI18nService);
550
+ set document(value) {
551
+ if (!value) {
552
+ return;
553
+ }
554
+ this.initializeDocument(value);
555
+ }
536
556
  primaryMode = 'group';
537
557
  editedDocument;
538
558
  editedConfig;
@@ -612,12 +632,18 @@ class PraxisTabsConfigEditor {
612
632
  this.initialDocument = structuredClone(incomingDocument);
613
633
  this.editedDocument = structuredClone(incomingDocument);
614
634
  this.editedConfig = this.editedDocument.config;
615
- this.bindings = this.editedDocument.bindings;
616
- this.primaryMode = this.inferPrimaryMode(this.editedConfig);
635
+ this.initializeDocument(incomingDocument);
636
+ this.componentOptions = this.registry.getAll().map((m) => ({ id: m.id, friendlyName: m.friendlyName }));
637
+ }
638
+ initializeDocument(document) {
639
+ const normalized = normalizeTabsAuthoringDocument(document);
640
+ this.initialDocument = structuredClone(normalized);
641
+ this.syncEditorStateFromDocument(normalized);
617
642
  this.jsonText = this.stringify(this.editedDocument);
643
+ this.isValid = true;
644
+ this.errorMsg = '';
618
645
  this.updateDirty();
619
646
  this.refreshDiagnostics();
620
- this.componentOptions = this.registry.getAll().map((m) => ({ id: m.id, friendlyName: m.friendlyName }));
621
647
  }
622
648
  inferPrimaryMode(config) {
623
649
  return config?.nav?.links?.length ? 'nav' : 'group';
@@ -784,6 +810,7 @@ class PraxisTabsConfigEditor {
784
810
  this.editedConfig.tabs.push({
785
811
  id: `tab${(this.editedConfig.tabs.length + 1)}`,
786
812
  textLabel: this.t('defaults.newTabLabel', 'New Tab'),
813
+ visible: true,
787
814
  });
788
815
  this.onAppearanceChange();
789
816
  }
@@ -859,6 +886,7 @@ class PraxisTabsConfigEditor {
859
886
  this.nav.links.push({
860
887
  id: `link${this.nav.links.length + 1}`,
861
888
  label: this.t('defaults.newLinkLabel', 'New Link'),
889
+ visible: true,
862
890
  });
863
891
  this.onAppearanceChange();
864
892
  }
@@ -953,7 +981,7 @@ class PraxisTabsConfigEditor {
953
981
  this.onAppearanceChange();
954
982
  }
955
983
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabsConfigEditor, deps: [{ token: SETTINGS_PANEL_DATA }, { token: i1.ComponentMetadataRegistry }], target: i0.ɵɵFactoryTarget.Component });
956
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisTabsConfigEditor, isStandalone: true, selector: "praxis-tabs-config-editor", providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], ngImport: i0, template: `
984
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisTabsConfigEditor, isStandalone: true, selector: "praxis-tabs-config-editor", inputs: { document: "document" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], ngImport: i0, template: `
957
985
  <div class="editor-shell">
958
986
  <div class="editor-topbar">
959
987
  <mat-form-field appearance="outline" class="editor-mode-field">
@@ -1293,6 +1321,9 @@ class PraxisTabsConfigEditor {
1293
1321
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1294
1322
  <input matInput [(ngModel)]="tab.textLabel" (ngModelChange)="onAppearanceChange()" />
1295
1323
  </mat-form-field>
1324
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1325
+ <input matInput [(ngModel)]="tab.icon" (ngModelChange)="onAppearanceChange()" />
1326
+ </mat-form-field>
1296
1327
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.labelClass', 'Classe do rotulo') }}</mat-label>
1297
1328
  <input matInput [(ngModel)]="tab.labelClass" (ngModelChange)="onAppearanceChange()" />
1298
1329
  </mat-form-field>
@@ -1306,7 +1337,10 @@ class PraxisTabsConfigEditor {
1306
1337
  <input matInput [(ngModel)]="tab.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
1307
1338
  </mat-form-field>
1308
1339
  </div>
1309
- <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1340
+ <div class="editor-row">
1341
+ <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1342
+ <mat-slide-toggle [ngModel]="tab.visible !== false" (ngModelChange)="tab.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1343
+ </div>
1310
1344
 
1311
1345
  <!-- Widgets (componentes dinâmicos) -->
1312
1346
  <div class="editor-divider editor-grid">
@@ -1384,9 +1418,13 @@ class PraxisTabsConfigEditor {
1384
1418
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1385
1419
  <input matInput [(ngModel)]="l.label" (ngModelChange)="onAppearanceChange()" />
1386
1420
  </mat-form-field>
1421
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1422
+ <input matInput [(ngModel)]="l.icon" (ngModelChange)="onAppearanceChange()" />
1423
+ </mat-form-field>
1387
1424
  </div>
1388
1425
  <div class="editor-row">
1389
1426
  <mat-slide-toggle [(ngModel)]="l.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.linkDisabled', 'Desativado') }}</mat-slide-toggle>
1427
+ <mat-slide-toggle [ngModel]="l.visible !== false" (ngModelChange)="l.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1390
1428
  <mat-slide-toggle [(ngModel)]="l.disableRipple" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disableRipple', 'Sem ripple') }}</mat-slide-toggle>
1391
1429
  <mat-slide-toggle [(ngModel)]="l.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.fitInkBarToContent', 'Indicador ajustado ao conteudo') }}</mat-slide-toggle>
1392
1430
  </div>
@@ -1797,6 +1835,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1797
1835
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1798
1836
  <input matInput [(ngModel)]="tab.textLabel" (ngModelChange)="onAppearanceChange()" />
1799
1837
  </mat-form-field>
1838
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1839
+ <input matInput [(ngModel)]="tab.icon" (ngModelChange)="onAppearanceChange()" />
1840
+ </mat-form-field>
1800
1841
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.labelClass', 'Classe do rotulo') }}</mat-label>
1801
1842
  <input matInput [(ngModel)]="tab.labelClass" (ngModelChange)="onAppearanceChange()" />
1802
1843
  </mat-form-field>
@@ -1810,7 +1851,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1810
1851
  <input matInput [(ngModel)]="tab.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
1811
1852
  </mat-form-field>
1812
1853
  </div>
1813
- <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1854
+ <div class="editor-row">
1855
+ <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1856
+ <mat-slide-toggle [ngModel]="tab.visible !== false" (ngModelChange)="tab.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1857
+ </div>
1814
1858
 
1815
1859
  <!-- Widgets (componentes dinâmicos) -->
1816
1860
  <div class="editor-divider editor-grid">
@@ -1888,9 +1932,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1888
1932
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1889
1933
  <input matInput [(ngModel)]="l.label" (ngModelChange)="onAppearanceChange()" />
1890
1934
  </mat-form-field>
1935
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1936
+ <input matInput [(ngModel)]="l.icon" (ngModelChange)="onAppearanceChange()" />
1937
+ </mat-form-field>
1891
1938
  </div>
1892
1939
  <div class="editor-row">
1893
1940
  <mat-slide-toggle [(ngModel)]="l.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.linkDisabled', 'Desativado') }}</mat-slide-toggle>
1941
+ <mat-slide-toggle [ngModel]="l.visible !== false" (ngModelChange)="l.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1894
1942
  <mat-slide-toggle [(ngModel)]="l.disableRipple" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disableRipple', 'Sem ripple') }}</mat-slide-toggle>
1895
1943
  <mat-slide-toggle [(ngModel)]="l.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.fitInkBarToContent', 'Indicador ajustado ao conteudo') }}</mat-slide-toggle>
1896
1944
  </div>
@@ -1949,7 +1997,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1949
1997
  }], ctorParameters: () => [{ type: undefined, decorators: [{
1950
1998
  type: Inject,
1951
1999
  args: [SETTINGS_PANEL_DATA]
1952
- }] }, { type: i1.ComponentMetadataRegistry }] });
2000
+ }] }, { type: i1.ComponentMetadataRegistry }], propDecorators: { document: [{
2001
+ type: Input
2002
+ }] } });
1953
2003
 
1954
2004
  class TabsQuickSetupComponent {
1955
2005
  i18n = inject(PraxisI18nService);
@@ -2259,10 +2309,80 @@ const TABS_AI_CAPABILITIES = {
2259
2309
 
2260
2310
  class TabsAiAdapter extends BaseAiAdapter {
2261
2311
  tabs;
2312
+ translate;
2262
2313
  componentName = 'Praxis Tabs';
2263
- constructor(tabs) {
2314
+ componentId = 'praxis-tabs';
2315
+ componentType = 'tabs';
2316
+ constructor(tabs, translate) {
2264
2317
  super();
2265
2318
  this.tabs = tabs;
2319
+ this.translate = translate;
2320
+ }
2321
+ compileAiResponse(response) {
2322
+ const plan = this.toRecord(response['componentEditPlan']);
2323
+ const operations = Array.isArray(plan?.['operations']) ? plan['operations'] : [];
2324
+ if (!plan || operations.length === 0) {
2325
+ return null;
2326
+ }
2327
+ const current = this.getCurrentConfig();
2328
+ const tabsPatch = [];
2329
+ const warnings = [];
2330
+ for (const operationCandidate of operations) {
2331
+ const operation = this.toRecord(operationCandidate);
2332
+ if (!operation) {
2333
+ warnings.push('tabs-component-edit-plan-operation-invalid');
2334
+ continue;
2335
+ }
2336
+ const operationId = this.toText(operation['operationId'] ?? operation['changeKind']);
2337
+ if (operationId === 'tab.label.set') {
2338
+ const targetId = this.resolveTargetId(operation, current);
2339
+ const textLabel = this.resolveTextLabel(operation);
2340
+ if (!targetId) {
2341
+ warnings.push('tabs-component-edit-plan-target-missing');
2342
+ continue;
2343
+ }
2344
+ if (!textLabel) {
2345
+ warnings.push('tabs-component-edit-plan-text-label-missing');
2346
+ continue;
2347
+ }
2348
+ tabsPatch.push({ id: targetId, textLabel });
2349
+ continue;
2350
+ }
2351
+ if (operationId === 'tab.add') {
2352
+ const tab = this.resolveTabAddInput(operation);
2353
+ if (!tab) {
2354
+ warnings.push('tabs-component-edit-plan-tab-add-input-invalid');
2355
+ continue;
2356
+ }
2357
+ tabsPatch.push(tab);
2358
+ continue;
2359
+ }
2360
+ if (operationId === 'tab.content.set') {
2361
+ const contentPatch = this.resolveTabContentInput(operation, current);
2362
+ if (!contentPatch) {
2363
+ warnings.push('tabs-component-edit-plan-tab-content-input-invalid');
2364
+ continue;
2365
+ }
2366
+ tabsPatch.push(contentPatch);
2367
+ continue;
2368
+ }
2369
+ warnings.push(`tabs-component-edit-plan-operation-unsupported:${operationId || '<missing>'}`);
2370
+ }
2371
+ if (tabsPatch.length === 0) {
2372
+ return {
2373
+ type: 'error',
2374
+ message: this.t('ai.review.unsupportedPlan', 'O componentEditPlan das abas não possui operações suportadas para aplicar.'),
2375
+ warnings,
2376
+ };
2377
+ }
2378
+ return {
2379
+ patch: { tabs: tabsPatch },
2380
+ explanation: this.summarizeOperations(operations, tabsPatch)
2381
+ ?? (typeof response['explanation'] === 'string'
2382
+ ? response['explanation']
2383
+ : this.t('ai.review.ready', 'Plano de edição das abas pronto para revisar.')),
2384
+ warnings: warnings.length ? warnings : undefined,
2385
+ };
2266
2386
  }
2267
2387
  getCurrentConfig() {
2268
2388
  return this.cloneConfig(this.tabs.config || {});
@@ -2270,6 +2390,49 @@ class TabsAiAdapter extends BaseAiAdapter {
2270
2390
  getCapabilities() {
2271
2391
  return TABS_AI_CAPABILITIES.capabilities;
2272
2392
  }
2393
+ getDataProfile() {
2394
+ const cfg = this.tabs.config;
2395
+ return {
2396
+ mode: cfg?.nav?.links?.length ? 'nav' : 'group',
2397
+ tabCount: cfg?.tabs?.length ?? 0,
2398
+ linkCount: cfg?.nav?.links?.length ?? 0,
2399
+ widgetCount: [
2400
+ ...(cfg?.tabs ?? []).flatMap((tab) => tab.widgets ?? []),
2401
+ ...(cfg?.nav?.links ?? []).flatMap((link) => link.widgets ?? []),
2402
+ ].length,
2403
+ fieldCount: [
2404
+ ...(cfg?.tabs ?? []).flatMap((tab) => tab.content ?? []),
2405
+ ...(cfg?.nav?.links ?? []).flatMap((link) => link.content ?? []),
2406
+ ].length,
2407
+ };
2408
+ }
2409
+ getSchemaFields() {
2410
+ const cfg = this.tabs.config;
2411
+ return [
2412
+ ...(cfg?.tabs ?? []).flatMap((tab) => tab.content ?? []),
2413
+ ...(cfg?.nav?.links ?? []).flatMap((link) => link.content ?? []),
2414
+ ]
2415
+ .map((field) => field?.name || field?.key || field?.id)
2416
+ .filter((name) => typeof name === 'string' && !!name.trim())
2417
+ .map((name) => ({ name }));
2418
+ }
2419
+ getAuthoringContext() {
2420
+ return {
2421
+ authoringManifestRef: {
2422
+ componentId: 'praxis-tabs',
2423
+ source: 'PRAXIS_TABS_AUTHORING_MANIFEST',
2424
+ },
2425
+ runtimeAuthoringPolicy: {
2426
+ mode: 'agentic-authoring',
2427
+ enableCustomization: !!this.tabs.enableCustomization,
2428
+ canApplyLocalPatch: false,
2429
+ reason: 'praxis-tabs ainda exige componentEditPlan manifest-backed antes de aplicar patch local pelo copiloto.',
2430
+ },
2431
+ domainCatalog: {
2432
+ recommendedAuthoringFlow: 'component_authoring',
2433
+ },
2434
+ };
2435
+ }
2273
2436
  getRuntimeState() {
2274
2437
  const cfg = this.tabs.config;
2275
2438
  return {
@@ -2321,16 +2484,40 @@ class TabsAiAdapter extends BaseAiAdapter {
2321
2484
  smartMergeTabsConfig(base, patch) {
2322
2485
  const result = deepMerge(base, patch);
2323
2486
  if (patch.tabs && Array.isArray(patch.tabs)) {
2324
- const merged = this.mergeByKey(base.tabs || [], patch.tabs, (t) => t.id || t.textLabel || '');
2487
+ const patchTabs = this.coalesceByKey(patch.tabs, (t) => t.id || t.textLabel || '');
2488
+ const merged = this.mergeByKey(base.tabs || [], patchTabs, (t) => t.id || t.textLabel || '');
2325
2489
  result.tabs = merged;
2326
2490
  }
2327
2491
  const patchLinks = patch.nav?.links;
2328
2492
  if (patchLinks && Array.isArray(patchLinks)) {
2329
- const merged = this.mergeByKey(base.nav?.links || [], patchLinks, (l) => l.id || l.label || '');
2493
+ const normalizedPatchLinks = this.coalesceByKey(patchLinks, (l) => l.id || l.label || '');
2494
+ const merged = this.mergeByKey(base.nav?.links || [], normalizedPatchLinks, (l) => l.id || l.label || '');
2330
2495
  result.nav = { ...(result.nav || {}), links: merged };
2331
2496
  }
2332
2497
  return result;
2333
2498
  }
2499
+ coalesceByKey(items, keyFn) {
2500
+ const keyed = new Map();
2501
+ const result = [];
2502
+ items.forEach((item) => {
2503
+ const key = keyFn(item);
2504
+ if (!key) {
2505
+ result.push(item);
2506
+ return;
2507
+ }
2508
+ const existing = keyed.get(key);
2509
+ if (existing) {
2510
+ keyed.set(key, deepMerge(existing, item));
2511
+ return;
2512
+ }
2513
+ keyed.set(key, item);
2514
+ result.push(item);
2515
+ });
2516
+ return result.map((item) => {
2517
+ const key = keyFn(item);
2518
+ return key && keyed.has(key) ? keyed.get(key) : item;
2519
+ });
2520
+ }
2334
2521
  mergeByKey(baseArr, patchArr, keyFn) {
2335
2522
  const merged = baseArr.map((orig) => {
2336
2523
  const key = keyFn(orig);
@@ -2353,8 +2540,506 @@ class TabsAiAdapter extends BaseAiAdapter {
2353
2540
  return JSON.parse(JSON.stringify(config));
2354
2541
  }
2355
2542
  }
2543
+ resolveTargetId(operation, current) {
2544
+ const target = this.toRecord(operation['target']);
2545
+ const rawTarget = target
2546
+ ? this.toText(target['id'] ?? target['label'] ?? target['textLabel'] ?? target['value'])
2547
+ : this.toText(operation['target'] ?? operation['targetId']);
2548
+ if (!rawTarget) {
2549
+ return null;
2550
+ }
2551
+ const normalized = this.normalizeToken(rawTarget);
2552
+ const tab = (current.tabs ?? []).find((candidate) => {
2553
+ const id = this.normalizeToken(candidate.id);
2554
+ const label = this.normalizeToken(candidate.textLabel);
2555
+ return id === normalized || label === normalized;
2556
+ });
2557
+ return tab?.id || rawTarget;
2558
+ }
2559
+ resolveTextLabel(operation) {
2560
+ const input = this.toRecord(operation['input'])
2561
+ ?? this.toRecord(operation['params'])
2562
+ ?? this.toRecord(operation['payload']);
2563
+ const value = input
2564
+ ? this.toText(input['textLabel'] ?? input['label'] ?? input['value'])
2565
+ : this.toText(operation['textLabel'] ?? operation['label'] ?? operation['value']);
2566
+ return value?.trim() || null;
2567
+ }
2568
+ resolveTabAddInput(operation) {
2569
+ const input = this.toRecord(operation['input'])
2570
+ ?? this.toRecord(operation['params'])
2571
+ ?? this.toRecord(operation['payload']);
2572
+ if (!input) {
2573
+ return null;
2574
+ }
2575
+ const id = this.toText(input['id']);
2576
+ const textLabel = this.toText(input['textLabel'] ?? input['label']);
2577
+ if (!id || !textLabel) {
2578
+ return null;
2579
+ }
2580
+ const tab = { id, textLabel };
2581
+ for (const key of ['icon', 'disabled', 'visible', 'content', 'widgets']) {
2582
+ if (input[key] !== undefined) {
2583
+ tab[key] = input[key];
2584
+ }
2585
+ }
2586
+ return tab;
2587
+ }
2588
+ resolveTabContentInput(operation, current) {
2589
+ const targetId = this.resolveTargetId(operation, current);
2590
+ if (!targetId) {
2591
+ return null;
2592
+ }
2593
+ const input = this.toRecord(operation['input'])
2594
+ ?? this.toRecord(operation['params'])
2595
+ ?? this.toRecord(operation['payload']);
2596
+ if (!input) {
2597
+ return null;
2598
+ }
2599
+ const patch = { id: targetId };
2600
+ const currentTab = (current.tabs ?? []).find(tab => tab.id === targetId);
2601
+ if (currentTab?.textLabel) {
2602
+ patch.textLabel = currentTab.textLabel;
2603
+ }
2604
+ for (const key of ['content', 'widgets']) {
2605
+ if (Array.isArray(input[key])) {
2606
+ patch[key] = input[key];
2607
+ }
2608
+ }
2609
+ return patch.content || patch.widgets ? patch : null;
2610
+ }
2611
+ summarizeOperations(operations, tabsPatch) {
2612
+ const summaries = [];
2613
+ for (const operationCandidate of operations) {
2614
+ const operation = this.toRecord(operationCandidate);
2615
+ if (!operation) {
2616
+ continue;
2617
+ }
2618
+ const operationId = this.toText(operation['operationId'] ?? operation['changeKind']);
2619
+ if (operationId === 'tab.add') {
2620
+ const tab = this.resolveTabAddInput(operation);
2621
+ if (tab?.textLabel) {
2622
+ summaries.push(this.t('ai.review.addTab', 'Adicionar aba: {{label}}.', { label: tab.textLabel }));
2623
+ }
2624
+ continue;
2625
+ }
2626
+ if (operationId === 'tab.label.set') {
2627
+ const textLabel = this.resolveTextLabel(operation);
2628
+ if (textLabel) {
2629
+ summaries.push(this.t('ai.review.renameTab', 'Renomear aba para: {{label}}.', { label: textLabel }));
2630
+ }
2631
+ continue;
2632
+ }
2633
+ if (operationId === 'tab.content.set') {
2634
+ const contentPatch = this.resolveTabContentInput(operation, this.getCurrentConfig());
2635
+ if (contentPatch?.id) {
2636
+ summaries.push(this.t('ai.review.setTabContent', 'Atualizar conteúdo da aba: {{label}}.', { label: contentPatch.textLabel || contentPatch.id }));
2637
+ }
2638
+ }
2639
+ }
2640
+ if (summaries.length === 1) {
2641
+ return summaries[0];
2642
+ }
2643
+ if (summaries.length > 1) {
2644
+ return this.t('ai.review.multipleAdjustments', 'Revisar {{count}} ajustes nas abas: {{items}}', { count: summaries.length, items: summaries.join(' ') });
2645
+ }
2646
+ if (tabsPatch.length === 1) {
2647
+ return this.t('ai.review.singleAdjustment', 'Revisar ajuste da aba: {{label}}.', { label: tabsPatch[0].textLabel || tabsPatch[0].id });
2648
+ }
2649
+ return tabsPatch.length > 1
2650
+ ? this.t('ai.review.multipleAdjustmentsShort', 'Revisar {{count}} ajustes nas abas.', { count: tabsPatch.length })
2651
+ : null;
2652
+ }
2653
+ t(key, fallback, params) {
2654
+ return this.translate ? this.translate(key, fallback, params) : this.interpolate(fallback, params);
2655
+ }
2656
+ interpolate(template, params) {
2657
+ if (!params) {
2658
+ return template;
2659
+ }
2660
+ return template.replace(/\{\{\s*([.\w-]+)\s*\}\}/g, (_, key) => String(params[key] ?? `{{${key}}}`));
2661
+ }
2662
+ toRecord(value) {
2663
+ return value && typeof value === 'object' && !Array.isArray(value)
2664
+ ? value
2665
+ : null;
2666
+ }
2667
+ toText(value) {
2668
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
2669
+ }
2670
+ normalizeToken(value) {
2671
+ return typeof value === 'string'
2672
+ ? value.trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '')
2673
+ : '';
2674
+ }
2675
+ }
2676
+
2677
+ class TabsAgenticAuthoringTurnFlow {
2678
+ adapter;
2679
+ aiApi;
2680
+ mode = 'agentic-authoring';
2681
+ constructor(adapter, aiApi) {
2682
+ this.adapter = adapter;
2683
+ this.aiApi = aiApi;
2684
+ }
2685
+ async submit(request) {
2686
+ const prompt = (request.prompt ?? '').trim();
2687
+ if (!prompt) {
2688
+ return {
2689
+ state: 'listening',
2690
+ phase: 'capture',
2691
+ statusText: '',
2692
+ };
2693
+ }
2694
+ const componentId = this.adapter.componentId || request.componentId || 'praxis-tabs';
2695
+ const componentType = this.adapter.componentType || request.componentType || 'tabs';
2696
+ const currentState = this.toAiJsonObject(this.adapter.getCurrentConfig());
2697
+ const dataProfile = this.optionalJsonObject(this.adapter.getDataProfile?.());
2698
+ const runtimeState = this.optionalJsonObject(this.adapter.getRuntimeState?.());
2699
+ const schemaFields = this.adapter.getSchemaFields?.()
2700
+ ?.map((field) => this.toAiJsonObject(field))
2701
+ .filter((field) => Object.keys(field).length > 0);
2702
+ const contextHints = this.optionalJsonObject(this.adapter.getAuthoringContext?.());
2703
+ if (this.shouldRouteToGovernedDecision(prompt, contextHints)) {
2704
+ return this.toGovernedDecisionHandoff(prompt, request);
2705
+ }
2706
+ const response = await firstValueFrom(this.aiApi.getPatch({
2707
+ componentId,
2708
+ componentType,
2709
+ userPrompt: prompt,
2710
+ sessionId: request.sessionId,
2711
+ clientTurnId: request.clientTurnId,
2712
+ messages: this.toChatMessages(request.messages, prompt),
2713
+ currentState,
2714
+ currentStateDigest: this.buildCurrentStateDigest(dataProfile),
2715
+ uiContextRef: {
2716
+ componentId,
2717
+ componentType,
2718
+ },
2719
+ ...(dataProfile ? { dataProfile } : {}),
2720
+ ...(runtimeState ? { runtimeState } : {}),
2721
+ ...(schemaFields?.length ? { schemaFields } : {}),
2722
+ ...(contextHints ? { contextHints } : {}),
2723
+ }));
2724
+ return this.toTurnResult(this.compileAdapterResponse(response), request);
2725
+ }
2726
+ async apply(request) {
2727
+ const patch = this.toRecord(request.pendingPatch);
2728
+ if (patch) {
2729
+ const result = await this.adapter.applyPatch(patch, request.prompt);
2730
+ if (!result.success) {
2731
+ return {
2732
+ state: 'error',
2733
+ phase: 'apply',
2734
+ assistantMessage: result.error || 'Nao foi possivel aplicar as alteracoes nas abas.',
2735
+ errorText: result.error || 'Nao foi possivel aplicar as alteracoes nas abas.',
2736
+ canApply: true,
2737
+ pendingPatch: patch,
2738
+ };
2739
+ }
2740
+ return {
2741
+ state: 'success',
2742
+ phase: 'summarize',
2743
+ assistantMessage: 'Alteracoes aplicadas nas abas.',
2744
+ statusText: 'Alteracoes aplicadas nas abas.',
2745
+ canApply: false,
2746
+ pendingPatch: null,
2747
+ diagnostics: result.warnings?.length ? { warnings: result.warnings } : undefined,
2748
+ };
2749
+ }
2750
+ return {
2751
+ state: 'error',
2752
+ phase: 'apply',
2753
+ assistantMessage: 'Nao ha alteracao de abas pronta para aplicar.',
2754
+ errorText: 'Nao ha alteracao de abas pronta para aplicar.',
2755
+ canApply: false,
2756
+ pendingPatch: null,
2757
+ };
2758
+ }
2759
+ cancel() {
2760
+ return Promise.resolve({
2761
+ state: 'listening',
2762
+ phase: 'capture',
2763
+ assistantMessage: 'Solicitacao cancelada.',
2764
+ statusText: '',
2765
+ canApply: false,
2766
+ pendingPatch: null,
2767
+ pendingClarification: null,
2768
+ });
2769
+ }
2770
+ retry(request) {
2771
+ const lastPrompt = [...(request.messages ?? [])].reverse()
2772
+ .find((message) => message.role === 'user')?.text;
2773
+ return this.submit({
2774
+ ...request,
2775
+ prompt: lastPrompt ?? request.prompt,
2776
+ action: { kind: 'retry' },
2777
+ });
2778
+ }
2779
+ toTurnResult(response, request) {
2780
+ if (!response) {
2781
+ return {
2782
+ state: 'error',
2783
+ phase: 'capture',
2784
+ assistantMessage: 'Resposta vazia da IA.',
2785
+ errorText: 'Resposta vazia da IA.',
2786
+ };
2787
+ }
2788
+ if (response.type === 'clarification') {
2789
+ return {
2790
+ state: 'clarification',
2791
+ phase: 'clarify',
2792
+ sessionId: response.sessionId ?? request.sessionId,
2793
+ assistantMessage: response.message || 'Preciso de mais detalhes para continuar.',
2794
+ clarificationQuestions: this.toClarificationQuestions(response),
2795
+ quickReplies: this.toQuickReplies(response),
2796
+ canApply: false,
2797
+ };
2798
+ }
2799
+ if (response.type === 'info') {
2800
+ const message = response.message || response.explanation || 'Informacao gerada.';
2801
+ return {
2802
+ state: 'success',
2803
+ phase: 'summarize',
2804
+ sessionId: response.sessionId ?? request.sessionId,
2805
+ assistantMessage: message,
2806
+ statusText: message,
2807
+ canApply: false,
2808
+ };
2809
+ }
2810
+ if (response.type === 'error') {
2811
+ const message = response.message || 'Falha ao gerar alteracao de abas.';
2812
+ return {
2813
+ state: 'error',
2814
+ phase: 'capture',
2815
+ sessionId: response.sessionId ?? request.sessionId,
2816
+ assistantMessage: message,
2817
+ errorText: message,
2818
+ canApply: false,
2819
+ pendingPatch: null,
2820
+ diagnostics: response.warnings?.length ? { warnings: response.warnings } : undefined,
2821
+ };
2822
+ }
2823
+ if (response.patch && Object.keys(response.patch).length > 0) {
2824
+ if (response.componentEditPlan) {
2825
+ const warnings = response.warnings?.filter(Boolean) ?? [];
2826
+ return {
2827
+ state: 'review',
2828
+ phase: 'review',
2829
+ sessionId: response.sessionId ?? request.sessionId,
2830
+ assistantMessage: response.explanation || 'Proposta de abas pronta para revisar.',
2831
+ statusText: 'Revise a proposta antes de aplicar.',
2832
+ canApply: true,
2833
+ pendingPatch: response.patch,
2834
+ preview: {
2835
+ kind: 'tabs-config-patch',
2836
+ diff: response.diff ?? [],
2837
+ },
2838
+ diagnostics: warnings.length ? { warnings } : undefined,
2839
+ };
2840
+ }
2841
+ return {
2842
+ state: 'error',
2843
+ phase: 'review',
2844
+ sessionId: response.sessionId ?? request.sessionId,
2845
+ assistantMessage: 'As abas rejeitaram patch livre. Gere um componentEditPlan validado pelo PRAXIS_TABS_AUTHORING_MANIFEST antes de propor alteracao local.',
2846
+ errorText: 'Patch livre de abas rejeitado.',
2847
+ canApply: false,
2848
+ pendingPatch: null,
2849
+ diagnostics: {
2850
+ warnings: [
2851
+ 'free-tabs-patch-rejected',
2852
+ 'Use componentEditPlan validado contra PRAXIS_TABS_AUTHORING_MANIFEST.',
2853
+ ],
2854
+ },
2855
+ };
2856
+ }
2857
+ return {
2858
+ state: 'success',
2859
+ phase: 'summarize',
2860
+ sessionId: response.sessionId ?? request.sessionId,
2861
+ assistantMessage: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
2862
+ statusText: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
2863
+ canApply: false,
2864
+ };
2865
+ }
2866
+ compileAdapterResponse(response) {
2867
+ const compiled = this.adapter.compileAiResponse?.(response);
2868
+ if (!compiled && response.patch && Object.keys(response.patch).length > 0) {
2869
+ return {
2870
+ type: 'error',
2871
+ message: 'Patch livre de abas rejeitado. Gere um componentEditPlan validado pelo manifesto antes de propor alteracao local.',
2872
+ warnings: [
2873
+ 'free-tabs-patch-rejected',
2874
+ 'Use componentEditPlan validado contra PRAXIS_TABS_AUTHORING_MANIFEST.',
2875
+ ],
2876
+ };
2877
+ }
2878
+ if (!compiled) {
2879
+ return response;
2880
+ }
2881
+ if (compiled.type === 'error') {
2882
+ return {
2883
+ type: 'error',
2884
+ message: compiled.message || 'O componentEditPlan das abas nao passou na validacao de capacidades.',
2885
+ warnings: compiled.warnings,
2886
+ };
2887
+ }
2888
+ const warnings = [
2889
+ ...(response.warnings ?? []),
2890
+ ...(compiled.warnings ?? []),
2891
+ ];
2892
+ return {
2893
+ ...response,
2894
+ ...compiled,
2895
+ patch: compiled.patch,
2896
+ warnings: warnings.length ? warnings : undefined,
2897
+ };
2898
+ }
2899
+ toChatMessages(messages, prompt) {
2900
+ const supported = (messages ?? [])
2901
+ .filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'system')
2902
+ .map((message) => ({
2903
+ role: message.role,
2904
+ content: message.text,
2905
+ }))
2906
+ .filter((message) => message.content.trim().length > 0);
2907
+ return supported.length ? supported : [{ role: 'user', content: prompt }];
2908
+ }
2909
+ toClarificationQuestions(response) {
2910
+ const labels = response.questions?.length
2911
+ ? response.questions
2912
+ : response.message
2913
+ ? [response.message]
2914
+ : ['Qual ajuste voce quer aplicar nas abas?'];
2915
+ const options = this.toQuickReplies(response).map((reply) => ({
2916
+ id: reply.id,
2917
+ label: reply.label,
2918
+ value: reply.prompt,
2919
+ }));
2920
+ return labels.map((label, index) => ({
2921
+ id: `tabs-clarification-${index + 1}`,
2922
+ type: options.length ? 'single-choice' : 'text',
2923
+ label,
2924
+ allowCustom: true,
2925
+ options,
2926
+ }));
2927
+ }
2928
+ toQuickReplies(response) {
2929
+ const payloads = response.optionPayloads ?? [];
2930
+ if (payloads.length) {
2931
+ return payloads
2932
+ .map((option, index) => {
2933
+ const label = option.label?.trim() || option.value?.trim() || `Opcao ${index + 1}`;
2934
+ const prompt = option.example?.trim() || option.value?.trim() || label;
2935
+ return {
2936
+ id: `option-${index + 1}`,
2937
+ label,
2938
+ prompt,
2939
+ kind: 'clarification-option',
2940
+ };
2941
+ });
2942
+ }
2943
+ return (response.options ?? [])
2944
+ .filter((option) => !!option?.trim())
2945
+ .map((option, index) => ({
2946
+ id: `option-${index + 1}`,
2947
+ label: option.trim(),
2948
+ prompt: option.trim(),
2949
+ kind: 'clarification-option',
2950
+ }));
2951
+ }
2952
+ buildCurrentStateDigest(dataProfile) {
2953
+ const tabCount = typeof dataProfile?.['tabCount'] === 'number' ? dataProfile['tabCount'] : undefined;
2954
+ const linkCount = typeof dataProfile?.['linkCount'] === 'number' ? dataProfile['linkCount'] : undefined;
2955
+ const rowCount = (tabCount ?? 0) + (linkCount ?? 0);
2956
+ return rowCount > 0 ? { rowCount } : {};
2957
+ }
2958
+ shouldRouteToGovernedDecision(prompt, contextHints) {
2959
+ return shouldRoutePromptToGovernedDecision(prompt, contextHints);
2960
+ }
2961
+ toGovernedDecisionHandoff(prompt, request) {
2962
+ const message = 'Esse pedido parece alterar uma decisao de negocio compartilhada. As abas podem ajudar a localizar a experiencia afetada, mas a regra deve seguir pelo fluxo governado de domain-rules antes de qualquer materializacao runtime.';
2963
+ return {
2964
+ state: 'clarification',
2965
+ phase: 'clarify',
2966
+ sessionId: request.sessionId,
2967
+ assistantMessage: message,
2968
+ statusText: 'Handoff governado necessario.',
2969
+ canApply: false,
2970
+ quickReplies: [
2971
+ {
2972
+ id: 'shared-rule-handoff',
2973
+ label: 'Continuar como regra governada',
2974
+ prompt,
2975
+ kind: 'shared-rule-handoff',
2976
+ description: 'Criar intake de domain-rules em vez de aplicar patch local nas abas.',
2977
+ icon: 'rule',
2978
+ tone: 'warning',
2979
+ contextHints: {
2980
+ flowId: 'shared_rule_authoring',
2981
+ source: 'praxis-tabs',
2982
+ recommendedAction: 'domain-rules/intake',
2983
+ },
2984
+ },
2985
+ ],
2986
+ clarificationQuestions: [
2987
+ {
2988
+ id: 'tabs-governed-rule-confirmation',
2989
+ type: 'confirm',
2990
+ label: 'Deseja continuar pelo fluxo governado de regras compartilhadas?',
2991
+ description: 'Esse caminho permite intake, simulacao, aprovacao/publicacao, materializacao e validacao de enforcement.',
2992
+ required: true,
2993
+ options: [
2994
+ {
2995
+ id: 'shared-rule-handoff',
2996
+ label: 'Sim, continuar governado',
2997
+ value: prompt,
2998
+ description: 'Nao aplicar como patch local das abas.',
2999
+ contextHints: {
3000
+ flowId: 'shared_rule_authoring',
3001
+ source: 'praxis-tabs',
3002
+ },
3003
+ },
3004
+ ],
3005
+ },
3006
+ ],
3007
+ diagnostics: {
3008
+ governedDecisionHandoff: {
3009
+ flowId: 'shared_rule_authoring',
3010
+ sourcePrompt: prompt,
3011
+ sourceComponent: 'praxis-tabs',
3012
+ },
3013
+ },
3014
+ };
3015
+ }
3016
+ optionalJsonObject(value) {
3017
+ if (value === undefined || value === null) {
3018
+ return undefined;
3019
+ }
3020
+ const object = this.toAiJsonObject(value);
3021
+ return Object.keys(object).length ? object : undefined;
3022
+ }
3023
+ toAiJsonObject(value) {
3024
+ const record = this.toRecord(value);
3025
+ if (!record) {
3026
+ return {};
3027
+ }
3028
+ try {
3029
+ return JSON.parse(JSON.stringify(record));
3030
+ }
3031
+ catch {
3032
+ return {};
3033
+ }
3034
+ }
3035
+ toRecord(value) {
3036
+ return value && typeof value === 'object' && !Array.isArray(value)
3037
+ ? value
3038
+ : null;
3039
+ }
2356
3040
  }
2357
3041
 
3042
+ const AGENTIC_PAGE_COMPOSITION_REQUEST_OUTPUT = 'agenticPageCompositionRequested';
2358
3043
  class PraxisTabs {
2359
3044
  i18n = inject(PraxisI18nService);
2360
3045
  settings = inject(SettingsPanelService);
@@ -2362,16 +3047,36 @@ class PraxisTabs {
2362
3047
  snack = inject(MatSnackBar);
2363
3048
  componentKeys = inject(ComponentKeyService);
2364
3049
  logger = inject(LoggerService);
3050
+ cdr = inject(ChangeDetectorRef);
3051
+ aiApi = inject(AiBackendApiService);
3052
+ assistantSessions = inject(PraxisAssistantSessionRegistryService);
3053
+ aiTurnOrchestrator = inject(PraxisAssistantTurnOrchestratorService);
2365
3054
  route = (() => { try {
2366
3055
  return inject(ActivatedRoute);
2367
3056
  }
2368
3057
  catch {
2369
3058
  return undefined;
2370
3059
  } })();
3060
+ aiAssistantSessionEffect = effect(() => {
3061
+ const session = this.assistantSessions.activeSession();
3062
+ if (!session || session.id !== this.resolveAiAssistantSessionId())
3063
+ return;
3064
+ if (!this.aiAssistantOpen) {
3065
+ this.openAiAssistantFromSession(session);
3066
+ }
3067
+ }, ...(ngDevMode ? [{ debugName: "aiAssistantSessionEffect" }] : []));
2371
3068
  warnedMissingId = false;
3069
+ loadedStorageKey = null;
2372
3070
  config = null;
2373
3071
  tabsId;
2374
3072
  componentInstanceId;
3073
+ configPersistenceStrategy = 'storage-first';
3074
+ set selectedIndex(index) {
3075
+ if (index == null)
3076
+ return;
3077
+ this.controlledSelectedIndex = index;
3078
+ this.applySelectedIndex(index, false, false);
3079
+ }
2375
3080
  enableCustomization = false;
2376
3081
  form = null;
2377
3082
  context = null;
@@ -2381,37 +3086,54 @@ class PraxisTabs {
2381
3086
  selectedTabChange = new EventEmitter();
2382
3087
  indexFocused = new EventEmitter();
2383
3088
  selectFocusedIndex = new EventEmitter();
3089
+ configChange = new EventEmitter();
2384
3090
  widgetEvent = new EventEmitter();
2385
- aiAdapter = new TabsAiAdapter(this);
3091
+ aiAdapter = new TabsAiAdapter(this, (key, fallback, params) => this.i18n.t(key, params, fallback, PRAXIS_TABS_I18N_NAMESPACE));
3092
+ aiAssistantOpen = false;
3093
+ aiAssistantPrompt = '';
3094
+ aiAssistantViewState = null;
3095
+ aiAssistantLayout = createPraxisAssistantViewportLayout();
3096
+ aiAssistantLabels = {
3097
+ title: 'Copiloto semantico Praxis',
3098
+ subtitle: 'Converse, revise e governe ajustes das abas.',
3099
+ prompt: 'Mensagem',
3100
+ promptPlaceholder: 'Descreva o ajuste que voce precisa nas abas.',
3101
+ emptyConversation: 'Diga o que voce quer alterar nas abas.',
3102
+ submit: 'Interpretar pedido',
3103
+ apply: 'Aplicar ajuste',
3104
+ };
3105
+ aiAssistantController = null;
3106
+ aiAssistantStateSubscription = null;
2386
3107
  // Signals to manage local state for selection in Nav mode and Group mode
2387
3108
  currentNavIndex = signal(0, ...(ngDevMode ? [{ debugName: "currentNavIndex" }] : []));
2388
3109
  selectedIndexSignal = signal(0, ...(ngDevMode ? [{ debugName: "selectedIndexSignal" }] : []));
2389
3110
  groupLoaded = new Set();
2390
3111
  navLoaded = new Set();
3112
+ controlledSelectedIndex;
2391
3113
  destroy$ = new Subject();
2392
3114
  widgetDefinitionCache = new WeakMap();
3115
+ generatedContentForm = new FormGroup({});
2393
3116
  ngOnInit() {
2394
3117
  this.syncSelectionFromConfig();
2395
- // Load stored config if tabsId provided
2396
- const key = this.storageKey();
2397
- if (key) {
2398
- this.storage.loadConfig(key).pipe(take(1)).subscribe((stored) => {
2399
- if (stored) {
2400
- this.config = stored;
2401
- }
2402
- this.syncSelectionFromConfig();
2403
- });
2404
- }
3118
+ this.syncGeneratedContentForm();
3119
+ this.loadStoredConfigForCurrentIdentity({ warnMissingId: false });
2405
3120
  }
2406
3121
  ngOnChanges(changes) {
3122
+ if (changes['tabsId'] || changes['componentInstanceId'] || changes['configPersistenceStrategy']) {
3123
+ this.loadStoredConfigForCurrentIdentity({ warnMissingId: false });
3124
+ }
2407
3125
  if (changes['config'] && this.config) {
2408
3126
  // Reset loaded caches on config change and seed with current selections
2409
3127
  this.syncSelectionFromConfig();
2410
- // Persist when tabsId provided
3128
+ this.syncGeneratedContentForm();
3129
+ // Persist when tabsId provided and the instance is storage-owned.
2411
3130
  this.persistConfig(this.config);
3131
+ this.reapplyControlledSelectedIndex();
2412
3132
  }
2413
3133
  }
2414
3134
  ngOnDestroy() {
3135
+ this.assistantSessions.removeContextSession(this.buildAiAssistantContextSnapshot().identity);
3136
+ this.aiAssistantStateSubscription?.unsubscribe();
2415
3137
  this.destroy$.next();
2416
3138
  this.destroy$.complete();
2417
3139
  }
@@ -2427,23 +3149,42 @@ class PraxisTabs {
2427
3149
  getNavActive(i) {
2428
3150
  return this.currentNavIndex() === i;
2429
3151
  }
3152
+ visibleNavLinkEntries() {
3153
+ return (this.config?.nav?.links ?? [])
3154
+ .map((link, index) => ({ link, index }))
3155
+ .filter((entry) => entry.link.visible !== false);
3156
+ }
3157
+ visibleTabEntries() {
3158
+ return (this.config?.tabs ?? [])
3159
+ .map((tab, index) => ({ tab, index }))
3160
+ .filter((entry) => entry.tab.visible !== false);
3161
+ }
3162
+ selectedVisibleNavIndex() {
3163
+ const entries = this.visibleNavLinkEntries();
3164
+ const index = entries.findIndex((entry) => entry.index === this.currentNavIndex());
3165
+ return index >= 0 ? index : 0;
3166
+ }
3167
+ selectedVisibleTabIndex() {
3168
+ const entries = this.visibleTabEntries();
3169
+ const index = entries.findIndex((entry) => entry.index === this.selectedIndexSignal());
3170
+ return index >= 0 ? index : 0;
3171
+ }
3172
+ effectiveContentForm() {
3173
+ return this.form || this.generatedContentForm;
3174
+ }
3175
+ onVisibleTabIndexChange(index) {
3176
+ const entry = this.visibleTabEntries()[index];
3177
+ if (!entry)
3178
+ return;
3179
+ this.onSelectedIndexChange(entry.index);
3180
+ }
2430
3181
  onNavClick(i) {
2431
3182
  if (!this.config?.nav?.links?.length)
2432
3183
  return;
2433
3184
  const linksCount = this.config.nav.links.length;
2434
3185
  if (i < 0 || i >= linksCount)
2435
3186
  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);
3187
+ this.applySelectedIndex(i, true);
2447
3188
  }
2448
3189
  onNavDrop(event) {
2449
3190
  if (!this.config?.nav?.links)
@@ -2491,10 +3232,39 @@ class PraxisTabs {
2491
3232
  });
2492
3233
  this.persistConfig(this.config);
2493
3234
  }
3235
+ onVisibleNavDrop(event) {
3236
+ const entries = this.visibleNavLinkEntries();
3237
+ const previous = entries[event.previousIndex];
3238
+ const current = entries[event.currentIndex];
3239
+ if (!previous || !current)
3240
+ return;
3241
+ this.onNavDrop({
3242
+ ...event,
3243
+ previousIndex: previous.index,
3244
+ currentIndex: current.index,
3245
+ });
3246
+ }
2494
3247
  onSelectedIndexChange(index) {
3248
+ this.applySelectedIndex(index, true);
3249
+ }
3250
+ applySelectedIndex(index, emit, persist = true) {
3251
+ if (this.isNavMode() && this.config) {
3252
+ const selected = this.clampIndex(index, this.config?.nav?.links?.length ?? 0);
3253
+ this.currentNavIndex.set(selected);
3254
+ this.config = produce(this.config, (draft) => {
3255
+ draft.nav.selectedIndex = selected;
3256
+ });
3257
+ if (persist) {
3258
+ this.persistConfig(this.config);
3259
+ }
3260
+ this.navLoaded.add(selected);
3261
+ if (emit) {
3262
+ this.selectedIndexChange.emit(selected);
3263
+ }
3264
+ return;
3265
+ }
2495
3266
  const selected = this.clampIndex(index, this.config?.tabs?.length ?? 0);
2496
3267
  this.selectedIndexSignal.set(selected);
2497
- // Update config immutably
2498
3268
  if (this.config) {
2499
3269
  this.config = produce(this.config, (draft) => {
2500
3270
  if (!draft.group) {
@@ -2504,11 +3274,20 @@ class PraxisTabs {
2504
3274
  draft.group.selectedIndex = selected;
2505
3275
  }
2506
3276
  });
2507
- this.persistConfig(this.config);
3277
+ if (persist) {
3278
+ this.persistConfig(this.config);
3279
+ }
2508
3280
  }
2509
- // Lazy: mark as loaded
2510
3281
  this.groupLoaded.add(selected);
2511
- this.selectedIndexChange.emit(selected);
3282
+ if (emit) {
3283
+ this.selectedIndexChange.emit(selected);
3284
+ }
3285
+ }
3286
+ reapplyControlledSelectedIndex() {
3287
+ if (this.controlledSelectedIndex == null) {
3288
+ return;
3289
+ }
3290
+ this.applySelectedIndex(this.controlledSelectedIndex, false, false);
2512
3291
  }
2513
3292
  closeTab(index) {
2514
3293
  if (!this.config?.tabs)
@@ -2596,6 +3375,311 @@ class PraxisTabs {
2596
3375
  ref.applied$.pipe(takeUntil(this.destroy$)).subscribe(applyDocument);
2597
3376
  ref.saved$.pipe(takeUntil(this.destroy$)).subscribe(applyDocument);
2598
3377
  }
3378
+ openAiAssistant() {
3379
+ this.initializeAiAssistantController();
3380
+ this.aiAssistantOpen = true;
3381
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
3382
+ this.syncAiAssistantSession('active');
3383
+ this.cdr.markForCheck();
3384
+ }
3385
+ openAiAssistantFromSession(session) {
3386
+ if (session.id !== this.resolveAiAssistantSessionId())
3387
+ return;
3388
+ this.initializeAiAssistantController();
3389
+ this.aiAssistantOpen = true;
3390
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
3391
+ this.syncAiAssistantSession('active');
3392
+ this.cdr.markForCheck();
3393
+ }
3394
+ closeAiAssistant() {
3395
+ this.aiAssistantOpen = false;
3396
+ this.syncAiAssistantSession('minimized');
3397
+ this.cdr.markForCheck();
3398
+ }
3399
+ onAiAssistantPromptChange(prompt) {
3400
+ this.aiAssistantPrompt = prompt;
3401
+ this.syncAiAssistantSession();
3402
+ }
3403
+ onAiAssistantSubmit(prompt) {
3404
+ if (this.shouldDelegateAiAssistantPromptToPageBuilder(prompt)) {
3405
+ this.emitAgenticPageCompositionRequest(prompt);
3406
+ this.aiAssistantPrompt = '';
3407
+ this.closeAiAssistant();
3408
+ return;
3409
+ }
3410
+ this.aiAssistantController?.submitPrompt(prompt).subscribe((state) => {
3411
+ this.aiAssistantPrompt = '';
3412
+ this.aiAssistantViewState = state;
3413
+ this.syncAiAssistantSession();
3414
+ this.cdr.markForCheck();
3415
+ });
3416
+ }
3417
+ onAiAssistantApply() {
3418
+ this.aiAssistantController?.apply().subscribe((state) => {
3419
+ this.aiAssistantViewState = state;
3420
+ this.syncAiAssistantSession();
3421
+ this.cdr.markForCheck();
3422
+ });
3423
+ }
3424
+ onAiAssistantRetry() {
3425
+ this.aiAssistantController?.retry().subscribe((state) => {
3426
+ this.aiAssistantViewState = state;
3427
+ this.syncAiAssistantSession();
3428
+ this.cdr.markForCheck();
3429
+ });
3430
+ }
3431
+ onAiAssistantCancel() {
3432
+ this.aiAssistantController?.cancel().subscribe((state) => {
3433
+ this.aiAssistantPrompt = '';
3434
+ this.aiAssistantViewState = state;
3435
+ this.syncAiAssistantSession();
3436
+ this.cdr.markForCheck();
3437
+ });
3438
+ }
3439
+ onAiAssistantQuickReply(reply) {
3440
+ const controller = this.aiAssistantController;
3441
+ if (!controller)
3442
+ return;
3443
+ const state = controller.snapshot();
3444
+ const next$ = state.state === 'clarification'
3445
+ ? controller.answerClarification(reply.prompt)
3446
+ : controller.submitPrompt(reply.prompt, {
3447
+ kind: reply.kind || 'quick-reply',
3448
+ id: reply.id,
3449
+ value: reply.prompt,
3450
+ });
3451
+ next$.subscribe((nextState) => {
3452
+ this.aiAssistantPrompt = '';
3453
+ this.aiAssistantViewState = nextState;
3454
+ this.syncAiAssistantSession();
3455
+ this.cdr.markForCheck();
3456
+ });
3457
+ }
3458
+ onAiAssistantEditMessage(message) {
3459
+ this.aiAssistantPrompt = message.text;
3460
+ this.cdr.markForCheck();
3461
+ }
3462
+ onAiAssistantResendMessage(message) {
3463
+ this.aiAssistantController?.resendMessage(message.id).subscribe((state) => {
3464
+ this.aiAssistantPrompt = '';
3465
+ this.aiAssistantViewState = state;
3466
+ this.syncAiAssistantSession();
3467
+ this.cdr.markForCheck();
3468
+ });
3469
+ }
3470
+ onAiAssistantLayoutChange(layout) {
3471
+ this.aiAssistantLayout = layout;
3472
+ }
3473
+ initializeAiAssistantController() {
3474
+ if (this.aiAssistantController)
3475
+ return;
3476
+ const flow = new TabsAgenticAuthoringTurnFlow(this.aiAdapter, this.aiApi);
3477
+ const controller = this.aiTurnOrchestrator.createController(flow, {
3478
+ componentId: this.aiAdapter.componentId || 'praxis-tabs',
3479
+ componentType: this.aiAdapter.componentType || 'tabs',
3480
+ contextItems: this.buildAiAssistantContextItems(),
3481
+ });
3482
+ this.aiAssistantController = controller;
3483
+ this.aiAssistantViewState = controller.snapshot();
3484
+ this.aiAssistantStateSubscription?.unsubscribe();
3485
+ this.aiAssistantStateSubscription = controller.state$.subscribe((state) => {
3486
+ this.aiAssistantViewState = state;
3487
+ this.syncAiAssistantSession();
3488
+ this.cdr.markForCheck();
3489
+ });
3490
+ this.cdr.markForCheck();
3491
+ }
3492
+ buildAiAssistantContextItems() {
3493
+ const items = [
3494
+ {
3495
+ id: 'component',
3496
+ label: 'Componente',
3497
+ value: 'Abas',
3498
+ kind: 'component',
3499
+ icon: 'tab',
3500
+ },
3501
+ {
3502
+ id: 'mode',
3503
+ label: 'Modo',
3504
+ value: this.isNavMode() ? 'nav' : 'group',
3505
+ kind: 'custom',
3506
+ icon: 'account_tree',
3507
+ },
3508
+ ];
3509
+ if (this.tabsId) {
3510
+ items.push({
3511
+ id: 'tabs-id',
3512
+ label: 'Tabs',
3513
+ value: this.tabsId,
3514
+ kind: 'custom',
3515
+ icon: 'tag',
3516
+ });
3517
+ }
3518
+ return items;
3519
+ }
3520
+ buildAiAssistantContextSnapshot() {
3521
+ const fieldNames = this.collectContentFieldNames();
3522
+ const counts = this.collectTabsCounts();
3523
+ return {
3524
+ identity: {
3525
+ sessionId: this.resolveAiAssistantSessionId(),
3526
+ ownerId: this.resolveAiAssistantOwnerId(),
3527
+ ownerType: 'tabs',
3528
+ componentId: 'praxis-tabs',
3529
+ componentType: 'tabs',
3530
+ routeKey: this.resolveAiAssistantRouteKey(),
3531
+ },
3532
+ target: {
3533
+ kind: 'component',
3534
+ id: this.resolveAiAssistantOwnerId(),
3535
+ label: this.tabsId || 'Abas',
3536
+ metadata: {
3537
+ mode: this.isNavMode() ? 'nav' : 'group',
3538
+ hasCustomization: !!this.enableCustomization,
3539
+ },
3540
+ },
3541
+ contextItems: this.buildAiAssistantContextItems().map((item) => ({
3542
+ id: item.id,
3543
+ label: item.label,
3544
+ value: item.value || '',
3545
+ kind: item.kind,
3546
+ })),
3547
+ mode: 'agentic-authoring',
3548
+ authoringManifestRef: {
3549
+ componentId: 'praxis-tabs',
3550
+ source: 'PRAXIS_TABS_AUTHORING_MANIFEST',
3551
+ },
3552
+ schemaFields: fieldNames.length ? fieldNames : undefined,
3553
+ dataProfileDigest: {
3554
+ summary: `${counts.tabCount} aba(s), ${counts.linkCount} link(s), ${counts.widgetCount} widget(s), ${counts.fieldCount} campo(s)`,
3555
+ counts,
3556
+ },
3557
+ runtimeStateDigest: {
3558
+ summary: this.isNavMode()
3559
+ ? `Nav ativo no indice ${this.currentNavIndex()}`
3560
+ : `Grupo ativo no indice ${this.selectedIndexSignal()}`,
3561
+ fields: [
3562
+ this.isNavMode() ? 'nav.links' : 'tabs',
3563
+ 'selectedIndex',
3564
+ 'widgetEvent',
3565
+ ],
3566
+ },
3567
+ capabilityRefs: [
3568
+ {
3569
+ id: 'tabs.component-edit-plan',
3570
+ label: 'Plano de edicao de abas',
3571
+ source: 'PRAXIS_TABS_AUTHORING_MANIFEST',
3572
+ risk: 'medium',
3573
+ },
3574
+ ],
3575
+ governanceHints: [
3576
+ {
3577
+ kind: 'business-rule-boundary',
3578
+ label: 'Regras compartilhadas exigem governanca',
3579
+ reason: 'Politicas, validacoes reutilizaveis e compliance nao devem ser aplicados como patch local das abas.',
3580
+ risk: 'high',
3581
+ },
3582
+ ],
3583
+ };
3584
+ }
3585
+ syncAiAssistantSession(visibility = null) {
3586
+ if (!this.enableCustomization)
3587
+ return;
3588
+ if (!this.aiAssistantOpen && !this.hasAiAssistantSessionState())
3589
+ return;
3590
+ const state = this.aiAssistantViewState;
3591
+ this.assistantSessions.upsertContextSession(this.buildAiAssistantContextSnapshot(), {
3592
+ title: 'Copiloto semantico Praxis',
3593
+ summary: this.resolveAiAssistantSummary(),
3594
+ mode: state?.mode || 'agentic-authoring',
3595
+ state: state?.state || 'idle',
3596
+ visibility: visibility ?? (this.aiAssistantOpen ? 'active' : 'minimized'),
3597
+ badge: this.resolveAiAssistantBadge(),
3598
+ icon: this.resolveAiAssistantIcon(),
3599
+ });
3600
+ }
3601
+ hasAiAssistantSessionState() {
3602
+ return !!this.aiAssistantPrompt.trim()
3603
+ || !!this.aiAssistantViewState?.messages?.length
3604
+ || !!this.aiAssistantViewState?.quickReplies?.length
3605
+ || !!this.aiAssistantViewState?.pendingPatch
3606
+ || !!this.aiAssistantViewState?.statusText?.trim()
3607
+ || !!this.aiAssistantViewState?.errorText?.trim();
3608
+ }
3609
+ resolveAiAssistantSessionId() {
3610
+ return `tabs:${this.resolveAiAssistantRouteKey()}:${this.resolveAiAssistantOwnerId()}`;
3611
+ }
3612
+ resolveAiAssistantOwnerId() {
3613
+ return (this.componentInstanceId || this.tabsId || 'tabs').trim() || 'tabs';
3614
+ }
3615
+ resolveAiAssistantRouteKey() {
3616
+ const routePath = this.route?.snapshot?.routeConfig?.path?.trim();
3617
+ return routePath || 'local';
3618
+ }
3619
+ resolveAiAssistantSummary() {
3620
+ const status = this.aiAssistantViewState?.statusText?.trim();
3621
+ if (status)
3622
+ return status;
3623
+ const error = this.aiAssistantViewState?.errorText?.trim();
3624
+ if (error)
3625
+ return error;
3626
+ const prompt = this.aiAssistantPrompt.trim();
3627
+ if (prompt)
3628
+ return prompt.length > 96 ? `${prompt.slice(0, 93)}...` : prompt;
3629
+ const lastMessage = [...(this.aiAssistantViewState?.messages ?? [])].reverse()
3630
+ .find((message) => message.role === 'assistant' || message.role === 'user');
3631
+ if (lastMessage?.text) {
3632
+ return lastMessage.text.length > 96 ? `${lastMessage.text.slice(0, 93)}...` : lastMessage.text;
3633
+ }
3634
+ return this.isNavMode() ? 'Assistente contextual das abas de navegacao.' : 'Assistente contextual do grupo de abas.';
3635
+ }
3636
+ resolveAiAssistantBadge() {
3637
+ const state = this.aiAssistantViewState?.state;
3638
+ if (state === 'error')
3639
+ return 'erro';
3640
+ if (state === 'clarification')
3641
+ return 'revisar';
3642
+ if (state === 'review')
3643
+ return 'preview';
3644
+ if (state === 'success')
3645
+ return 'ok';
3646
+ return undefined;
3647
+ }
3648
+ resolveAiAssistantIcon() {
3649
+ const state = this.aiAssistantViewState?.state;
3650
+ if (state === 'error')
3651
+ return 'error';
3652
+ if (state === 'clarification')
3653
+ return 'rule';
3654
+ if (state === 'review')
3655
+ return 'rate_review';
3656
+ return 'auto_awesome';
3657
+ }
3658
+ collectContentFieldNames() {
3659
+ const fields = [
3660
+ ...(this.config?.tabs ?? []).flatMap((tab) => tab.content ?? []),
3661
+ ...(this.config?.nav?.links ?? []).flatMap((link) => link.content ?? []),
3662
+ ];
3663
+ return Array.from(new Set(fields
3664
+ .map((field) => field?.name || field?.key || field?.id)
3665
+ .filter((name) => typeof name === 'string' && !!name.trim())));
3666
+ }
3667
+ collectTabsCounts() {
3668
+ const tabs = this.config?.tabs ?? [];
3669
+ const links = this.config?.nav?.links ?? [];
3670
+ return {
3671
+ tabCount: tabs.length,
3672
+ linkCount: links.length,
3673
+ widgetCount: [
3674
+ ...tabs.flatMap((tab) => tab.widgets ?? []),
3675
+ ...links.flatMap((link) => link.widgets ?? []),
3676
+ ].length,
3677
+ fieldCount: [
3678
+ ...tabs.flatMap((tab) => tab.content ?? []),
3679
+ ...links.flatMap((link) => link.content ?? []),
3680
+ ].length,
3681
+ };
3682
+ }
2599
3683
  addEmptyTab() {
2600
3684
  const next = produce(this.config || {}, (draft) => {
2601
3685
  if (!draft.group)
@@ -2609,6 +3693,7 @@ class PraxisTabs {
2609
3693
  draft.group.selectedIndex = (draft.tabs.length || 1) - 1;
2610
3694
  });
2611
3695
  this.config = next;
3696
+ this.syncGeneratedContentForm();
2612
3697
  this.persistConfig(this.config);
2613
3698
  this.selectedIndexSignal.set(this.config?.group?.selectedIndex ?? 0);
2614
3699
  }
@@ -2670,6 +3755,7 @@ class PraxisTabs {
2670
3755
  }
2671
3756
  this.config = plan.canonicalConfig;
2672
3757
  this.syncSelectionFromConfig();
3758
+ this.syncGeneratedContentForm();
2673
3759
  if (plan.runtime.rebuildLazyState) {
2674
3760
  this.groupLoaded.clear();
2675
3761
  this.navLoaded.clear();
@@ -2686,31 +3772,60 @@ class PraxisTabs {
2686
3772
  this.persistConfig(this.config);
2687
3773
  }
2688
3774
  }
2689
- storageKey() {
2690
- const id = this.componentKeyId();
3775
+ storageKey(options = {}) {
3776
+ const id = this.componentKeyId(options);
2691
3777
  return id ? `tabs:${id}` : null;
2692
3778
  }
3779
+ loadStoredConfigForCurrentIdentity(options = {}) {
3780
+ if (this.usesInputFirstConfig()) {
3781
+ return;
3782
+ }
3783
+ const key = this.storageKey(options);
3784
+ if (!key || key === this.loadedStorageKey)
3785
+ return;
3786
+ this.loadedStorageKey = key;
3787
+ this.storage.loadConfig(key).pipe(take(1)).subscribe((stored) => {
3788
+ if (stored) {
3789
+ this.config = stored;
3790
+ this.syncGeneratedContentForm();
3791
+ }
3792
+ this.syncSelectionFromConfig();
3793
+ this.reapplyControlledSelectedIndex();
3794
+ });
3795
+ }
2693
3796
  syncSelectionFromConfig() {
2694
- this.groupLoaded.clear();
2695
- this.navLoaded.clear();
2696
3797
  const tabsLength = this.config?.tabs?.length ?? 0;
2697
3798
  const linksLength = this.config?.nav?.links?.length ?? 0;
2698
3799
  const groupIndex = this.clampIndex(this.config?.group?.selectedIndex, tabsLength);
2699
3800
  const navIndex = this.clampIndex(this.config?.nav?.selectedIndex, linksLength);
2700
3801
  this.selectedIndexSignal.set(groupIndex);
2701
3802
  this.currentNavIndex.set(navIndex);
3803
+ this.pruneLoadedIndexes(this.groupLoaded, tabsLength);
3804
+ this.pruneLoadedIndexes(this.navLoaded, linksLength);
2702
3805
  if (tabsLength > 0)
2703
3806
  this.groupLoaded.add(groupIndex);
2704
3807
  if (linksLength > 0)
2705
3808
  this.navLoaded.add(navIndex);
2706
3809
  }
3810
+ pruneLoadedIndexes(indexes, size) {
3811
+ for (const index of Array.from(indexes)) {
3812
+ if (index < 0 || index >= size) {
3813
+ indexes.delete(index);
3814
+ }
3815
+ }
3816
+ }
2707
3817
  persistConfig(config) {
3818
+ if (this.usesInputFirstConfig())
3819
+ return;
2708
3820
  const key = this.storageKey();
2709
3821
  if (!key || !config)
2710
3822
  return;
2711
3823
  this.storage.saveConfig(key, config).pipe(take(1)).subscribe({ error: () => { } });
2712
3824
  }
2713
- componentKeyId() {
3825
+ usesInputFirstConfig() {
3826
+ return this.configPersistenceStrategy === 'input-first' && !!this.config;
3827
+ }
3828
+ componentKeyId(options = {}) {
2714
3829
  const key = this.componentKeys.buildComponentId({
2715
3830
  componentType: 'praxis-tabs',
2716
3831
  componentId: this.tabsId,
@@ -2719,7 +3834,7 @@ class PraxisTabs {
2719
3834
  route: this.route,
2720
3835
  requireComponentId: true,
2721
3836
  });
2722
- if (!key)
3837
+ if (!key && options.warnMissingId !== false)
2723
3838
  this.warnMissingId();
2724
3839
  return key;
2725
3840
  }
@@ -2749,7 +3864,9 @@ class PraxisTabs {
2749
3864
  applyConfigFromAdapter(next) {
2750
3865
  this.config = next;
2751
3866
  this.syncSelectionFromConfig();
3867
+ this.syncGeneratedContentForm();
2752
3868
  this.persistConfig(this.config);
3869
+ this.configChange.emit({ inputPatch: { config: this.cloneTabsMetadata(this.config) } });
2753
3870
  }
2754
3871
  // =====================
2755
3872
  // Lazy load helpers
@@ -2764,10 +3881,16 @@ class PraxisTabs {
2764
3881
  return !this.isLazy() || this.navLoaded.has(index) || this.currentNavIndex() === index;
2765
3882
  }
2766
3883
  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);
3884
+ const hasTabs = this.visibleTabEntries().length > 0;
3885
+ const hasLinks = this.visibleNavLinkEntries().length > 0;
2769
3886
  return !(hasTabs || hasLinks);
2770
3887
  }
3888
+ trackVisibleNavLink(index, entry) {
3889
+ return entry.link.id || `${entry.link.label || 'nav-link'}:${entry.index ?? index}`;
3890
+ }
3891
+ trackVisibleTab(index, entry) {
3892
+ return entry.tab.id || entry.tab.textLabel || `tab:${entry.index ?? index}`;
3893
+ }
2771
3894
  trackNavLink(index, link) {
2772
3895
  return link.id || `${link.label || 'nav-link'}:${index}`;
2773
3896
  }
@@ -2777,6 +3900,12 @@ class PraxisTabs {
2777
3900
  trackWidgetDefinition(index, widget) {
2778
3901
  return widget.childWidgetKey || widget.id || `widget:${index}`;
2779
3902
  }
3903
+ safeWidgetDefinitions(widgets) {
3904
+ if (!Array.isArray(widgets)) {
3905
+ return [];
3906
+ }
3907
+ return widgets.filter((widget) => (!!widget && typeof widget === 'object'));
3908
+ }
2780
3909
  resolveWidgetDefinition(widget) {
2781
3910
  const cached = this.widgetDefinitionCache.get(widget);
2782
3911
  if (cached) {
@@ -2786,12 +3915,67 @@ class PraxisTabs {
2786
3915
  this.widgetDefinitionCache.set(widget, clone);
2787
3916
  return clone;
2788
3917
  }
3918
+ syncGeneratedContentForm() {
3919
+ if (this.form) {
3920
+ return;
3921
+ }
3922
+ for (const field of this.collectContentFields()) {
3923
+ const name = this.resolveContentFieldName(field);
3924
+ if (!name || this.generatedContentForm.contains(name)) {
3925
+ continue;
3926
+ }
3927
+ this.generatedContentForm.addControl(name, new FormControl(field?.defaultValue ?? null));
3928
+ }
3929
+ }
3930
+ collectContentFields() {
3931
+ return [
3932
+ ...((this.config?.tabs ?? []).flatMap((tab) => tab.content ?? [])),
3933
+ ...((this.config?.nav?.links ?? []).flatMap((link) => link.content ?? [])),
3934
+ ];
3935
+ }
3936
+ resolveContentFieldName(field) {
3937
+ const name = field?.name ?? field?.key ?? field?.id;
3938
+ return typeof name === 'string' && name.trim() ? name.trim() : null;
3939
+ }
2789
3940
  emitWidgetEvent(path, ev) {
2790
3941
  this.widgetEvent.emit({
2791
3942
  ...ev,
2792
3943
  path: [...path, ...(ev.path || [])],
2793
3944
  });
2794
3945
  }
3946
+ emitAgenticPageCompositionRequest(prompt) {
3947
+ this.widgetEvent.emit({
3948
+ sourceComponentId: 'praxis-tabs',
3949
+ output: AGENTIC_PAGE_COMPOSITION_REQUEST_OUTPUT,
3950
+ payload: {
3951
+ prompt,
3952
+ source: 'praxis-tabs',
3953
+ reason: 'nested-page-composition-request',
3954
+ },
3955
+ path: [{ kind: 'tabs', id: this.tabsId }],
3956
+ });
3957
+ }
3958
+ shouldDelegateAiAssistantPromptToPageBuilder(prompt) {
3959
+ const normalized = this.normalizeAssistantPrompt(prompt);
3960
+ if (!normalized)
3961
+ return false;
3962
+ const hasCompositionVerb = /\b(ajust\w*|adicion\w*|inclu\w*|cri\w*|mont\w*|complet\w*|preench\w*|use|usar|organiz\w*)\b/.test(normalized);
3963
+ const hasTabsContainer = /\b(abas?|tabs?|cadastro|registros?|acompanhamento|solicitacoes|workspace)\b/.test(normalized);
3964
+ const hasNestedWidget = /\b(formularios?|forms?|listas?|listagem|crud|cards?|tabelas?|componentes?|widgets?)\b/.test(normalized);
3965
+ const hasStructuredContent = /\b(campos?|colunas?|sla|historico|responsavel|prioridade|prazo|titulo|status|item)\b/.test(normalized);
3966
+ const hasLocalEditorialBoundary = /\b(local|editorial|fictici\w*|mock|sem api real|nao conecte api|sem conectar api|sem schema externo)\b/.test(normalized);
3967
+ return hasCompositionVerb
3968
+ && hasTabsContainer
3969
+ && (hasNestedWidget || (hasLocalEditorialBoundary && hasStructuredContent));
3970
+ }
3971
+ normalizeAssistantPrompt(prompt) {
3972
+ return (prompt || '')
3973
+ .normalize('NFD')
3974
+ .replace(/[\u0300-\u036f]/g, '')
3975
+ .toLowerCase()
3976
+ .replace(/\s+/g, ' ')
3977
+ .trim();
3978
+ }
2795
3979
  tabEventPath(tabId, tabIndex) {
2796
3980
  return [
2797
3981
  { kind: 'tabs', id: this.tabsId },
@@ -2951,8 +4135,17 @@ class PraxisTabs {
2951
4135
  catch { }
2952
4136
  return JSON.parse(JSON.stringify(widget));
2953
4137
  }
4138
+ cloneTabsMetadata(config) {
4139
+ try {
4140
+ if (typeof structuredClone === 'function') {
4141
+ return structuredClone(config);
4142
+ }
4143
+ }
4144
+ catch { }
4145
+ return JSON.parse(JSON.stringify(config));
4146
+ }
2954
4147
  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: `
4148
+ 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", configPersistenceStrategy: "configPersistenceStrategy", selectedIndex: "selectedIndex", enableCustomization: "enableCustomization", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", configChange: "configChange", widgetEvent: "widgetEvent" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], usesOnChanges: true, ngImport: i0, template: `
2956
4149
  <div
2957
4150
  class="praxis-tabs-root"
2958
4151
  [class.density-compact]="config?.appearance?.density === 'compact'"
@@ -2969,9 +4162,51 @@ class PraxisTabs {
2969
4162
  <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
2970
4163
 
2971
4164
  <div class="tabs-ai-assistant" *ngIf="enableCustomization">
2972
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
4165
+ <button
4166
+ mat-mini-fab
4167
+ type="button"
4168
+ color="primary"
4169
+ class="tabs-ai-assistant-trigger"
4170
+ (click)="openAiAssistant()"
4171
+ matTooltip="Copiloto semantico Praxis"
4172
+ aria-label="Abrir copiloto semantico Praxis das abas"
4173
+ data-testid="praxis-tabs-ai-assistant-trigger"
4174
+ >
4175
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
4176
+ </button>
2973
4177
  </div>
2974
4178
 
4179
+ <praxis-ai-assistant-shell
4180
+ *ngIf="aiAssistantOpen && aiAssistantViewState"
4181
+ [labels]="aiAssistantLabels"
4182
+ [mode]="aiAssistantViewState.mode"
4183
+ [state]="aiAssistantViewState.state"
4184
+ [contextItems]="aiAssistantViewState.contextItems"
4185
+ [attachments]="aiAssistantViewState.attachments"
4186
+ [messages]="aiAssistantViewState.messages"
4187
+ [quickReplies]="aiAssistantViewState.quickReplies"
4188
+ [prompt]="aiAssistantPrompt"
4189
+ [statusText]="aiAssistantViewState.statusText"
4190
+ [errorText]="aiAssistantViewState.errorText"
4191
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
4192
+ [canApply]="aiAssistantViewState.canApply"
4193
+ [layout]="aiAssistantLayout"
4194
+ testIdPrefix="praxis-tabs-ai-assistant"
4195
+ panelTestId="praxis-tabs-ai-assistant-panel"
4196
+ submitTestId="praxis-tabs-ai-assistant-submit"
4197
+ applyTestId="praxis-tabs-ai-assistant-apply"
4198
+ (promptChange)="onAiAssistantPromptChange($event)"
4199
+ (submitPrompt)="onAiAssistantSubmit($event)"
4200
+ (apply)="onAiAssistantApply()"
4201
+ (retryTurn)="onAiAssistantRetry()"
4202
+ (cancelTurn)="onAiAssistantCancel()"
4203
+ (quickReply)="onAiAssistantQuickReply($event)"
4204
+ (editMessage)="onAiAssistantEditMessage($event)"
4205
+ (resendMessage)="onAiAssistantResendMessage($event)"
4206
+ (layoutChange)="onAiAssistantLayoutChange($event)"
4207
+ (close)="closeAiAssistant()"
4208
+ ></praxis-ai-assistant-shell>
4209
+
2975
4210
  <!-- Empty state (global) -->
2976
4211
  <ng-container *ngIf="isEmptyGlobal(); else notEmpty">
2977
4212
  <praxis-empty-state-card
@@ -2995,13 +4230,13 @@ class PraxisTabs {
2995
4230
  cdkDropList
2996
4231
  cdkDropListOrientation="horizontal"
2997
4232
  [cdkDropListDisabled]="!config?.behavior?.reorderable"
2998
- (cdkDropListDropped)="onNavDrop($event)"
4233
+ (cdkDropListDropped)="onVisibleNavDrop($event)"
2999
4234
  [disablePagination]="config?.nav?.disablePagination"
3000
4235
  [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
3001
4236
  [mat-stretch-tabs]="config?.nav?.stretchTabs"
3002
4237
  [color]="config?.nav?.color"
3003
4238
  [backgroundColor]="config?.nav?.backgroundColor"
3004
- [selectedIndex]="currentNavIndex()"
4239
+ [selectedIndex]="selectedVisibleNavIndex()"
3005
4240
  [attr.aria-label]="config?.nav?.ariaLabel || config?.group?.ariaLabel || null"
3006
4241
  [attr.aria-labelledby]="config?.nav?.ariaLabelledby || config?.group?.ariaLabelledby || null"
3007
4242
  [animationDuration]="effectiveAnimationDuration()"
@@ -3010,21 +4245,22 @@ class PraxisTabs {
3010
4245
  >
3011
4246
  <a
3012
4247
  mat-tab-link
3013
- *ngFor="let link of config?.nav?.links; let i = index; trackBy: trackNavLink"
4248
+ *ngFor="let entry of visibleNavLinkEntries(); let i = index; trackBy: trackVisibleNavLink"
3014
4249
  cdkDrag
3015
4250
  [cdkDragDisabled]="!config?.behavior?.reorderable"
3016
4251
  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)"
4252
+ [active]="getNavActive(entry.index)"
4253
+ [disabled]="entry.link.disabled"
4254
+ [disableRipple]="config?.nav?.disableRipple || entry.link.disableRipple"
4255
+ [fitInkBarToContent]="entry.link.fitInkBarToContent || false"
4256
+ [id]="entry.link.id || ''"
4257
+ (click)="onNavClick(entry.index)"
3023
4258
  >
3024
4259
  <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
3025
4260
  <mat-icon fontIcon="drag_indicator"></mat-icon>
3026
4261
  </span>
3027
- {{ link.label }}
4262
+ <mat-icon *ngIf="entry.link.icon" class="tab-label-icon" [praxisIcon]="entry.link.icon"></mat-icon>
4263
+ {{ entry.link.label }}
3028
4264
  </a>
3029
4265
  </nav>
3030
4266
 
@@ -3033,16 +4269,16 @@ class PraxisTabs {
3033
4269
  <ng-container
3034
4270
  *ngIf="config?.nav?.links?.[currentNavIndex()] as l"
3035
4271
  >
3036
- <ng-container *ngIf="(l.content?.length || l.widgets?.length) && navContentReady(currentNavIndex()); else emptyNav">
3037
- <ng-container *ngIf="l.content && form">
4272
+ <ng-container *ngIf="(l.content?.length || safeWidgetDefinitions(l.widgets).length) && navContentReady(currentNavIndex()); else emptyNav">
4273
+ <ng-container *ngIf="l.content">
3038
4274
  <ng-container
3039
4275
  dynamicFieldLoader
3040
4276
  [fields]="l.content || []"
3041
- [formGroup]="form!"
4277
+ [formGroup]="effectiveContentForm()"
3042
4278
  ></ng-container>
3043
4279
  </ng-container>
3044
- <ng-container *ngIf="l.widgets?.length">
3045
- <ng-container *ngFor="let w of l.widgets; let wi = index; trackBy: trackWidgetDefinition">
4280
+ <ng-container *ngIf="safeWidgetDefinitions(l.widgets).length">
4281
+ <ng-container *ngFor="let w of safeWidgetDefinitions(l.widgets); let wi = index; trackBy: trackWidgetDefinition">
3046
4282
  <ng-container
3047
4283
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3048
4284
  [context]="context || {}"
@@ -3074,7 +4310,7 @@ class PraxisTabs {
3074
4310
  [fitInkBarToContent]="config?.group?.fitInkBarToContent"
3075
4311
  [headerPosition]="config?.group?.headerPosition ?? 'above'"
3076
4312
  [preserveContent]="config?.group?.preserveContent"
3077
- [selectedIndex]="selectedIndexSignal()"
4313
+ [selectedIndex]="selectedVisibleTabIndex()"
3078
4314
  [mat-stretch-tabs]="config?.group?.stretchTabs"
3079
4315
  [mat-align-tabs]="config?.group?.alignTabs || 'start'"
3080
4316
  [color]="config?.group?.color"
@@ -3084,26 +4320,27 @@ class PraxisTabs {
3084
4320
  [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
3085
4321
  (animationDone)="animationDone.emit()"
3086
4322
  (focusChange)="focusChange.emit($event)"
3087
- (selectedIndexChange)="onSelectedIndexChange($event)"
4323
+ (selectedIndexChange)="onVisibleTabIndexChange($event)"
3088
4324
  (selectedTabChange)="selectedTabChange.emit($event)"
3089
4325
  class="praxis-tabs-group"
3090
4326
  >
3091
4327
  <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"
4328
+ *ngFor="let entry of visibleTabEntries(); let i = index; trackBy: trackVisibleTab"
4329
+ [disabled]="entry.tab.disabled"
4330
+ [labelClass]="entry.tab.labelClass ?? ''"
4331
+ [bodyClass]="entry.tab.bodyClass ?? ''"
4332
+ [id]="entry.tab.id || ''"
4333
+ [attr.aria-label]="entry.tab.ariaLabel || null"
4334
+ [attr.aria-labelledby]="entry.tab.ariaLabelledby || null"
3099
4335
  >
3100
4336
  <ng-template mat-tab-label>
3101
- <span>{{ tab.textLabel }}</span>
4337
+ <mat-icon *ngIf="entry.tab.icon" class="tab-label-icon" [praxisIcon]="entry.tab.icon"></mat-icon>
4338
+ <span>{{ entry.tab.textLabel }}</span>
3102
4339
  <button
3103
4340
  *ngIf="config?.behavior?.closeable"
3104
4341
  mat-icon-button
3105
4342
  type="button"
3106
- (click)="closeTab(i); $event.stopPropagation()"
4343
+ (click)="closeTab(entry.index); $event.stopPropagation()"
3107
4344
  (keydown.enter)="$event.stopPropagation()"
3108
4345
  (keydown.space)="$event.stopPropagation()"
3109
4346
  [attr.aria-label]="t('chrome.closeTab', 'Fechar aba')"
@@ -3114,10 +4351,10 @@ class PraxisTabs {
3114
4351
  <button
3115
4352
  mat-icon-button
3116
4353
  type="button"
3117
- (click)="moveTab(i, -1); $event.stopPropagation()"
4354
+ (click)="moveTab(entry.index, -1); $event.stopPropagation()"
3118
4355
  (keydown.enter)="$event.stopPropagation()"
3119
4356
  (keydown.space)="$event.stopPropagation()"
3120
- [disabled]="i===0"
4357
+ [disabled]="entry.index===0"
3121
4358
  [attr.aria-label]="t('chrome.moveTabLeft', 'Mover aba para esquerda')"
3122
4359
  >
3123
4360
  <mat-icon fontIcon="arrow_back"></mat-icon>
@@ -3125,10 +4362,10 @@ class PraxisTabs {
3125
4362
  <button
3126
4363
  mat-icon-button
3127
4364
  type="button"
3128
- (click)="moveTab(i, 1); $event.stopPropagation()"
4365
+ (click)="moveTab(entry.index, 1); $event.stopPropagation()"
3129
4366
  (keydown.enter)="$event.stopPropagation()"
3130
4367
  (keydown.space)="$event.stopPropagation()"
3131
- [disabled]="i===(config?.tabs?.length||1)-1"
4368
+ [disabled]="entry.index===(config?.tabs?.length||1)-1"
3132
4369
  [attr.aria-label]="t('chrome.moveTabRight', 'Mover aba para direita')"
3133
4370
  >
3134
4371
  <mat-icon fontIcon="arrow_forward"></mat-icon>
@@ -3136,34 +4373,32 @@ class PraxisTabs {
3136
4373
  </ng-container>
3137
4374
  </ng-template>
3138
4375
 
3139
- <ng-template matTabContent>
3140
- <ng-container *ngIf="(tab.content?.length || tab.widgets?.length) && groupContentReady(i); else emptyTab">
3141
- <ng-container *ngIf="tab.content && form">
4376
+ <ng-container *ngIf="(entry.tab.content?.length || safeWidgetDefinitions(entry.tab.widgets).length) && groupContentReady(entry.index); else emptyTab">
4377
+ <ng-container *ngIf="entry.tab.content">
4378
+ <ng-container
4379
+ dynamicFieldLoader
4380
+ [fields]="entry.tab.content || []"
4381
+ [formGroup]="effectiveContentForm()"
4382
+ ></ng-container>
4383
+ </ng-container>
4384
+ <ng-container *ngIf="safeWidgetDefinitions(entry.tab.widgets).length">
4385
+ <ng-container *ngFor="let w of safeWidgetDefinitions(entry.tab.widgets); let wi = index; trackBy: trackWidgetDefinition">
3142
4386
  <ng-container
3143
- dynamicFieldLoader
3144
- [fields]="tab.content || []"
3145
- [formGroup]="form!"
4387
+ [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
4388
+ [context]="context || {}"
4389
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
3146
4390
  ></ng-container>
3147
4391
  </ng-container>
3148
- <ng-container *ngIf="tab.widgets?.length">
3149
- <ng-container *ngFor="let w of tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3150
- <ng-container
3151
- [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3152
- [context]="context || {}"
3153
- (widgetEvent)="emitWidgetEvent(tabEventPath(tab.id, i), $event)"
3154
- ></ng-container>
3155
- </ng-container>
3156
- </ng-container>
3157
4392
  </ng-container>
3158
- <ng-template #emptyTab>
3159
- <praxis-empty-state-card
3160
- [inline]="true"
3161
- icon="dashboard_customize"
3162
- [title]="t('emptyState.tabTitle', 'Sem conteudo nesta aba')"
3163
- [description]="t('emptyState.tabDescription', 'Adicione conteudo ou use o editor para configurar.')"
3164
- [primaryAction]="{ label: t('emptyState.openEditor', 'Abrir editor'), icon: 'tune', action: openEditor.bind(this) }"
3165
- ></praxis-empty-state-card>
3166
- </ng-template>
4393
+ </ng-container>
4394
+ <ng-template #emptyTab>
4395
+ <praxis-empty-state-card
4396
+ [inline]="true"
4397
+ icon="dashboard_customize"
4398
+ [title]="t('emptyState.tabTitle', 'Sem conteudo nesta aba')"
4399
+ [description]="t('emptyState.tabDescription', 'Adicione conteudo ou use o editor para configurar.')"
4400
+ [primaryAction]="{ label: t('emptyState.openEditor', 'Abrir editor'), icon: 'tune', action: openEditor.bind(this) }"
4401
+ ></praxis-empty-state-card>
3167
4402
  </ng-template>
3168
4403
  </mat-tab>
3169
4404
  </mat-tab-group>
@@ -3191,7 +4426,7 @@ class PraxisTabs {
3191
4426
  <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
3192
4427
  </button>
3193
4428
  </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 });
4429
+ `, 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}.tabs-ai-assistant-trigger{box-shadow:var(--md-sys-elevation-level2)}.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.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", "canvasFocus"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "ownerWidgetKey", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent", "widgetDiagnostic"], exportAs: ["dynamicWidgetLoader"] }, { kind: "component", type: PraxisAiAssistantShellComponent, selector: "praxis-ai-assistant-shell", inputs: ["labels", "mode", "state", "contextItems", "attachments", "messages", "quickReplies", "prompt", "statusText", "errorText", "testIdPrefix", "panelTestId", "submitTestId", "applyTestId", "primaryAction", "secondaryActions", "governanceActions", "busy", "canSubmit", "canApply", "submitOnEnter", "showAttachAction", "enablePastedAttachments", "enableFileAttachments", "attachmentAccept", "attachmentMultiple", "draggable", "resizable", "minWidth", "minHeight", "margin", "layout"], outputs: ["promptChange", "submitPrompt", "apply", "retryTurn", "cancelTurn", "shellAction", "close", "attach", "attachmentsPasted", "attachmentsSelected", "removeAttachment", "messageAction", "editMessage", "resendMessage", "quickReply", "layoutChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3195
4430
  }
3196
4431
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabs, decorators: [{
3197
4432
  type: Component,
@@ -3207,7 +4442,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3207
4442
  EmptyStateCardComponent,
3208
4443
  DynamicFieldLoaderDirective,
3209
4444
  DynamicWidgetLoaderDirective,
3210
- PraxisAiAssistantComponent,
4445
+ PraxisAiAssistantShellComponent,
3211
4446
  ], template: `
3212
4447
  <div
3213
4448
  class="praxis-tabs-root"
@@ -3225,9 +4460,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3225
4460
  <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
3226
4461
 
3227
4462
  <div class="tabs-ai-assistant" *ngIf="enableCustomization">
3228
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
4463
+ <button
4464
+ mat-mini-fab
4465
+ type="button"
4466
+ color="primary"
4467
+ class="tabs-ai-assistant-trigger"
4468
+ (click)="openAiAssistant()"
4469
+ matTooltip="Copiloto semantico Praxis"
4470
+ aria-label="Abrir copiloto semantico Praxis das abas"
4471
+ data-testid="praxis-tabs-ai-assistant-trigger"
4472
+ >
4473
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
4474
+ </button>
3229
4475
  </div>
3230
4476
 
4477
+ <praxis-ai-assistant-shell
4478
+ *ngIf="aiAssistantOpen && aiAssistantViewState"
4479
+ [labels]="aiAssistantLabels"
4480
+ [mode]="aiAssistantViewState.mode"
4481
+ [state]="aiAssistantViewState.state"
4482
+ [contextItems]="aiAssistantViewState.contextItems"
4483
+ [attachments]="aiAssistantViewState.attachments"
4484
+ [messages]="aiAssistantViewState.messages"
4485
+ [quickReplies]="aiAssistantViewState.quickReplies"
4486
+ [prompt]="aiAssistantPrompt"
4487
+ [statusText]="aiAssistantViewState.statusText"
4488
+ [errorText]="aiAssistantViewState.errorText"
4489
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
4490
+ [canApply]="aiAssistantViewState.canApply"
4491
+ [layout]="aiAssistantLayout"
4492
+ testIdPrefix="praxis-tabs-ai-assistant"
4493
+ panelTestId="praxis-tabs-ai-assistant-panel"
4494
+ submitTestId="praxis-tabs-ai-assistant-submit"
4495
+ applyTestId="praxis-tabs-ai-assistant-apply"
4496
+ (promptChange)="onAiAssistantPromptChange($event)"
4497
+ (submitPrompt)="onAiAssistantSubmit($event)"
4498
+ (apply)="onAiAssistantApply()"
4499
+ (retryTurn)="onAiAssistantRetry()"
4500
+ (cancelTurn)="onAiAssistantCancel()"
4501
+ (quickReply)="onAiAssistantQuickReply($event)"
4502
+ (editMessage)="onAiAssistantEditMessage($event)"
4503
+ (resendMessage)="onAiAssistantResendMessage($event)"
4504
+ (layoutChange)="onAiAssistantLayoutChange($event)"
4505
+ (close)="closeAiAssistant()"
4506
+ ></praxis-ai-assistant-shell>
4507
+
3231
4508
  <!-- Empty state (global) -->
3232
4509
  <ng-container *ngIf="isEmptyGlobal(); else notEmpty">
3233
4510
  <praxis-empty-state-card
@@ -3251,13 +4528,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3251
4528
  cdkDropList
3252
4529
  cdkDropListOrientation="horizontal"
3253
4530
  [cdkDropListDisabled]="!config?.behavior?.reorderable"
3254
- (cdkDropListDropped)="onNavDrop($event)"
4531
+ (cdkDropListDropped)="onVisibleNavDrop($event)"
3255
4532
  [disablePagination]="config?.nav?.disablePagination"
3256
4533
  [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
3257
4534
  [mat-stretch-tabs]="config?.nav?.stretchTabs"
3258
4535
  [color]="config?.nav?.color"
3259
4536
  [backgroundColor]="config?.nav?.backgroundColor"
3260
- [selectedIndex]="currentNavIndex()"
4537
+ [selectedIndex]="selectedVisibleNavIndex()"
3261
4538
  [attr.aria-label]="config?.nav?.ariaLabel || config?.group?.ariaLabel || null"
3262
4539
  [attr.aria-labelledby]="config?.nav?.ariaLabelledby || config?.group?.ariaLabelledby || null"
3263
4540
  [animationDuration]="effectiveAnimationDuration()"
@@ -3266,21 +4543,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3266
4543
  >
3267
4544
  <a
3268
4545
  mat-tab-link
3269
- *ngFor="let link of config?.nav?.links; let i = index; trackBy: trackNavLink"
4546
+ *ngFor="let entry of visibleNavLinkEntries(); let i = index; trackBy: trackVisibleNavLink"
3270
4547
  cdkDrag
3271
4548
  [cdkDragDisabled]="!config?.behavior?.reorderable"
3272
4549
  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)"
4550
+ [active]="getNavActive(entry.index)"
4551
+ [disabled]="entry.link.disabled"
4552
+ [disableRipple]="config?.nav?.disableRipple || entry.link.disableRipple"
4553
+ [fitInkBarToContent]="entry.link.fitInkBarToContent || false"
4554
+ [id]="entry.link.id || ''"
4555
+ (click)="onNavClick(entry.index)"
3279
4556
  >
3280
4557
  <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
3281
4558
  <mat-icon fontIcon="drag_indicator"></mat-icon>
3282
4559
  </span>
3283
- {{ link.label }}
4560
+ <mat-icon *ngIf="entry.link.icon" class="tab-label-icon" [praxisIcon]="entry.link.icon"></mat-icon>
4561
+ {{ entry.link.label }}
3284
4562
  </a>
3285
4563
  </nav>
3286
4564
 
@@ -3289,16 +4567,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3289
4567
  <ng-container
3290
4568
  *ngIf="config?.nav?.links?.[currentNavIndex()] as l"
3291
4569
  >
3292
- <ng-container *ngIf="(l.content?.length || l.widgets?.length) && navContentReady(currentNavIndex()); else emptyNav">
3293
- <ng-container *ngIf="l.content && form">
4570
+ <ng-container *ngIf="(l.content?.length || safeWidgetDefinitions(l.widgets).length) && navContentReady(currentNavIndex()); else emptyNav">
4571
+ <ng-container *ngIf="l.content">
3294
4572
  <ng-container
3295
4573
  dynamicFieldLoader
3296
4574
  [fields]="l.content || []"
3297
- [formGroup]="form!"
4575
+ [formGroup]="effectiveContentForm()"
3298
4576
  ></ng-container>
3299
4577
  </ng-container>
3300
- <ng-container *ngIf="l.widgets?.length">
3301
- <ng-container *ngFor="let w of l.widgets; let wi = index; trackBy: trackWidgetDefinition">
4578
+ <ng-container *ngIf="safeWidgetDefinitions(l.widgets).length">
4579
+ <ng-container *ngFor="let w of safeWidgetDefinitions(l.widgets); let wi = index; trackBy: trackWidgetDefinition">
3302
4580
  <ng-container
3303
4581
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3304
4582
  [context]="context || {}"
@@ -3330,7 +4608,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3330
4608
  [fitInkBarToContent]="config?.group?.fitInkBarToContent"
3331
4609
  [headerPosition]="config?.group?.headerPosition ?? 'above'"
3332
4610
  [preserveContent]="config?.group?.preserveContent"
3333
- [selectedIndex]="selectedIndexSignal()"
4611
+ [selectedIndex]="selectedVisibleTabIndex()"
3334
4612
  [mat-stretch-tabs]="config?.group?.stretchTabs"
3335
4613
  [mat-align-tabs]="config?.group?.alignTabs || 'start'"
3336
4614
  [color]="config?.group?.color"
@@ -3340,26 +4618,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3340
4618
  [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
3341
4619
  (animationDone)="animationDone.emit()"
3342
4620
  (focusChange)="focusChange.emit($event)"
3343
- (selectedIndexChange)="onSelectedIndexChange($event)"
4621
+ (selectedIndexChange)="onVisibleTabIndexChange($event)"
3344
4622
  (selectedTabChange)="selectedTabChange.emit($event)"
3345
4623
  class="praxis-tabs-group"
3346
4624
  >
3347
4625
  <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"
4626
+ *ngFor="let entry of visibleTabEntries(); let i = index; trackBy: trackVisibleTab"
4627
+ [disabled]="entry.tab.disabled"
4628
+ [labelClass]="entry.tab.labelClass ?? ''"
4629
+ [bodyClass]="entry.tab.bodyClass ?? ''"
4630
+ [id]="entry.tab.id || ''"
4631
+ [attr.aria-label]="entry.tab.ariaLabel || null"
4632
+ [attr.aria-labelledby]="entry.tab.ariaLabelledby || null"
3355
4633
  >
3356
4634
  <ng-template mat-tab-label>
3357
- <span>{{ tab.textLabel }}</span>
4635
+ <mat-icon *ngIf="entry.tab.icon" class="tab-label-icon" [praxisIcon]="entry.tab.icon"></mat-icon>
4636
+ <span>{{ entry.tab.textLabel }}</span>
3358
4637
  <button
3359
4638
  *ngIf="config?.behavior?.closeable"
3360
4639
  mat-icon-button
3361
4640
  type="button"
3362
- (click)="closeTab(i); $event.stopPropagation()"
4641
+ (click)="closeTab(entry.index); $event.stopPropagation()"
3363
4642
  (keydown.enter)="$event.stopPropagation()"
3364
4643
  (keydown.space)="$event.stopPropagation()"
3365
4644
  [attr.aria-label]="t('chrome.closeTab', 'Fechar aba')"
@@ -3370,10 +4649,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3370
4649
  <button
3371
4650
  mat-icon-button
3372
4651
  type="button"
3373
- (click)="moveTab(i, -1); $event.stopPropagation()"
4652
+ (click)="moveTab(entry.index, -1); $event.stopPropagation()"
3374
4653
  (keydown.enter)="$event.stopPropagation()"
3375
4654
  (keydown.space)="$event.stopPropagation()"
3376
- [disabled]="i===0"
4655
+ [disabled]="entry.index===0"
3377
4656
  [attr.aria-label]="t('chrome.moveTabLeft', 'Mover aba para esquerda')"
3378
4657
  >
3379
4658
  <mat-icon fontIcon="arrow_back"></mat-icon>
@@ -3381,10 +4660,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3381
4660
  <button
3382
4661
  mat-icon-button
3383
4662
  type="button"
3384
- (click)="moveTab(i, 1); $event.stopPropagation()"
4663
+ (click)="moveTab(entry.index, 1); $event.stopPropagation()"
3385
4664
  (keydown.enter)="$event.stopPropagation()"
3386
4665
  (keydown.space)="$event.stopPropagation()"
3387
- [disabled]="i===(config?.tabs?.length||1)-1"
4666
+ [disabled]="entry.index===(config?.tabs?.length||1)-1"
3388
4667
  [attr.aria-label]="t('chrome.moveTabRight', 'Mover aba para direita')"
3389
4668
  >
3390
4669
  <mat-icon fontIcon="arrow_forward"></mat-icon>
@@ -3392,34 +4671,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3392
4671
  </ng-container>
3393
4672
  </ng-template>
3394
4673
 
3395
- <ng-template matTabContent>
3396
- <ng-container *ngIf="(tab.content?.length || tab.widgets?.length) && groupContentReady(i); else emptyTab">
3397
- <ng-container *ngIf="tab.content && form">
4674
+ <ng-container *ngIf="(entry.tab.content?.length || safeWidgetDefinitions(entry.tab.widgets).length) && groupContentReady(entry.index); else emptyTab">
4675
+ <ng-container *ngIf="entry.tab.content">
4676
+ <ng-container
4677
+ dynamicFieldLoader
4678
+ [fields]="entry.tab.content || []"
4679
+ [formGroup]="effectiveContentForm()"
4680
+ ></ng-container>
4681
+ </ng-container>
4682
+ <ng-container *ngIf="safeWidgetDefinitions(entry.tab.widgets).length">
4683
+ <ng-container *ngFor="let w of safeWidgetDefinitions(entry.tab.widgets); let wi = index; trackBy: trackWidgetDefinition">
3398
4684
  <ng-container
3399
- dynamicFieldLoader
3400
- [fields]="tab.content || []"
3401
- [formGroup]="form!"
4685
+ [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
4686
+ [context]="context || {}"
4687
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
3402
4688
  ></ng-container>
3403
4689
  </ng-container>
3404
- <ng-container *ngIf="tab.widgets?.length">
3405
- <ng-container *ngFor="let w of tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3406
- <ng-container
3407
- [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3408
- [context]="context || {}"
3409
- (widgetEvent)="emitWidgetEvent(tabEventPath(tab.id, i), $event)"
3410
- ></ng-container>
3411
- </ng-container>
3412
- </ng-container>
3413
4690
  </ng-container>
3414
- <ng-template #emptyTab>
3415
- <praxis-empty-state-card
3416
- [inline]="true"
3417
- icon="dashboard_customize"
3418
- [title]="t('emptyState.tabTitle', 'Sem conteudo nesta aba')"
3419
- [description]="t('emptyState.tabDescription', 'Adicione conteudo ou use o editor para configurar.')"
3420
- [primaryAction]="{ label: t('emptyState.openEditor', 'Abrir editor'), icon: 'tune', action: openEditor.bind(this) }"
3421
- ></praxis-empty-state-card>
3422
- </ng-template>
4691
+ </ng-container>
4692
+ <ng-template #emptyTab>
4693
+ <praxis-empty-state-card
4694
+ [inline]="true"
4695
+ icon="dashboard_customize"
4696
+ [title]="t('emptyState.tabTitle', 'Sem conteudo nesta aba')"
4697
+ [description]="t('emptyState.tabDescription', 'Adicione conteudo ou use o editor para configurar.')"
4698
+ [primaryAction]="{ label: t('emptyState.openEditor', 'Abrir editor'), icon: 'tune', action: openEditor.bind(this) }"
4699
+ ></praxis-empty-state-card>
3423
4700
  </ng-template>
3424
4701
  </mat-tab>
3425
4702
  </mat-tab-group>
@@ -3447,7 +4724,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3447
4724
  <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
3448
4725
  </button>
3449
4726
  </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"] }]
4727
+ `, 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}.tabs-ai-assistant-trigger{box-shadow:var(--md-sys-elevation-level2)}.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
4728
  }], propDecorators: { config: [{
3452
4729
  type: Input
3453
4730
  }], tabsId: [{
@@ -3455,6 +4732,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3455
4732
  args: [{ required: true }]
3456
4733
  }], componentInstanceId: [{
3457
4734
  type: Input
4735
+ }], configPersistenceStrategy: [{
4736
+ type: Input
4737
+ }], selectedIndex: [{
4738
+ type: Input
3458
4739
  }], enableCustomization: [{
3459
4740
  type: Input
3460
4741
  }], form: [{
@@ -3473,10 +4754,114 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3473
4754
  type: Output
3474
4755
  }], selectFocusedIndex: [{
3475
4756
  type: Output
4757
+ }], configChange: [{
4758
+ type: Output
3476
4759
  }], widgetEvent: [{
3477
4760
  type: Output
3478
4761
  }] } });
3479
4762
 
4763
+ class PraxisTabsWidgetConfigEditor {
4764
+ set inputs(value) {
4765
+ this._inputs = value;
4766
+ this.editorDocument = this.createDocument();
4767
+ }
4768
+ get inputs() {
4769
+ return this._inputs;
4770
+ }
4771
+ set widgetKey(value) {
4772
+ this._widgetKey = value;
4773
+ this.editorDocument = this.createDocument();
4774
+ }
4775
+ get widgetKey() {
4776
+ return this._widgetKey;
4777
+ }
4778
+ tabsEditor;
4779
+ isDirty$ = new BehaviorSubject(false);
4780
+ isValid$ = new BehaviorSubject(true);
4781
+ isBusy$ = new BehaviorSubject(false);
4782
+ subscription = new Subscription();
4783
+ emptyConfig = {};
4784
+ _inputs = null;
4785
+ _widgetKey;
4786
+ editorDocument = this.createDocument();
4787
+ get config() {
4788
+ return this.inputs?.config ?? this.emptyConfig;
4789
+ }
4790
+ get tabsId() {
4791
+ return this.inputs?.tabsId ?? this.widgetKey;
4792
+ }
4793
+ get componentInstanceId() {
4794
+ return this.inputs?.componentInstanceId ?? undefined;
4795
+ }
4796
+ ngAfterViewInit() {
4797
+ if (!this.tabsEditor) {
4798
+ return;
4799
+ }
4800
+ this.subscription.add(this.tabsEditor.isDirty$.subscribe((value) => this.isDirty$.next(value)));
4801
+ this.subscription.add(this.tabsEditor.isValid$.subscribe((value) => this.isValid$.next(value)));
4802
+ this.subscription.add(this.tabsEditor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
4803
+ }
4804
+ ngOnDestroy() {
4805
+ this.subscription.unsubscribe();
4806
+ }
4807
+ getSettingsValue() {
4808
+ return this.buildValue(this.tabsEditor?.getSettingsValue());
4809
+ }
4810
+ onSave() {
4811
+ return this.buildValue(this.tabsEditor?.onSave?.() ?? this.tabsEditor?.getSettingsValue());
4812
+ }
4813
+ reset() {
4814
+ this.tabsEditor?.reset();
4815
+ }
4816
+ buildValue(document) {
4817
+ const bindings = document?.bindings ?? {};
4818
+ return {
4819
+ inputs: {
4820
+ ...(this.inputs ?? {}),
4821
+ config: document?.config ?? this.config,
4822
+ ...(bindings.tabsId ? { tabsId: bindings.tabsId } : this.tabsId ? { tabsId: this.tabsId } : {}),
4823
+ ...(bindings.componentInstanceId ? { componentInstanceId: bindings.componentInstanceId } : {}),
4824
+ },
4825
+ };
4826
+ }
4827
+ createDocument() {
4828
+ return createTabsAuthoringDocument({
4829
+ config: this.config,
4830
+ bindings: {
4831
+ tabsId: this.tabsId,
4832
+ componentInstanceId: this.componentInstanceId,
4833
+ },
4834
+ });
4835
+ }
4836
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabsWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
4837
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisTabsWidgetConfigEditor, isStandalone: true, selector: "praxis-tabs-widget-config-editor", inputs: { inputs: "inputs", widgetKey: "widgetKey" }, viewQueries: [{ propertyName: "tabsEditor", first: true, predicate: ["tabsEditor"], descendants: true }], ngImport: i0, template: `
4838
+ <section data-testid="tabs-widget-config-editor">
4839
+ <praxis-tabs-config-editor #tabsEditor [document]="editorDocument" />
4840
+ </section>
4841
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisTabsConfigEditor, selector: "praxis-tabs-config-editor", inputs: ["document"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4842
+ }
4843
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabsWidgetConfigEditor, decorators: [{
4844
+ type: Component,
4845
+ args: [{
4846
+ selector: 'praxis-tabs-widget-config-editor',
4847
+ standalone: true,
4848
+ imports: [CommonModule, PraxisTabsConfigEditor],
4849
+ template: `
4850
+ <section data-testid="tabs-widget-config-editor">
4851
+ <praxis-tabs-config-editor #tabsEditor [document]="editorDocument" />
4852
+ </section>
4853
+ `,
4854
+ changeDetection: ChangeDetectionStrategy.OnPush,
4855
+ }]
4856
+ }], propDecorators: { inputs: [{
4857
+ type: Input
4858
+ }], widgetKey: [{
4859
+ type: Input
4860
+ }], tabsEditor: [{
4861
+ type: ViewChild,
4862
+ args: ['tabsEditor']
4863
+ }] } });
4864
+
3480
4865
  const PRAXIS_TABS_PORTS = [
3481
4866
  {
3482
4867
  id: 'context',
@@ -3504,6 +4889,19 @@ const PRAXIS_TABS_PORTS = [
3504
4889
  description: 'Fragmento canonico de configuracao das tabs/nav e dos widgets internos.',
3505
4890
  exposure: { public: true, group: 'config' },
3506
4891
  },
4892
+ {
4893
+ id: 'selectedIndex',
4894
+ label: 'Indice selecionado',
4895
+ direction: 'input',
4896
+ semanticKind: 'value',
4897
+ schema: {
4898
+ id: 'number',
4899
+ kind: 'ts-type',
4900
+ ref: 'number',
4901
+ },
4902
+ description: 'Indice ativo de abas/nav para controle externo por composicao.',
4903
+ exposure: { public: true, group: 'state' },
4904
+ },
3507
4905
  {
3508
4906
  id: 'selectedIndexChange',
3509
4907
  label: 'Troca de indice selecionado',
@@ -3518,6 +4916,20 @@ const PRAXIS_TABS_PORTS = [
3518
4916
  description: 'Evento canonico emitido quando a selecao de aba muda.',
3519
4917
  exposure: { public: true, group: 'events' },
3520
4918
  },
4919
+ {
4920
+ id: 'configChange',
4921
+ label: 'Configuracao alterada',
4922
+ direction: 'output',
4923
+ semanticKind: 'event',
4924
+ schema: {
4925
+ id: 'TabsConfigChangeEvent',
4926
+ kind: 'ts-type',
4927
+ ref: 'TabsConfigChangeEvent',
4928
+ },
4929
+ cardinality: 'stream',
4930
+ description: 'Evento canonico emitido quando autoria assistida altera TabsMetadata; carrega inputPatch.config para persistencia pelo host.',
4931
+ exposure: { public: true, group: 'config' },
4932
+ },
3521
4933
  {
3522
4934
  id: 'widgetEvent',
3523
4935
  label: 'Evento interno de widget',
@@ -3540,6 +4952,14 @@ const PRAXIS_TABS_COMPONENT_METADATA = {
3540
4952
  friendlyName: 'Praxis Tabs',
3541
4953
  description: 'Abas dinâmicas baseadas em metadata, com MatTabGroup/TabNav e suporte a tokens M3 via appearance.',
3542
4954
  icon: 'tab',
4955
+ authoringManifestRef: {
4956
+ componentId: 'praxis-tabs',
4957
+ source: 'PRAXIS_TABS_AUTHORING_MANIFEST',
4958
+ },
4959
+ configEditor: {
4960
+ component: PraxisTabsWidgetConfigEditor,
4961
+ title: 'Configure tabs',
4962
+ },
3543
4963
  inputs: [
3544
4964
  { name: 'config', type: 'TabsMetadata', label: 'Configuração', description: 'Configuração JSON (tabs/nav, aparência e widgets internos)' },
3545
4965
  {
@@ -3554,6 +4974,19 @@ const PRAXIS_TABS_COMPONENT_METADATA = {
3554
4974
  label: 'ID da instância',
3555
4975
  description: 'Identificador opcional para múltiplas instâncias na mesma rota',
3556
4976
  },
4977
+ {
4978
+ name: 'configPersistenceStrategy',
4979
+ type: '"storage-first" | "input-first"',
4980
+ default: 'storage-first',
4981
+ label: 'Estratégia de persistência',
4982
+ description: 'Define se a configuração persistida ou a configuração de entrada governa a instância.',
4983
+ },
4984
+ {
4985
+ name: 'selectedIndex',
4986
+ type: 'number',
4987
+ label: 'Indice selecionado',
4988
+ description: 'Indice ativo de abas/nav controlavel externamente pela composicao',
4989
+ },
3557
4990
  {
3558
4991
  name: 'enableCustomization',
3559
4992
  type: 'boolean',
@@ -3576,6 +5009,12 @@ const PRAXIS_TABS_COMPONENT_METADATA = {
3576
5009
  ],
3577
5010
  outputs: [
3578
5011
  { name: 'selectedIndexChange', type: 'number', label: 'Índice selecionado' },
5012
+ {
5013
+ name: 'configChange',
5014
+ type: 'TabsConfigChangeEvent',
5015
+ label: 'Configuração alterada',
5016
+ description: 'Emite inputPatch.config quando uma autoria assistida altera TabsMetadata.',
5017
+ },
3579
5018
  { name: 'selectedTabChange', type: 'MatTabChangeEvent', label: 'Troca de aba' },
3580
5019
  { name: 'animationDone', type: 'void' },
3581
5020
  { name: 'focusChange', type: 'MatTabChangeEvent', label: 'Foco alterado' },
@@ -3675,6 +5114,349 @@ function providePraxisTabsMetadata() {
3675
5114
  };
3676
5115
  }
3677
5116
 
5117
+ const tabItemSchema = {
5118
+ type: 'object',
5119
+ required: ['id', 'textLabel'],
5120
+ properties: {
5121
+ id: { type: 'string' },
5122
+ textLabel: { type: 'string' },
5123
+ icon: { type: 'string' },
5124
+ disabled: { type: 'boolean' },
5125
+ visible: { type: 'boolean', default: true },
5126
+ content: { type: 'array', items: { type: 'object' } },
5127
+ widgets: { type: 'array', items: { type: 'object' } },
5128
+ },
5129
+ };
5130
+ const tabPatchSchema = {
5131
+ type: 'object',
5132
+ minProperties: 1,
5133
+ properties: {
5134
+ id: { type: 'string' },
5135
+ textLabel: { type: 'string' },
5136
+ icon: { type: 'string' },
5137
+ disabled: { type: 'boolean' },
5138
+ visible: { type: 'boolean' },
5139
+ content: { type: 'array', items: { type: 'object' } },
5140
+ widgets: { type: 'array', items: { type: 'object' } },
5141
+ },
5142
+ };
5143
+ const PRAXIS_TABS_AUTHORING_MANIFEST = {
5144
+ schemaVersion: '1.0.0',
5145
+ componentId: 'praxis-tabs',
5146
+ ownerPackage: '@praxisui/tabs',
5147
+ configSchemaId: 'TabsMetadata',
5148
+ manifestVersion: '1.0.0',
5149
+ runtimeInputs: [
5150
+ { name: 'config', type: 'TabsMetadata', description: 'Canonical tabs/nav configuration.' },
5151
+ { name: 'tabsId', type: 'string', description: 'Stable id used to derive persistence scope.' },
5152
+ { name: 'componentInstanceId', type: 'string', description: 'Optional instance discriminator for persistence scope.' },
5153
+ {
5154
+ name: 'configPersistenceStrategy',
5155
+ type: '"storage-first" | "input-first"',
5156
+ description: 'Controls whether persisted customization or explicit input config governs the runtime. Governed previews with nested widgets should use input-first.',
5157
+ },
5158
+ { name: 'form', type: 'FormGroup', description: 'FormGroup consumed by dynamic field content.' },
5159
+ { name: 'context', type: 'Record<string, any>', description: 'Context passed to nested widgets.' },
5160
+ { name: 'enableCustomization', type: 'boolean', description: 'Enables Settings Panel authoring surfaces.' },
5161
+ ],
5162
+ editableTargets: [
5163
+ { kind: 'tab', resolver: 'tab-by-id-or-label', description: 'A group-mode tab in config.tabs[].' },
5164
+ { kind: 'tabLabel', resolver: 'tab-by-id-or-label', description: 'The text label of a group-mode tab.' },
5165
+ { kind: 'tabIcon', resolver: 'tab-by-id-or-label', description: 'Icon metadata rendered in a group tab label.' },
5166
+ { kind: 'tabContent', resolver: 'tab-or-link-by-id', description: 'Dynamic fields or widgets hosted by a tab or nav link.' },
5167
+ { kind: 'activeTab', resolver: 'tab-index-or-id', description: 'Selected tab or nav link index.' },
5168
+ { kind: 'visibility', resolver: 'tab-or-link-by-id', description: 'Runtime visibility flag for a group tab or nav link.' },
5169
+ { kind: 'disabledState', resolver: 'tab-or-link-by-id', description: 'Disabled state of a tab or nav link.' },
5170
+ { kind: 'layout', resolver: 'tabs-layout-config', description: 'Group/nav mode, header position, density, stretch and behavior settings.' },
5171
+ ],
5172
+ operations: [
5173
+ {
5174
+ operationId: 'tab.add',
5175
+ title: 'Add tab',
5176
+ scope: 'global',
5177
+ targetKind: 'tab',
5178
+ target: { kind: 'tab', resolver: 'tabs-array', ambiguityPolicy: 'fail', required: false },
5179
+ inputSchema: tabItemSchema,
5180
+ effects: [{ kind: 'append-unique', path: 'tabs[]', key: 'id' }],
5181
+ validators: ['tab-id-unique', 'tabs-mode-compatible', 'tab-content-valid'],
5182
+ destructive: false,
5183
+ requiresConfirmation: false,
5184
+ affectedPaths: ['tabs[]'],
5185
+ submissionImpact: 'config-only',
5186
+ preconditions: ['config-initialized'],
5187
+ },
5188
+ {
5189
+ operationId: 'tab.remove',
5190
+ title: 'Remove tab',
5191
+ scope: 'layout',
5192
+ targetKind: 'tab',
5193
+ target: { kind: 'tab', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
5194
+ inputSchema: {
5195
+ type: 'object',
5196
+ properties: {
5197
+ replacementActiveTabId: { type: 'string' },
5198
+ },
5199
+ },
5200
+ effects: [
5201
+ {
5202
+ kind: 'compile-domain-patch',
5203
+ handler: 'tabs.remove-tab-and-reselect',
5204
+ handlerContract: {
5205
+ reads: ['tabs[]', 'group.selectedIndex'],
5206
+ writes: ['tabs[]', 'group.selectedIndex'],
5207
+ identityKeys: ['tabs[].id'],
5208
+ inputSchema: {
5209
+ type: 'object',
5210
+ properties: { replacementActiveTabId: { type: 'string' } },
5211
+ },
5212
+ failureModes: ['target-tab-missing', 'replacement-tab-missing', 'confirmation-missing'],
5213
+ description: 'Removes the target tab by stable id and reselects a safe replacement when the active/default tab is removed.',
5214
+ },
5215
+ },
5216
+ ],
5217
+ destructive: true,
5218
+ requiresConfirmation: true,
5219
+ validators: ['tab-exists', 'active-tab-removal-safe', 'tab-content-removal-confirmed'],
5220
+ affectedPaths: ['tabs[]', 'group.selectedIndex'],
5221
+ submissionImpact: 'config-only',
5222
+ preconditions: ['config-initialized', 'target-tab-exists', 'confirmation-collected'],
5223
+ },
5224
+ {
5225
+ operationId: 'tab.label.set',
5226
+ title: 'Set tab label',
5227
+ scope: 'layout',
5228
+ targetKind: 'tabLabel',
5229
+ target: { kind: 'tabLabel', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
5230
+ inputSchema: { type: 'object', required: ['textLabel'], properties: { textLabel: { type: 'string' } } },
5231
+ effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
5232
+ destructive: false,
5233
+ requiresConfirmation: false,
5234
+ validators: ['tab-exists', 'tab-label-valid'],
5235
+ affectedPaths: ['tabs[].textLabel'],
5236
+ submissionImpact: 'config-only',
5237
+ preconditions: ['config-initialized', 'target-tab-exists'],
5238
+ },
5239
+ {
5240
+ operationId: 'tab.icon.set',
5241
+ title: 'Set tab icon',
5242
+ scope: 'layout',
5243
+ targetKind: 'tabIcon',
5244
+ target: { kind: 'tabIcon', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
5245
+ inputSchema: { type: 'object', required: ['icon'], properties: { icon: { type: 'string' } } },
5246
+ effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
5247
+ destructive: false,
5248
+ requiresConfirmation: false,
5249
+ validators: ['tab-exists', 'tab-icon-valid'],
5250
+ affectedPaths: ['tabs[].icon'],
5251
+ submissionImpact: 'visual-only',
5252
+ preconditions: ['config-initialized', 'target-tab-exists'],
5253
+ },
5254
+ {
5255
+ operationId: 'tab.order.set',
5256
+ title: 'Reorder tabs',
5257
+ scope: 'layout',
5258
+ targetKind: 'tab',
5259
+ target: { kind: 'tab', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
5260
+ inputSchema: { type: 'object', required: ['beforeTabId'], properties: { beforeTabId: { type: 'string' } } },
5261
+ effects: [
5262
+ {
5263
+ kind: 'compile-domain-patch',
5264
+ handler: 'tabs.reorder-tab-and-preserve-selection',
5265
+ handlerContract: {
5266
+ reads: ['tabs[]', 'group.selectedIndex'],
5267
+ writes: ['tabs[]', 'group.selectedIndex'],
5268
+ identityKeys: ['tabs[].id'],
5269
+ inputSchema: { type: 'object', required: ['beforeTabId'], properties: { beforeTabId: { type: 'string' } } },
5270
+ failureModes: ['target-tab-missing', 'before-tab-missing', 'unstable-tab-id'],
5271
+ description: 'Reorders tabs by stable id and remaps group.selectedIndex when the selected tab crosses positions.',
5272
+ },
5273
+ },
5274
+ ],
5275
+ destructive: false,
5276
+ requiresConfirmation: false,
5277
+ validators: ['tab-exists', 'tab-order-deterministic'],
5278
+ affectedPaths: ['tabs[]', 'group.selectedIndex'],
5279
+ submissionImpact: 'config-only',
5280
+ preconditions: ['config-initialized', 'target-tab-exists'],
5281
+ },
5282
+ {
5283
+ operationId: 'tab.disabled.set',
5284
+ title: 'Set tab disabled state',
5285
+ scope: 'interaction',
5286
+ targetKind: 'disabledState',
5287
+ target: { kind: 'disabledState', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
5288
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
5289
+ effects: [
5290
+ {
5291
+ kind: 'compile-domain-patch',
5292
+ handler: 'tabs.set-tab-or-link-disabled',
5293
+ handlerContract: {
5294
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
5295
+ writes: ['tabs[].disabled', 'nav.links[].disabled'],
5296
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
5297
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
5298
+ failureModes: ['target-tab-or-link-missing', 'ambiguous-target', 'active-item-disabled-without-reselection'],
5299
+ description: 'Sets disabled on the resolved group tab or nav link without guessing between modes.',
5300
+ },
5301
+ },
5302
+ ],
5303
+ destructive: false,
5304
+ requiresConfirmation: false,
5305
+ validators: ['tab-or-link-exists', 'active-tab-disabled-safe'],
5306
+ affectedPaths: ['tabs[].disabled', 'nav.links[].disabled'],
5307
+ submissionImpact: 'config-only',
5308
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
5309
+ },
5310
+ {
5311
+ operationId: 'tab.visible.set',
5312
+ title: 'Set tab visibility',
5313
+ scope: 'interaction',
5314
+ targetKind: 'visibility',
5315
+ target: { kind: 'visibility', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
5316
+ inputSchema: { type: 'object', required: ['visible'], properties: { visible: { type: 'boolean' } } },
5317
+ effects: [
5318
+ {
5319
+ kind: 'compile-domain-patch',
5320
+ handler: 'tabs.set-tab-or-link-visible',
5321
+ handlerContract: {
5322
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
5323
+ writes: ['tabs[].visible', 'nav.links[].visible'],
5324
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
5325
+ inputSchema: { type: 'object', required: ['visible'], properties: { visible: { type: 'boolean' } } },
5326
+ failureModes: ['target-tab-or-link-missing', 'ambiguous-target', 'active-item-hidden-without-reselection'],
5327
+ description: 'Sets visible on the resolved group tab or nav link and preserves deterministic visible-index mapping.',
5328
+ },
5329
+ },
5330
+ ],
5331
+ destructive: false,
5332
+ requiresConfirmation: false,
5333
+ validators: ['tab-or-link-exists', 'active-tab-visibility-safe'],
5334
+ affectedPaths: ['tabs[].visible', 'nav.links[].visible'],
5335
+ submissionImpact: 'config-only',
5336
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
5337
+ },
5338
+ {
5339
+ operationId: 'tab.active.set',
5340
+ title: 'Set active tab',
5341
+ scope: 'interaction',
5342
+ targetKind: 'activeTab',
5343
+ target: { kind: 'activeTab', resolver: 'tab-index-or-id', ambiguityPolicy: 'fail', required: true },
5344
+ inputSchema: { type: 'object', required: ['selectedIndex'], properties: { selectedIndex: { type: 'number' }, tabId: { type: 'string' } } },
5345
+ effects: [
5346
+ {
5347
+ kind: 'compile-domain-patch',
5348
+ handler: 'tabs.set-active-item',
5349
+ handlerContract: {
5350
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
5351
+ writes: ['group.selectedIndex', 'nav.selectedIndex'],
5352
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
5353
+ inputSchema: { type: 'object', required: ['selectedIndex'], properties: { selectedIndex: { type: 'number' }, tabId: { type: 'string' } } },
5354
+ failureModes: ['target-tab-or-link-missing', 'selected-index-out-of-range', 'hidden-or-disabled-target'],
5355
+ description: 'Sets the active index for the current primary mode using either selectedIndex or a resolved tab/link id.',
5356
+ },
5357
+ },
5358
+ ],
5359
+ destructive: false,
5360
+ requiresConfirmation: false,
5361
+ validators: ['active-tab-exists', 'selected-index-in-range'],
5362
+ affectedPaths: ['group.selectedIndex', 'nav.selectedIndex'],
5363
+ submissionImpact: 'config-only',
5364
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
5365
+ },
5366
+ {
5367
+ operationId: 'layout.variant.set',
5368
+ title: 'Set tabs layout variant',
5369
+ scope: 'layout',
5370
+ targetKind: 'layout',
5371
+ target: { kind: 'layout', resolver: 'tabs-layout-config', ambiguityPolicy: 'fail', required: true },
5372
+ inputSchema: {
5373
+ type: 'object',
5374
+ required: ['mode'],
5375
+ properties: {
5376
+ mode: { enum: ['group', 'nav'] },
5377
+ density: { enum: ['compact', 'comfortable', 'spacious'] },
5378
+ headerPosition: { enum: ['above', 'below'] },
5379
+ alignTabs: { enum: ['start', 'center', 'end'] },
5380
+ stretchTabs: { type: 'boolean' },
5381
+ lazyLoad: { type: 'boolean' },
5382
+ },
5383
+ },
5384
+ effects: [{ kind: 'merge-object', path: 'appearance' }, { kind: 'merge-object', path: 'group' }, { kind: 'merge-object', path: 'nav' }, { kind: 'merge-object', path: 'behavior' }],
5385
+ destructive: false,
5386
+ requiresConfirmation: false,
5387
+ validators: ['tabs-mode-compatible', 'layout-values-valid', 'editor-runtime-round-trip'],
5388
+ affectedPaths: ['appearance.density', 'group.headerPosition', 'group.alignTabs', 'group.stretchTabs', 'nav.stretchTabs', 'behavior.lazyLoad'],
5389
+ submissionImpact: 'config-only',
5390
+ preconditions: ['config-initialized'],
5391
+ },
5392
+ {
5393
+ operationId: 'tab.content.set',
5394
+ title: 'Set tab content',
5395
+ scope: 'layout',
5396
+ targetKind: 'tabContent',
5397
+ target: { kind: 'tabContent', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
5398
+ inputSchema: tabPatchSchema,
5399
+ effects: [
5400
+ {
5401
+ kind: 'compile-domain-patch',
5402
+ handler: 'tabs.set-tab-or-link-content',
5403
+ handlerContract: {
5404
+ reads: ['tabs[]', 'nav.links[]'],
5405
+ writes: ['tabs[].content', 'tabs[].widgets', 'nav.links[].content', 'nav.links[].widgets'],
5406
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
5407
+ inputSchema: tabPatchSchema,
5408
+ failureModes: ['target-tab-or-link-missing', 'invalid-dynamic-field-content', 'invalid-widget-definition'],
5409
+ description: 'Updates content/widgets only on the resolved group tab or nav link while preserving nested widget identity.',
5410
+ },
5411
+ },
5412
+ ],
5413
+ destructive: false,
5414
+ requiresConfirmation: false,
5415
+ validators: ['tab-or-link-exists', 'tab-content-valid', 'widget-event-delegated'],
5416
+ affectedPaths: ['tabs[].content', 'tabs[].widgets', 'nav.links[].content', 'nav.links[].widgets'],
5417
+ submissionImpact: 'config-only',
5418
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
5419
+ },
5420
+ ],
5421
+ validators: [
5422
+ { validatorId: 'tab-id-unique', level: 'error', code: 'PTABS001', description: 'Tab ids and nav link ids must be unique within their mode.' },
5423
+ { validatorId: 'tab-exists', level: 'error', code: 'PTABS002', description: 'Target tab must exist before applying the operation.' },
5424
+ { validatorId: 'tab-or-link-exists', level: 'error', code: 'PTABS003', description: 'Target must resolve to an existing group tab or nav link.' },
5425
+ { validatorId: 'active-tab-exists', level: 'error', code: 'PTABS004', description: 'Active tab or nav link selection must reference an existing item.' },
5426
+ { validatorId: 'selected-index-in-range', level: 'error', code: 'PTABS005', description: 'Selected index must be clamped to the target mode item count.' },
5427
+ { validatorId: 'active-tab-removal-safe', level: 'error', code: 'PTABS006', description: 'Removing the active/default tab requires confirmation or a replacement active tab.' },
5428
+ { validatorId: 'tab-content-removal-confirmed', level: 'error', code: 'PTABS007', description: 'Removing a tab or link with content/widgets is destructive and requires confirmation.' },
5429
+ { validatorId: 'tab-label-valid', level: 'error', code: 'PTABS008', description: 'Tab labels must be non-empty text values after localization/domain projection.' },
5430
+ { validatorId: 'tab-icon-valid', level: 'warning', code: 'PTABS009', description: 'Tab icon metadata must remain compatible with the icon directive and editor round-trip.' },
5431
+ { validatorId: 'tab-order-deterministic', level: 'error', code: 'PTABS010', description: 'Tab ordering must use stable ids, not transient array index as identity.' },
5432
+ { validatorId: 'tabs-mode-compatible', level: 'error', code: 'PTABS011', description: 'Authoring must resolve to one primary mode: group tabs or nav links.' },
5433
+ { validatorId: 'layout-values-valid', level: 'error', code: 'PTABS012', description: 'Layout values must match TabsMetadata enums and runtime bindings.' },
5434
+ { 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.' },
5435
+ { validatorId: 'active-tab-disabled-safe', level: 'warning', code: 'PTABS014', description: 'Disabling the active item should move selection or request explicit confirmation.' },
5436
+ { validatorId: 'active-tab-visibility-safe', level: 'warning', code: 'PTABS015', description: 'Hiding the active item should move selection or request explicit confirmation.' },
5437
+ { validatorId: 'tab-content-valid', level: 'error', code: 'PTABS016', description: 'Tab content must be valid DynamicFieldMetadata[] or WidgetDefinition[] and preserve nested widget identity.' },
5438
+ { 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.' },
5439
+ ],
5440
+ roundTripRequirements: [
5441
+ 'Operations must preserve stable tab/link ids; array index may be used only as a resolver fallback, never as canonical identity.',
5442
+ 'Settings Panel, quick setup and JSON editor must round-trip through TabsAuthoringDocument without losing config or bindings.',
5443
+ 'Group and nav modes must remain mutually explicit; authoring cannot silently mix config.tabs and nav.links as competing primary modes.',
5444
+ 'Nested widget events remain delegated through widgetEvent path enrichment and component-port nestedPath semantics.',
5445
+ ],
5446
+ examples: [
5447
+ { id: 'add-overview-tab', request: 'Add an Overview tab before the details tab.', operationId: 'tab.add', params: { id: 'overview', textLabel: 'Overview' }, isPositive: true },
5448
+ { id: 'add-list-to-current-tab', request: 'Create a list widget inside the current training tab.', operationId: 'tab.content.set', target: 'training', params: { widgets: [{ id: 'training-list', component: 'praxis-list', title: 'Training list' }] }, isPositive: true },
5449
+ { id: 'add-form-fields-to-existing-tab', request: 'Add name, date and status fields to the existing onboarding tab.', operationId: 'tab.content.set', target: 'onboarding', params: { content: [{ name: 'name', label: 'Name', controlType: 'text' }, { name: 'plannedDate', label: 'Planned date', controlType: 'date' }, { name: 'status', label: 'Status', controlType: 'select' }] }, isPositive: true },
5450
+ { id: 'rename-tab', request: 'Rename the details tab to Account Details.', operationId: 'tab.label.set', target: 'details', params: { textLabel: 'Account Details' }, isPositive: true },
5451
+ { id: 'reorder-tabs', request: 'Move billing before overview.', operationId: 'tab.order.set', target: 'billing', params: { beforeTabId: 'overview' }, isPositive: true },
5452
+ { id: 'disable-tab', request: 'Disable the audit tab until the user has permission.', operationId: 'tab.disabled.set', target: 'audit', params: { disabled: true }, isPositive: true },
5453
+ { id: 'activate-tab', request: 'Open the documents tab by default.', operationId: 'tab.active.set', target: 'documents', params: { tabId: 'documents', selectedIndex: 2 }, isPositive: true },
5454
+ { id: 'reject-duplicate-tab-id', request: 'Add another tab with id overview.', operationId: 'tab.add', params: { id: 'overview', textLabel: 'Duplicate Overview' }, isPositive: false },
5455
+ { id: 'reject-current-tab-content-as-tab-add', request: 'Create a list in this tab; do not add a new tab.', operationId: 'tab.add', params: { id: 'list-in-this-tab', textLabel: 'List in this tab' }, isPositive: false },
5456
+ { id: 'confirm-remove-content-tab', request: 'Remove the details tab that contains widgets.', operationId: 'tab.remove', target: 'details', params: { replacementActiveTabId: 'overview' }, isPositive: true },
5457
+ ],
5458
+ };
5459
+
3678
5460
  /*
3679
5461
  * Public API Surface of praxis-tabs
3680
5462
  */
@@ -3683,4 +5465,4 @@ function providePraxisTabsMetadata() {
3683
5465
  * Generated bundle index. Do not edit.
3684
5466
  */
3685
5467
 
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 };
5468
+ export { PRAXIS_TABS_AUTHORING_MANIFEST, PRAXIS_TABS_COMPONENT_METADATA, PRAXIS_TABS_I18N_CONFIG, PRAXIS_TABS_I18N_NAMESPACE, PraxisTabs, PraxisTabsConfigEditor, PraxisTabsWidgetConfigEditor, TABS_AI_CAPABILITIES, buildTabsApplyPlan, createPraxisTabsI18nConfig, createTabsAuthoringDocument, normalizeTabsAuthoringDocument, providePraxisTabsI18n, providePraxisTabsMetadata, serializeTabsAuthoringDocument, toCanonicalTabsConfig, validateTabsAuthoringDocument };