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

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';
@@ -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
31
  import { take, takeUntil } from 'rxjs/operators';
32
- import { BaseAiAdapter, PraxisAiAssistantComponent } from '@praxisui/ai';
32
+ import { BaseAiAdapter, AiBackendApiService, PraxisAssistantSessionRegistryService, PraxisAssistantTurnOrchestratorService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
33
33
 
34
34
  const DOCUMENT_KIND = 'praxis.tabs.editor';
35
35
  const DOCUMENT_VERSION = 1;
@@ -533,6 +533,12 @@ function providePraxisTabsI18n() {
533
533
  class PraxisTabsConfigEditor {
534
534
  registry;
535
535
  i18n = inject(PraxisI18nService);
536
+ set document(value) {
537
+ if (!value) {
538
+ return;
539
+ }
540
+ this.initializeDocument(value);
541
+ }
536
542
  primaryMode = 'group';
537
543
  editedDocument;
538
544
  editedConfig;
@@ -612,12 +618,18 @@ class PraxisTabsConfigEditor {
612
618
  this.initialDocument = structuredClone(incomingDocument);
613
619
  this.editedDocument = structuredClone(incomingDocument);
614
620
  this.editedConfig = this.editedDocument.config;
615
- this.bindings = this.editedDocument.bindings;
616
- this.primaryMode = this.inferPrimaryMode(this.editedConfig);
621
+ this.initializeDocument(incomingDocument);
622
+ this.componentOptions = this.registry.getAll().map((m) => ({ id: m.id, friendlyName: m.friendlyName }));
623
+ }
624
+ initializeDocument(document) {
625
+ const normalized = normalizeTabsAuthoringDocument(document);
626
+ this.initialDocument = structuredClone(normalized);
627
+ this.syncEditorStateFromDocument(normalized);
617
628
  this.jsonText = this.stringify(this.editedDocument);
629
+ this.isValid = true;
630
+ this.errorMsg = '';
618
631
  this.updateDirty();
619
632
  this.refreshDiagnostics();
620
- this.componentOptions = this.registry.getAll().map((m) => ({ id: m.id, friendlyName: m.friendlyName }));
621
633
  }
622
634
  inferPrimaryMode(config) {
623
635
  return config?.nav?.links?.length ? 'nav' : 'group';
@@ -784,6 +796,7 @@ class PraxisTabsConfigEditor {
784
796
  this.editedConfig.tabs.push({
785
797
  id: `tab${(this.editedConfig.tabs.length + 1)}`,
786
798
  textLabel: this.t('defaults.newTabLabel', 'New Tab'),
799
+ visible: true,
787
800
  });
788
801
  this.onAppearanceChange();
789
802
  }
@@ -859,6 +872,7 @@ class PraxisTabsConfigEditor {
859
872
  this.nav.links.push({
860
873
  id: `link${this.nav.links.length + 1}`,
861
874
  label: this.t('defaults.newLinkLabel', 'New Link'),
875
+ visible: true,
862
876
  });
863
877
  this.onAppearanceChange();
864
878
  }
@@ -953,7 +967,7 @@ class PraxisTabsConfigEditor {
953
967
  this.onAppearanceChange();
954
968
  }
955
969
  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: `
970
+ 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
971
  <div class="editor-shell">
958
972
  <div class="editor-topbar">
959
973
  <mat-form-field appearance="outline" class="editor-mode-field">
@@ -1293,6 +1307,9 @@ class PraxisTabsConfigEditor {
1293
1307
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1294
1308
  <input matInput [(ngModel)]="tab.textLabel" (ngModelChange)="onAppearanceChange()" />
1295
1309
  </mat-form-field>
1310
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1311
+ <input matInput [(ngModel)]="tab.icon" (ngModelChange)="onAppearanceChange()" />
1312
+ </mat-form-field>
1296
1313
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.labelClass', 'Classe do rotulo') }}</mat-label>
1297
1314
  <input matInput [(ngModel)]="tab.labelClass" (ngModelChange)="onAppearanceChange()" />
1298
1315
  </mat-form-field>
@@ -1306,7 +1323,10 @@ class PraxisTabsConfigEditor {
1306
1323
  <input matInput [(ngModel)]="tab.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
1307
1324
  </mat-form-field>
1308
1325
  </div>
1309
- <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1326
+ <div class="editor-row">
1327
+ <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1328
+ <mat-slide-toggle [ngModel]="tab.visible !== false" (ngModelChange)="tab.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1329
+ </div>
1310
1330
 
1311
1331
  <!-- Widgets (componentes dinâmicos) -->
1312
1332
  <div class="editor-divider editor-grid">
@@ -1384,9 +1404,13 @@ class PraxisTabsConfigEditor {
1384
1404
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1385
1405
  <input matInput [(ngModel)]="l.label" (ngModelChange)="onAppearanceChange()" />
1386
1406
  </mat-form-field>
1407
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1408
+ <input matInput [(ngModel)]="l.icon" (ngModelChange)="onAppearanceChange()" />
1409
+ </mat-form-field>
1387
1410
  </div>
1388
1411
  <div class="editor-row">
1389
1412
  <mat-slide-toggle [(ngModel)]="l.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.linkDisabled', 'Desativado') }}</mat-slide-toggle>
1413
+ <mat-slide-toggle [ngModel]="l.visible !== false" (ngModelChange)="l.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1390
1414
  <mat-slide-toggle [(ngModel)]="l.disableRipple" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disableRipple', 'Sem ripple') }}</mat-slide-toggle>
1391
1415
  <mat-slide-toggle [(ngModel)]="l.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.fitInkBarToContent', 'Indicador ajustado ao conteudo') }}</mat-slide-toggle>
1392
1416
  </div>
@@ -1797,6 +1821,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1797
1821
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1798
1822
  <input matInput [(ngModel)]="tab.textLabel" (ngModelChange)="onAppearanceChange()" />
1799
1823
  </mat-form-field>
1824
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1825
+ <input matInput [(ngModel)]="tab.icon" (ngModelChange)="onAppearanceChange()" />
1826
+ </mat-form-field>
1800
1827
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.labelClass', 'Classe do rotulo') }}</mat-label>
1801
1828
  <input matInput [(ngModel)]="tab.labelClass" (ngModelChange)="onAppearanceChange()" />
1802
1829
  </mat-form-field>
@@ -1810,7 +1837,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1810
1837
  <input matInput [(ngModel)]="tab.ariaLabelledby" (ngModelChange)="onAppearanceChange()" />
1811
1838
  </mat-form-field>
1812
1839
  </div>
1813
- <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1840
+ <div class="editor-row">
1841
+ <mat-slide-toggle [(ngModel)]="tab.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disabled', 'Desativada') }}</mat-slide-toggle>
1842
+ <mat-slide-toggle [ngModel]="tab.visible !== false" (ngModelChange)="tab.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1843
+ </div>
1814
1844
 
1815
1845
  <!-- Widgets (componentes dinâmicos) -->
1816
1846
  <div class="editor-divider editor-grid">
@@ -1888,9 +1918,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1888
1918
  <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.label', 'Rotulo') }}</mat-label>
1889
1919
  <input matInput [(ngModel)]="l.label" (ngModelChange)="onAppearanceChange()" />
1890
1920
  </mat-form-field>
1921
+ <mat-form-field appearance="outline"><mat-label>{{ t('editor.fields.icon', 'Icone') }}</mat-label>
1922
+ <input matInput [(ngModel)]="l.icon" (ngModelChange)="onAppearanceChange()" />
1923
+ </mat-form-field>
1891
1924
  </div>
1892
1925
  <div class="editor-row">
1893
1926
  <mat-slide-toggle [(ngModel)]="l.disabled" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.linkDisabled', 'Desativado') }}</mat-slide-toggle>
1927
+ <mat-slide-toggle [ngModel]="l.visible !== false" (ngModelChange)="l.visible = $event; onAppearanceChange()">{{ t('editor.toggles.visible', 'Visivel') }}</mat-slide-toggle>
1894
1928
  <mat-slide-toggle [(ngModel)]="l.disableRipple" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.disableRipple', 'Sem ripple') }}</mat-slide-toggle>
1895
1929
  <mat-slide-toggle [(ngModel)]="l.fitInkBarToContent" (ngModelChange)="onAppearanceChange()">{{ t('editor.toggles.fitInkBarToContent', 'Indicador ajustado ao conteudo') }}</mat-slide-toggle>
1896
1930
  </div>
@@ -1949,7 +1983,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1949
1983
  }], ctorParameters: () => [{ type: undefined, decorators: [{
1950
1984
  type: Inject,
1951
1985
  args: [SETTINGS_PANEL_DATA]
1952
- }] }, { type: i1.ComponentMetadataRegistry }] });
1986
+ }] }, { type: i1.ComponentMetadataRegistry }], propDecorators: { document: [{
1987
+ type: Input
1988
+ }] } });
1953
1989
 
1954
1990
  class TabsQuickSetupComponent {
1955
1991
  i18n = inject(PraxisI18nService);
@@ -2260,6 +2296,8 @@ const TABS_AI_CAPABILITIES = {
2260
2296
  class TabsAiAdapter extends BaseAiAdapter {
2261
2297
  tabs;
2262
2298
  componentName = 'Praxis Tabs';
2299
+ componentId = 'praxis-tabs';
2300
+ componentType = 'tabs';
2263
2301
  constructor(tabs) {
2264
2302
  super();
2265
2303
  this.tabs = tabs;
@@ -2270,6 +2308,49 @@ class TabsAiAdapter extends BaseAiAdapter {
2270
2308
  getCapabilities() {
2271
2309
  return TABS_AI_CAPABILITIES.capabilities;
2272
2310
  }
2311
+ getDataProfile() {
2312
+ const cfg = this.tabs.config;
2313
+ return {
2314
+ mode: cfg?.nav?.links?.length ? 'nav' : 'group',
2315
+ tabCount: cfg?.tabs?.length ?? 0,
2316
+ linkCount: cfg?.nav?.links?.length ?? 0,
2317
+ widgetCount: [
2318
+ ...(cfg?.tabs ?? []).flatMap((tab) => tab.widgets ?? []),
2319
+ ...(cfg?.nav?.links ?? []).flatMap((link) => link.widgets ?? []),
2320
+ ].length,
2321
+ fieldCount: [
2322
+ ...(cfg?.tabs ?? []).flatMap((tab) => tab.content ?? []),
2323
+ ...(cfg?.nav?.links ?? []).flatMap((link) => link.content ?? []),
2324
+ ].length,
2325
+ };
2326
+ }
2327
+ getSchemaFields() {
2328
+ const cfg = this.tabs.config;
2329
+ return [
2330
+ ...(cfg?.tabs ?? []).flatMap((tab) => tab.content ?? []),
2331
+ ...(cfg?.nav?.links ?? []).flatMap((link) => link.content ?? []),
2332
+ ]
2333
+ .map((field) => field?.name || field?.key || field?.id)
2334
+ .filter((name) => typeof name === 'string' && !!name.trim())
2335
+ .map((name) => ({ name }));
2336
+ }
2337
+ getAuthoringContext() {
2338
+ return {
2339
+ authoringManifestRef: {
2340
+ componentId: 'praxis-tabs',
2341
+ source: 'PRAXIS_TABS_AUTHORING_MANIFEST',
2342
+ },
2343
+ runtimeAuthoringPolicy: {
2344
+ mode: 'agentic-authoring',
2345
+ enableCustomization: !!this.tabs.enableCustomization,
2346
+ canApplyLocalPatch: false,
2347
+ reason: 'praxis-tabs ainda exige componentEditPlan manifest-backed antes de aplicar patch local pelo copiloto.',
2348
+ },
2349
+ domainCatalog: {
2350
+ recommendedAuthoringFlow: 'component_authoring',
2351
+ },
2352
+ };
2353
+ }
2273
2354
  getRuntimeState() {
2274
2355
  const cfg = this.tabs.config;
2275
2356
  return {
@@ -2355,6 +2436,340 @@ class TabsAiAdapter extends BaseAiAdapter {
2355
2436
  }
2356
2437
  }
2357
2438
 
2439
+ class TabsAgenticAuthoringTurnFlow {
2440
+ adapter;
2441
+ aiApi;
2442
+ mode = 'agentic-authoring';
2443
+ constructor(adapter, aiApi) {
2444
+ this.adapter = adapter;
2445
+ this.aiApi = aiApi;
2446
+ }
2447
+ async submit(request) {
2448
+ const prompt = (request.prompt ?? '').trim();
2449
+ if (!prompt) {
2450
+ return {
2451
+ state: 'listening',
2452
+ phase: 'capture',
2453
+ statusText: '',
2454
+ };
2455
+ }
2456
+ const componentId = this.adapter.componentId || request.componentId || 'praxis-tabs';
2457
+ const componentType = this.adapter.componentType || request.componentType || 'tabs';
2458
+ const currentState = this.toAiJsonObject(this.adapter.getCurrentConfig());
2459
+ const dataProfile = this.optionalJsonObject(this.adapter.getDataProfile?.());
2460
+ const runtimeState = this.optionalJsonObject(this.adapter.getRuntimeState?.());
2461
+ const schemaFields = this.adapter.getSchemaFields?.()
2462
+ ?.map((field) => this.toAiJsonObject(field))
2463
+ .filter((field) => Object.keys(field).length > 0);
2464
+ const contextHints = this.optionalJsonObject(this.adapter.getAuthoringContext?.());
2465
+ if (this.shouldRouteToGovernedDecision(prompt, contextHints)) {
2466
+ return this.toGovernedDecisionHandoff(prompt, request);
2467
+ }
2468
+ const response = await firstValueFrom(this.aiApi.getPatch({
2469
+ componentId,
2470
+ componentType,
2471
+ userPrompt: prompt,
2472
+ sessionId: request.sessionId,
2473
+ clientTurnId: request.clientTurnId,
2474
+ messages: this.toChatMessages(request.messages, prompt),
2475
+ currentState,
2476
+ currentStateDigest: this.buildCurrentStateDigest(dataProfile),
2477
+ uiContextRef: {
2478
+ componentId,
2479
+ componentType,
2480
+ },
2481
+ ...(dataProfile ? { dataProfile } : {}),
2482
+ ...(runtimeState ? { runtimeState } : {}),
2483
+ ...(schemaFields?.length ? { schemaFields } : {}),
2484
+ ...(contextHints ? { contextHints } : {}),
2485
+ }));
2486
+ return this.toTurnResult(this.compileAdapterResponse(response), request);
2487
+ }
2488
+ async apply(_request) {
2489
+ return {
2490
+ state: 'error',
2491
+ phase: 'apply',
2492
+ assistantMessage: 'As abas ainda exigem componentEditPlan validado pelo manifesto antes de aplicar mudancas locais.',
2493
+ errorText: 'Aplicacao local bloqueada ate existir compilacao manifest-backed para praxis-tabs.',
2494
+ canApply: false,
2495
+ pendingPatch: null,
2496
+ };
2497
+ }
2498
+ cancel() {
2499
+ return Promise.resolve({
2500
+ state: 'listening',
2501
+ phase: 'capture',
2502
+ assistantMessage: 'Solicitacao cancelada.',
2503
+ statusText: '',
2504
+ canApply: false,
2505
+ pendingPatch: null,
2506
+ pendingClarification: null,
2507
+ });
2508
+ }
2509
+ retry(request) {
2510
+ const lastPrompt = [...(request.messages ?? [])].reverse()
2511
+ .find((message) => message.role === 'user')?.text;
2512
+ return this.submit({
2513
+ ...request,
2514
+ prompt: lastPrompt ?? request.prompt,
2515
+ action: { kind: 'retry' },
2516
+ });
2517
+ }
2518
+ toTurnResult(response, request) {
2519
+ if (!response) {
2520
+ return {
2521
+ state: 'error',
2522
+ phase: 'capture',
2523
+ assistantMessage: 'Resposta vazia da IA.',
2524
+ errorText: 'Resposta vazia da IA.',
2525
+ };
2526
+ }
2527
+ if (response.type === 'clarification') {
2528
+ return {
2529
+ state: 'clarification',
2530
+ phase: 'clarify',
2531
+ sessionId: response.sessionId ?? request.sessionId,
2532
+ assistantMessage: response.message || 'Preciso de mais detalhes para continuar.',
2533
+ clarificationQuestions: this.toClarificationQuestions(response),
2534
+ quickReplies: this.toQuickReplies(response),
2535
+ canApply: false,
2536
+ };
2537
+ }
2538
+ if (response.type === 'info') {
2539
+ const message = response.message || response.explanation || 'Informacao gerada.';
2540
+ return {
2541
+ state: 'success',
2542
+ phase: 'summarize',
2543
+ sessionId: response.sessionId ?? request.sessionId,
2544
+ assistantMessage: message,
2545
+ statusText: message,
2546
+ canApply: false,
2547
+ };
2548
+ }
2549
+ if (response.type === 'error') {
2550
+ const message = response.message || 'Falha ao gerar alteracao de abas.';
2551
+ return {
2552
+ state: 'error',
2553
+ phase: 'capture',
2554
+ sessionId: response.sessionId ?? request.sessionId,
2555
+ assistantMessage: message,
2556
+ errorText: message,
2557
+ diagnostics: response.warnings?.length ? { warnings: response.warnings } : undefined,
2558
+ };
2559
+ }
2560
+ if (response.patch && Object.keys(response.patch).length > 0) {
2561
+ return {
2562
+ state: 'error',
2563
+ phase: 'review',
2564
+ sessionId: response.sessionId ?? request.sessionId,
2565
+ assistantMessage: 'As abas rejeitaram patch livre. Gere um componentEditPlan validado pelo PRAXIS_TABS_AUTHORING_MANIFEST antes de propor alteracao local.',
2566
+ errorText: 'Patch livre de abas rejeitado.',
2567
+ canApply: false,
2568
+ pendingPatch: null,
2569
+ diagnostics: {
2570
+ warnings: [
2571
+ 'free-tabs-patch-rejected',
2572
+ 'Use componentEditPlan validado contra PRAXIS_TABS_AUTHORING_MANIFEST.',
2573
+ ],
2574
+ },
2575
+ };
2576
+ }
2577
+ return {
2578
+ state: 'success',
2579
+ phase: 'summarize',
2580
+ sessionId: response.sessionId ?? request.sessionId,
2581
+ assistantMessage: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
2582
+ statusText: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
2583
+ canApply: false,
2584
+ };
2585
+ }
2586
+ compileAdapterResponse(response) {
2587
+ const compiled = this.adapter.compileAiResponse?.(response);
2588
+ if (!compiled) {
2589
+ return response;
2590
+ }
2591
+ if (compiled.type === 'error') {
2592
+ return {
2593
+ type: 'error',
2594
+ message: compiled.message || 'O componentEditPlan das abas nao passou na validacao de capacidades.',
2595
+ warnings: compiled.warnings,
2596
+ };
2597
+ }
2598
+ const warnings = [
2599
+ ...(response.warnings ?? []),
2600
+ ...(compiled.warnings ?? []),
2601
+ ];
2602
+ return {
2603
+ ...response,
2604
+ ...compiled,
2605
+ patch: compiled.patch,
2606
+ warnings: warnings.length ? warnings : undefined,
2607
+ };
2608
+ }
2609
+ toChatMessages(messages, prompt) {
2610
+ const supported = (messages ?? [])
2611
+ .filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'system')
2612
+ .map((message) => ({
2613
+ role: message.role,
2614
+ content: message.text,
2615
+ }))
2616
+ .filter((message) => message.content.trim().length > 0);
2617
+ return supported.length ? supported : [{ role: 'user', content: prompt }];
2618
+ }
2619
+ toClarificationQuestions(response) {
2620
+ const labels = response.questions?.length
2621
+ ? response.questions
2622
+ : response.message
2623
+ ? [response.message]
2624
+ : ['Qual ajuste voce quer aplicar nas abas?'];
2625
+ const options = this.toQuickReplies(response).map((reply) => ({
2626
+ id: reply.id,
2627
+ label: reply.label,
2628
+ value: reply.prompt,
2629
+ }));
2630
+ return labels.map((label, index) => ({
2631
+ id: `tabs-clarification-${index + 1}`,
2632
+ type: options.length ? 'single-choice' : 'text',
2633
+ label,
2634
+ allowCustom: true,
2635
+ options,
2636
+ }));
2637
+ }
2638
+ toQuickReplies(response) {
2639
+ const payloads = response.optionPayloads ?? [];
2640
+ if (payloads.length) {
2641
+ return payloads
2642
+ .map((option, index) => {
2643
+ const label = option.label?.trim() || option.value?.trim() || `Opcao ${index + 1}`;
2644
+ const prompt = option.example?.trim() || option.value?.trim() || label;
2645
+ return {
2646
+ id: `option-${index + 1}`,
2647
+ label,
2648
+ prompt,
2649
+ kind: 'clarification-option',
2650
+ };
2651
+ });
2652
+ }
2653
+ return (response.options ?? [])
2654
+ .filter((option) => !!option?.trim())
2655
+ .map((option, index) => ({
2656
+ id: `option-${index + 1}`,
2657
+ label: option.trim(),
2658
+ prompt: option.trim(),
2659
+ kind: 'clarification-option',
2660
+ }));
2661
+ }
2662
+ buildCurrentStateDigest(dataProfile) {
2663
+ const tabCount = typeof dataProfile?.['tabCount'] === 'number' ? dataProfile['tabCount'] : undefined;
2664
+ const linkCount = typeof dataProfile?.['linkCount'] === 'number' ? dataProfile['linkCount'] : undefined;
2665
+ const rowCount = (tabCount ?? 0) + (linkCount ?? 0);
2666
+ return rowCount > 0 ? { rowCount } : {};
2667
+ }
2668
+ shouldRouteToGovernedDecision(prompt, contextHints) {
2669
+ const normalized = prompt.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
2670
+ const recommendedFlow = this.toRecord(contextHints?.['domainCatalog'])?.['recommendedAuthoringFlow'];
2671
+ if (recommendedFlow === 'shared_rule_authoring')
2672
+ return true;
2673
+ return [
2674
+ 'regra',
2675
+ 'politica',
2676
+ 'policy',
2677
+ 'compliance',
2678
+ 'lgpd',
2679
+ 'privacidade',
2680
+ 'aprovacao',
2681
+ 'aprovar',
2682
+ 'publicar',
2683
+ 'materializar',
2684
+ 'enforcement',
2685
+ 'validacao de negocio',
2686
+ 'validar negocio',
2687
+ 'elegibilidade',
2688
+ 'permissao',
2689
+ 'acesso',
2690
+ ].some((term) => normalized.includes(term));
2691
+ }
2692
+ toGovernedDecisionHandoff(prompt, request) {
2693
+ 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.';
2694
+ return {
2695
+ state: 'clarification',
2696
+ phase: 'clarify',
2697
+ sessionId: request.sessionId,
2698
+ assistantMessage: message,
2699
+ statusText: 'Handoff governado necessario.',
2700
+ canApply: false,
2701
+ quickReplies: [
2702
+ {
2703
+ id: 'shared-rule-handoff',
2704
+ label: 'Continuar como regra governada',
2705
+ prompt,
2706
+ kind: 'shared-rule-handoff',
2707
+ description: 'Criar intake de domain-rules em vez de aplicar patch local nas abas.',
2708
+ icon: 'rule',
2709
+ tone: 'warning',
2710
+ contextHints: {
2711
+ flowId: 'shared_rule_authoring',
2712
+ source: 'praxis-tabs',
2713
+ recommendedAction: 'domain-rules/intake',
2714
+ },
2715
+ },
2716
+ ],
2717
+ clarificationQuestions: [
2718
+ {
2719
+ id: 'tabs-governed-rule-confirmation',
2720
+ type: 'confirm',
2721
+ label: 'Deseja continuar pelo fluxo governado de regras compartilhadas?',
2722
+ description: 'Esse caminho permite intake, simulacao, aprovacao/publicacao, materializacao e validacao de enforcement.',
2723
+ required: true,
2724
+ options: [
2725
+ {
2726
+ id: 'shared-rule-handoff',
2727
+ label: 'Sim, continuar governado',
2728
+ value: prompt,
2729
+ description: 'Nao aplicar como patch local das abas.',
2730
+ contextHints: {
2731
+ flowId: 'shared_rule_authoring',
2732
+ source: 'praxis-tabs',
2733
+ },
2734
+ },
2735
+ ],
2736
+ },
2737
+ ],
2738
+ diagnostics: {
2739
+ governedDecisionHandoff: {
2740
+ flowId: 'shared_rule_authoring',
2741
+ sourcePrompt: prompt,
2742
+ sourceComponent: 'praxis-tabs',
2743
+ },
2744
+ },
2745
+ };
2746
+ }
2747
+ optionalJsonObject(value) {
2748
+ if (value === undefined || value === null) {
2749
+ return undefined;
2750
+ }
2751
+ const object = this.toAiJsonObject(value);
2752
+ return Object.keys(object).length ? object : undefined;
2753
+ }
2754
+ toAiJsonObject(value) {
2755
+ const record = this.toRecord(value);
2756
+ if (!record) {
2757
+ return {};
2758
+ }
2759
+ try {
2760
+ return JSON.parse(JSON.stringify(record));
2761
+ }
2762
+ catch {
2763
+ return {};
2764
+ }
2765
+ }
2766
+ toRecord(value) {
2767
+ return value && typeof value === 'object' && !Array.isArray(value)
2768
+ ? value
2769
+ : null;
2770
+ }
2771
+ }
2772
+
2358
2773
  class PraxisTabs {
2359
2774
  i18n = inject(PraxisI18nService);
2360
2775
  settings = inject(SettingsPanelService);
@@ -2362,16 +2777,34 @@ class PraxisTabs {
2362
2777
  snack = inject(MatSnackBar);
2363
2778
  componentKeys = inject(ComponentKeyService);
2364
2779
  logger = inject(LoggerService);
2780
+ cdr = inject(ChangeDetectorRef);
2781
+ aiApi = inject(AiBackendApiService);
2782
+ assistantSessions = inject(PraxisAssistantSessionRegistryService);
2783
+ aiTurnOrchestrator = inject(PraxisAssistantTurnOrchestratorService);
2365
2784
  route = (() => { try {
2366
2785
  return inject(ActivatedRoute);
2367
2786
  }
2368
2787
  catch {
2369
2788
  return undefined;
2370
2789
  } })();
2790
+ aiAssistantSessionEffect = effect(() => {
2791
+ const session = this.assistantSessions.activeSession();
2792
+ if (!session || session.id !== this.resolveAiAssistantSessionId())
2793
+ return;
2794
+ if (!this.aiAssistantOpen) {
2795
+ this.openAiAssistantFromSession(session);
2796
+ }
2797
+ }, ...(ngDevMode ? [{ debugName: "aiAssistantSessionEffect" }] : []));
2371
2798
  warnedMissingId = false;
2372
2799
  config = null;
2373
2800
  tabsId;
2374
2801
  componentInstanceId;
2802
+ set selectedIndex(index) {
2803
+ if (index == null)
2804
+ return;
2805
+ this.controlledSelectedIndex = index;
2806
+ this.applySelectedIndex(index, false, false);
2807
+ }
2375
2808
  enableCustomization = false;
2376
2809
  form = null;
2377
2810
  context = null;
@@ -2383,11 +2816,27 @@ class PraxisTabs {
2383
2816
  selectFocusedIndex = new EventEmitter();
2384
2817
  widgetEvent = new EventEmitter();
2385
2818
  aiAdapter = new TabsAiAdapter(this);
2819
+ aiAssistantOpen = false;
2820
+ aiAssistantPrompt = '';
2821
+ aiAssistantViewState = null;
2822
+ aiAssistantLayout = createPraxisAssistantViewportLayout();
2823
+ aiAssistantLabels = {
2824
+ title: 'Copiloto semantico Praxis',
2825
+ subtitle: 'Converse, revise e governe ajustes das abas.',
2826
+ prompt: 'Mensagem',
2827
+ promptPlaceholder: 'Descreva o ajuste que voce precisa nas abas.',
2828
+ emptyConversation: 'Diga o que voce quer alterar nas abas.',
2829
+ submit: 'Interpretar pedido',
2830
+ apply: 'Aplicar ajuste',
2831
+ };
2832
+ aiAssistantController = null;
2833
+ aiAssistantStateSubscription = null;
2386
2834
  // Signals to manage local state for selection in Nav mode and Group mode
2387
2835
  currentNavIndex = signal(0, ...(ngDevMode ? [{ debugName: "currentNavIndex" }] : []));
2388
2836
  selectedIndexSignal = signal(0, ...(ngDevMode ? [{ debugName: "selectedIndexSignal" }] : []));
2389
2837
  groupLoaded = new Set();
2390
2838
  navLoaded = new Set();
2839
+ controlledSelectedIndex;
2391
2840
  destroy$ = new Subject();
2392
2841
  widgetDefinitionCache = new WeakMap();
2393
2842
  ngOnInit() {
@@ -2400,6 +2849,7 @@ class PraxisTabs {
2400
2849
  this.config = stored;
2401
2850
  }
2402
2851
  this.syncSelectionFromConfig();
2852
+ this.reapplyControlledSelectedIndex();
2403
2853
  });
2404
2854
  }
2405
2855
  }
@@ -2409,9 +2859,12 @@ class PraxisTabs {
2409
2859
  this.syncSelectionFromConfig();
2410
2860
  // Persist when tabsId provided
2411
2861
  this.persistConfig(this.config);
2862
+ this.reapplyControlledSelectedIndex();
2412
2863
  }
2413
2864
  }
2414
2865
  ngOnDestroy() {
2866
+ this.assistantSessions.removeContextSession(this.buildAiAssistantContextSnapshot().identity);
2867
+ this.aiAssistantStateSubscription?.unsubscribe();
2415
2868
  this.destroy$.next();
2416
2869
  this.destroy$.complete();
2417
2870
  }
@@ -2427,23 +2880,39 @@ class PraxisTabs {
2427
2880
  getNavActive(i) {
2428
2881
  return this.currentNavIndex() === i;
2429
2882
  }
2883
+ visibleNavLinkEntries() {
2884
+ return (this.config?.nav?.links ?? [])
2885
+ .map((link, index) => ({ link, index }))
2886
+ .filter((entry) => entry.link.visible !== false);
2887
+ }
2888
+ visibleTabEntries() {
2889
+ return (this.config?.tabs ?? [])
2890
+ .map((tab, index) => ({ tab, index }))
2891
+ .filter((entry) => entry.tab.visible !== false);
2892
+ }
2893
+ selectedVisibleNavIndex() {
2894
+ const entries = this.visibleNavLinkEntries();
2895
+ const index = entries.findIndex((entry) => entry.index === this.currentNavIndex());
2896
+ return index >= 0 ? index : 0;
2897
+ }
2898
+ selectedVisibleTabIndex() {
2899
+ const entries = this.visibleTabEntries();
2900
+ const index = entries.findIndex((entry) => entry.index === this.selectedIndexSignal());
2901
+ return index >= 0 ? index : 0;
2902
+ }
2903
+ onVisibleTabIndexChange(index) {
2904
+ const entry = this.visibleTabEntries()[index];
2905
+ if (!entry)
2906
+ return;
2907
+ this.onSelectedIndexChange(entry.index);
2908
+ }
2430
2909
  onNavClick(i) {
2431
2910
  if (!this.config?.nav?.links?.length)
2432
2911
  return;
2433
2912
  const linksCount = this.config.nav.links.length;
2434
2913
  if (i < 0 || i >= linksCount)
2435
2914
  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);
2915
+ this.applySelectedIndex(i, true);
2447
2916
  }
2448
2917
  onNavDrop(event) {
2449
2918
  if (!this.config?.nav?.links)
@@ -2491,10 +2960,39 @@ class PraxisTabs {
2491
2960
  });
2492
2961
  this.persistConfig(this.config);
2493
2962
  }
2963
+ onVisibleNavDrop(event) {
2964
+ const entries = this.visibleNavLinkEntries();
2965
+ const previous = entries[event.previousIndex];
2966
+ const current = entries[event.currentIndex];
2967
+ if (!previous || !current)
2968
+ return;
2969
+ this.onNavDrop({
2970
+ ...event,
2971
+ previousIndex: previous.index,
2972
+ currentIndex: current.index,
2973
+ });
2974
+ }
2494
2975
  onSelectedIndexChange(index) {
2976
+ this.applySelectedIndex(index, true);
2977
+ }
2978
+ applySelectedIndex(index, emit, persist = true) {
2979
+ if (this.isNavMode() && this.config) {
2980
+ const selected = this.clampIndex(index, this.config?.nav?.links?.length ?? 0);
2981
+ this.currentNavIndex.set(selected);
2982
+ this.config = produce(this.config, (draft) => {
2983
+ draft.nav.selectedIndex = selected;
2984
+ });
2985
+ if (persist) {
2986
+ this.persistConfig(this.config);
2987
+ }
2988
+ this.navLoaded.add(selected);
2989
+ if (emit) {
2990
+ this.selectedIndexChange.emit(selected);
2991
+ }
2992
+ return;
2993
+ }
2495
2994
  const selected = this.clampIndex(index, this.config?.tabs?.length ?? 0);
2496
2995
  this.selectedIndexSignal.set(selected);
2497
- // Update config immutably
2498
2996
  if (this.config) {
2499
2997
  this.config = produce(this.config, (draft) => {
2500
2998
  if (!draft.group) {
@@ -2504,11 +3002,20 @@ class PraxisTabs {
2504
3002
  draft.group.selectedIndex = selected;
2505
3003
  }
2506
3004
  });
2507
- this.persistConfig(this.config);
3005
+ if (persist) {
3006
+ this.persistConfig(this.config);
3007
+ }
2508
3008
  }
2509
- // Lazy: mark as loaded
2510
3009
  this.groupLoaded.add(selected);
2511
- this.selectedIndexChange.emit(selected);
3010
+ if (emit) {
3011
+ this.selectedIndexChange.emit(selected);
3012
+ }
3013
+ }
3014
+ reapplyControlledSelectedIndex() {
3015
+ if (this.controlledSelectedIndex == null) {
3016
+ return;
3017
+ }
3018
+ this.applySelectedIndex(this.controlledSelectedIndex, false, false);
2512
3019
  }
2513
3020
  closeTab(index) {
2514
3021
  if (!this.config?.tabs)
@@ -2596,6 +3103,305 @@ class PraxisTabs {
2596
3103
  ref.applied$.pipe(takeUntil(this.destroy$)).subscribe(applyDocument);
2597
3104
  ref.saved$.pipe(takeUntil(this.destroy$)).subscribe(applyDocument);
2598
3105
  }
3106
+ openAiAssistant() {
3107
+ this.initializeAiAssistantController();
3108
+ this.aiAssistantOpen = true;
3109
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
3110
+ this.syncAiAssistantSession('active');
3111
+ this.cdr.markForCheck();
3112
+ }
3113
+ openAiAssistantFromSession(session) {
3114
+ if (session.id !== this.resolveAiAssistantSessionId())
3115
+ return;
3116
+ this.initializeAiAssistantController();
3117
+ this.aiAssistantOpen = true;
3118
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
3119
+ this.syncAiAssistantSession('active');
3120
+ this.cdr.markForCheck();
3121
+ }
3122
+ closeAiAssistant() {
3123
+ this.aiAssistantOpen = false;
3124
+ this.syncAiAssistantSession('minimized');
3125
+ this.cdr.markForCheck();
3126
+ }
3127
+ onAiAssistantPromptChange(prompt) {
3128
+ this.aiAssistantPrompt = prompt;
3129
+ this.syncAiAssistantSession();
3130
+ }
3131
+ onAiAssistantSubmit(prompt) {
3132
+ this.aiAssistantController?.submitPrompt(prompt).subscribe((state) => {
3133
+ this.aiAssistantPrompt = '';
3134
+ this.aiAssistantViewState = state;
3135
+ this.syncAiAssistantSession();
3136
+ this.cdr.markForCheck();
3137
+ });
3138
+ }
3139
+ onAiAssistantApply() {
3140
+ this.aiAssistantController?.apply().subscribe((state) => {
3141
+ this.aiAssistantViewState = state;
3142
+ this.syncAiAssistantSession();
3143
+ this.cdr.markForCheck();
3144
+ });
3145
+ }
3146
+ onAiAssistantRetry() {
3147
+ this.aiAssistantController?.retry().subscribe((state) => {
3148
+ this.aiAssistantViewState = state;
3149
+ this.syncAiAssistantSession();
3150
+ this.cdr.markForCheck();
3151
+ });
3152
+ }
3153
+ onAiAssistantCancel() {
3154
+ this.aiAssistantController?.cancel().subscribe((state) => {
3155
+ this.aiAssistantPrompt = '';
3156
+ this.aiAssistantViewState = state;
3157
+ this.syncAiAssistantSession();
3158
+ this.cdr.markForCheck();
3159
+ });
3160
+ }
3161
+ onAiAssistantQuickReply(reply) {
3162
+ const controller = this.aiAssistantController;
3163
+ if (!controller)
3164
+ return;
3165
+ const state = controller.snapshot();
3166
+ const next$ = state.state === 'clarification'
3167
+ ? controller.answerClarification(reply.prompt)
3168
+ : controller.submitPrompt(reply.prompt, {
3169
+ kind: reply.kind || 'quick-reply',
3170
+ id: reply.id,
3171
+ value: reply.prompt,
3172
+ });
3173
+ next$.subscribe((nextState) => {
3174
+ this.aiAssistantPrompt = '';
3175
+ this.aiAssistantViewState = nextState;
3176
+ this.syncAiAssistantSession();
3177
+ this.cdr.markForCheck();
3178
+ });
3179
+ }
3180
+ onAiAssistantEditMessage(message) {
3181
+ this.aiAssistantPrompt = message.text;
3182
+ this.cdr.markForCheck();
3183
+ }
3184
+ onAiAssistantResendMessage(message) {
3185
+ this.aiAssistantController?.resendMessage(message.id).subscribe((state) => {
3186
+ this.aiAssistantPrompt = '';
3187
+ this.aiAssistantViewState = state;
3188
+ this.syncAiAssistantSession();
3189
+ this.cdr.markForCheck();
3190
+ });
3191
+ }
3192
+ onAiAssistantLayoutChange(layout) {
3193
+ this.aiAssistantLayout = layout;
3194
+ }
3195
+ initializeAiAssistantController() {
3196
+ if (this.aiAssistantController)
3197
+ return;
3198
+ const flow = new TabsAgenticAuthoringTurnFlow(this.aiAdapter, this.aiApi);
3199
+ const controller = this.aiTurnOrchestrator.createController(flow, {
3200
+ componentId: this.aiAdapter.componentId || 'praxis-tabs',
3201
+ componentType: this.aiAdapter.componentType || 'tabs',
3202
+ contextItems: this.buildAiAssistantContextItems(),
3203
+ });
3204
+ this.aiAssistantController = controller;
3205
+ this.aiAssistantViewState = controller.snapshot();
3206
+ this.aiAssistantStateSubscription?.unsubscribe();
3207
+ this.aiAssistantStateSubscription = controller.state$.subscribe((state) => {
3208
+ this.aiAssistantViewState = state;
3209
+ this.syncAiAssistantSession();
3210
+ this.cdr.markForCheck();
3211
+ });
3212
+ this.cdr.markForCheck();
3213
+ }
3214
+ buildAiAssistantContextItems() {
3215
+ const items = [
3216
+ {
3217
+ id: 'component',
3218
+ label: 'Componente',
3219
+ value: 'Abas',
3220
+ kind: 'component',
3221
+ icon: 'tab',
3222
+ },
3223
+ {
3224
+ id: 'mode',
3225
+ label: 'Modo',
3226
+ value: this.isNavMode() ? 'nav' : 'group',
3227
+ kind: 'custom',
3228
+ icon: 'account_tree',
3229
+ },
3230
+ ];
3231
+ if (this.tabsId) {
3232
+ items.push({
3233
+ id: 'tabs-id',
3234
+ label: 'Tabs',
3235
+ value: this.tabsId,
3236
+ kind: 'custom',
3237
+ icon: 'tag',
3238
+ });
3239
+ }
3240
+ return items;
3241
+ }
3242
+ buildAiAssistantContextSnapshot() {
3243
+ const fieldNames = this.collectContentFieldNames();
3244
+ const counts = this.collectTabsCounts();
3245
+ return {
3246
+ identity: {
3247
+ sessionId: this.resolveAiAssistantSessionId(),
3248
+ ownerId: this.resolveAiAssistantOwnerId(),
3249
+ ownerType: 'tabs',
3250
+ componentId: 'praxis-tabs',
3251
+ componentType: 'tabs',
3252
+ routeKey: this.resolveAiAssistantRouteKey(),
3253
+ },
3254
+ target: {
3255
+ kind: 'component',
3256
+ id: this.resolveAiAssistantOwnerId(),
3257
+ label: this.tabsId || 'Abas',
3258
+ metadata: {
3259
+ mode: this.isNavMode() ? 'nav' : 'group',
3260
+ hasCustomization: !!this.enableCustomization,
3261
+ },
3262
+ },
3263
+ contextItems: this.buildAiAssistantContextItems().map((item) => ({
3264
+ id: item.id,
3265
+ label: item.label,
3266
+ value: item.value || '',
3267
+ kind: item.kind,
3268
+ })),
3269
+ mode: 'agentic-authoring',
3270
+ authoringManifestRef: {
3271
+ componentId: 'praxis-tabs',
3272
+ source: 'PRAXIS_TABS_AUTHORING_MANIFEST',
3273
+ },
3274
+ schemaFields: fieldNames.length ? fieldNames : undefined,
3275
+ dataProfileDigest: {
3276
+ summary: `${counts.tabCount} aba(s), ${counts.linkCount} link(s), ${counts.widgetCount} widget(s), ${counts.fieldCount} campo(s)`,
3277
+ counts,
3278
+ },
3279
+ runtimeStateDigest: {
3280
+ summary: this.isNavMode()
3281
+ ? `Nav ativo no indice ${this.currentNavIndex()}`
3282
+ : `Grupo ativo no indice ${this.selectedIndexSignal()}`,
3283
+ fields: [
3284
+ this.isNavMode() ? 'nav.links' : 'tabs',
3285
+ 'selectedIndex',
3286
+ 'widgetEvent',
3287
+ ],
3288
+ },
3289
+ capabilityRefs: [
3290
+ {
3291
+ id: 'tabs.component-edit-plan',
3292
+ label: 'Plano de edicao de abas',
3293
+ source: 'PRAXIS_TABS_AUTHORING_MANIFEST',
3294
+ risk: 'medium',
3295
+ },
3296
+ ],
3297
+ governanceHints: [
3298
+ {
3299
+ kind: 'business-rule-boundary',
3300
+ label: 'Regras compartilhadas exigem governanca',
3301
+ reason: 'Politicas, validacoes reutilizaveis e compliance nao devem ser aplicados como patch local das abas.',
3302
+ risk: 'high',
3303
+ },
3304
+ ],
3305
+ };
3306
+ }
3307
+ syncAiAssistantSession(visibility = null) {
3308
+ if (!this.enableCustomization)
3309
+ return;
3310
+ if (!this.aiAssistantOpen && !this.hasAiAssistantSessionState())
3311
+ return;
3312
+ const state = this.aiAssistantViewState;
3313
+ this.assistantSessions.upsertContextSession(this.buildAiAssistantContextSnapshot(), {
3314
+ title: 'Copiloto semantico Praxis',
3315
+ summary: this.resolveAiAssistantSummary(),
3316
+ mode: state?.mode || 'agentic-authoring',
3317
+ state: state?.state || 'idle',
3318
+ visibility: visibility ?? (this.aiAssistantOpen ? 'active' : 'minimized'),
3319
+ badge: this.resolveAiAssistantBadge(),
3320
+ icon: this.resolveAiAssistantIcon(),
3321
+ });
3322
+ }
3323
+ hasAiAssistantSessionState() {
3324
+ return !!this.aiAssistantPrompt.trim()
3325
+ || !!this.aiAssistantViewState?.messages?.length
3326
+ || !!this.aiAssistantViewState?.quickReplies?.length
3327
+ || !!this.aiAssistantViewState?.pendingPatch
3328
+ || !!this.aiAssistantViewState?.statusText?.trim()
3329
+ || !!this.aiAssistantViewState?.errorText?.trim();
3330
+ }
3331
+ resolveAiAssistantSessionId() {
3332
+ return `tabs:${this.resolveAiAssistantRouteKey()}:${this.resolveAiAssistantOwnerId()}`;
3333
+ }
3334
+ resolveAiAssistantOwnerId() {
3335
+ return (this.componentInstanceId || this.tabsId || 'tabs').trim() || 'tabs';
3336
+ }
3337
+ resolveAiAssistantRouteKey() {
3338
+ const routePath = this.route?.snapshot?.routeConfig?.path?.trim();
3339
+ return routePath || 'local';
3340
+ }
3341
+ resolveAiAssistantSummary() {
3342
+ const status = this.aiAssistantViewState?.statusText?.trim();
3343
+ if (status)
3344
+ return status;
3345
+ const error = this.aiAssistantViewState?.errorText?.trim();
3346
+ if (error)
3347
+ return error;
3348
+ const prompt = this.aiAssistantPrompt.trim();
3349
+ if (prompt)
3350
+ return prompt.length > 96 ? `${prompt.slice(0, 93)}...` : prompt;
3351
+ const lastMessage = [...(this.aiAssistantViewState?.messages ?? [])].reverse()
3352
+ .find((message) => message.role === 'assistant' || message.role === 'user');
3353
+ if (lastMessage?.text) {
3354
+ return lastMessage.text.length > 96 ? `${lastMessage.text.slice(0, 93)}...` : lastMessage.text;
3355
+ }
3356
+ return this.isNavMode() ? 'Assistente contextual das abas de navegacao.' : 'Assistente contextual do grupo de abas.';
3357
+ }
3358
+ resolveAiAssistantBadge() {
3359
+ const state = this.aiAssistantViewState?.state;
3360
+ if (state === 'error')
3361
+ return 'erro';
3362
+ if (state === 'clarification')
3363
+ return 'revisar';
3364
+ if (state === 'review')
3365
+ return 'preview';
3366
+ if (state === 'success')
3367
+ return 'ok';
3368
+ return undefined;
3369
+ }
3370
+ resolveAiAssistantIcon() {
3371
+ const state = this.aiAssistantViewState?.state;
3372
+ if (state === 'error')
3373
+ return 'error';
3374
+ if (state === 'clarification')
3375
+ return 'rule';
3376
+ if (state === 'review')
3377
+ return 'rate_review';
3378
+ return 'auto_awesome';
3379
+ }
3380
+ collectContentFieldNames() {
3381
+ const fields = [
3382
+ ...(this.config?.tabs ?? []).flatMap((tab) => tab.content ?? []),
3383
+ ...(this.config?.nav?.links ?? []).flatMap((link) => link.content ?? []),
3384
+ ];
3385
+ return Array.from(new Set(fields
3386
+ .map((field) => field?.name || field?.key || field?.id)
3387
+ .filter((name) => typeof name === 'string' && !!name.trim())));
3388
+ }
3389
+ collectTabsCounts() {
3390
+ const tabs = this.config?.tabs ?? [];
3391
+ const links = this.config?.nav?.links ?? [];
3392
+ return {
3393
+ tabCount: tabs.length,
3394
+ linkCount: links.length,
3395
+ widgetCount: [
3396
+ ...tabs.flatMap((tab) => tab.widgets ?? []),
3397
+ ...links.flatMap((link) => link.widgets ?? []),
3398
+ ].length,
3399
+ fieldCount: [
3400
+ ...tabs.flatMap((tab) => tab.content ?? []),
3401
+ ...links.flatMap((link) => link.content ?? []),
3402
+ ].length,
3403
+ };
3404
+ }
2599
3405
  addEmptyTab() {
2600
3406
  const next = produce(this.config || {}, (draft) => {
2601
3407
  if (!draft.group)
@@ -2691,19 +3497,26 @@ class PraxisTabs {
2691
3497
  return id ? `tabs:${id}` : null;
2692
3498
  }
2693
3499
  syncSelectionFromConfig() {
2694
- this.groupLoaded.clear();
2695
- this.navLoaded.clear();
2696
3500
  const tabsLength = this.config?.tabs?.length ?? 0;
2697
3501
  const linksLength = this.config?.nav?.links?.length ?? 0;
2698
3502
  const groupIndex = this.clampIndex(this.config?.group?.selectedIndex, tabsLength);
2699
3503
  const navIndex = this.clampIndex(this.config?.nav?.selectedIndex, linksLength);
2700
3504
  this.selectedIndexSignal.set(groupIndex);
2701
3505
  this.currentNavIndex.set(navIndex);
3506
+ this.pruneLoadedIndexes(this.groupLoaded, tabsLength);
3507
+ this.pruneLoadedIndexes(this.navLoaded, linksLength);
2702
3508
  if (tabsLength > 0)
2703
3509
  this.groupLoaded.add(groupIndex);
2704
3510
  if (linksLength > 0)
2705
3511
  this.navLoaded.add(navIndex);
2706
3512
  }
3513
+ pruneLoadedIndexes(indexes, size) {
3514
+ for (const index of Array.from(indexes)) {
3515
+ if (index < 0 || index >= size) {
3516
+ indexes.delete(index);
3517
+ }
3518
+ }
3519
+ }
2707
3520
  persistConfig(config) {
2708
3521
  const key = this.storageKey();
2709
3522
  if (!key || !config)
@@ -2764,10 +3577,16 @@ class PraxisTabs {
2764
3577
  return !this.isLazy() || this.navLoaded.has(index) || this.currentNavIndex() === index;
2765
3578
  }
2766
3579
  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);
3580
+ const hasTabs = this.visibleTabEntries().length > 0;
3581
+ const hasLinks = this.visibleNavLinkEntries().length > 0;
2769
3582
  return !(hasTabs || hasLinks);
2770
3583
  }
3584
+ trackVisibleNavLink(index, entry) {
3585
+ return entry.link.id || `${entry.link.label || 'nav-link'}:${entry.index ?? index}`;
3586
+ }
3587
+ trackVisibleTab(index, entry) {
3588
+ return entry.tab.id || entry.tab.textLabel || `tab:${entry.index ?? index}`;
3589
+ }
2771
3590
  trackNavLink(index, link) {
2772
3591
  return link.id || `${link.label || 'nav-link'}:${index}`;
2773
3592
  }
@@ -2952,7 +3771,7 @@ class PraxisTabs {
2952
3771
  return JSON.parse(JSON.stringify(widget));
2953
3772
  }
2954
3773
  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: `
3774
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisTabs, isStandalone: true, selector: "praxis-tabs", inputs: { config: "config", tabsId: "tabsId", componentInstanceId: "componentInstanceId", selectedIndex: "selectedIndex", enableCustomization: "enableCustomization", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", widgetEvent: "widgetEvent" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], usesOnChanges: true, ngImport: i0, template: `
2956
3775
  <div
2957
3776
  class="praxis-tabs-root"
2958
3777
  [class.density-compact]="config?.appearance?.density === 'compact'"
@@ -2969,9 +3788,51 @@ class PraxisTabs {
2969
3788
  <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
2970
3789
 
2971
3790
  <div class="tabs-ai-assistant" *ngIf="enableCustomization">
2972
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
3791
+ <button
3792
+ mat-mini-fab
3793
+ type="button"
3794
+ color="primary"
3795
+ class="tabs-ai-assistant-trigger"
3796
+ (click)="openAiAssistant()"
3797
+ matTooltip="Copiloto semantico Praxis"
3798
+ aria-label="Abrir copiloto semantico Praxis das abas"
3799
+ data-testid="praxis-tabs-ai-assistant-trigger"
3800
+ >
3801
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
3802
+ </button>
2973
3803
  </div>
2974
3804
 
3805
+ <praxis-ai-assistant-shell
3806
+ *ngIf="aiAssistantOpen && aiAssistantViewState"
3807
+ [labels]="aiAssistantLabels"
3808
+ [mode]="aiAssistantViewState.mode"
3809
+ [state]="aiAssistantViewState.state"
3810
+ [contextItems]="aiAssistantViewState.contextItems"
3811
+ [attachments]="aiAssistantViewState.attachments"
3812
+ [messages]="aiAssistantViewState.messages"
3813
+ [quickReplies]="aiAssistantViewState.quickReplies"
3814
+ [prompt]="aiAssistantPrompt"
3815
+ [statusText]="aiAssistantViewState.statusText"
3816
+ [errorText]="aiAssistantViewState.errorText"
3817
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
3818
+ [canApply]="aiAssistantViewState.canApply"
3819
+ [layout]="aiAssistantLayout"
3820
+ testIdPrefix="praxis-tabs-ai-assistant"
3821
+ panelTestId="praxis-tabs-ai-assistant-panel"
3822
+ submitTestId="praxis-tabs-ai-assistant-submit"
3823
+ applyTestId="praxis-tabs-ai-assistant-apply"
3824
+ (promptChange)="onAiAssistantPromptChange($event)"
3825
+ (submitPrompt)="onAiAssistantSubmit($event)"
3826
+ (apply)="onAiAssistantApply()"
3827
+ (retryTurn)="onAiAssistantRetry()"
3828
+ (cancelTurn)="onAiAssistantCancel()"
3829
+ (quickReply)="onAiAssistantQuickReply($event)"
3830
+ (editMessage)="onAiAssistantEditMessage($event)"
3831
+ (resendMessage)="onAiAssistantResendMessage($event)"
3832
+ (layoutChange)="onAiAssistantLayoutChange($event)"
3833
+ (close)="closeAiAssistant()"
3834
+ ></praxis-ai-assistant-shell>
3835
+
2975
3836
  <!-- Empty state (global) -->
2976
3837
  <ng-container *ngIf="isEmptyGlobal(); else notEmpty">
2977
3838
  <praxis-empty-state-card
@@ -2995,13 +3856,13 @@ class PraxisTabs {
2995
3856
  cdkDropList
2996
3857
  cdkDropListOrientation="horizontal"
2997
3858
  [cdkDropListDisabled]="!config?.behavior?.reorderable"
2998
- (cdkDropListDropped)="onNavDrop($event)"
3859
+ (cdkDropListDropped)="onVisibleNavDrop($event)"
2999
3860
  [disablePagination]="config?.nav?.disablePagination"
3000
3861
  [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
3001
3862
  [mat-stretch-tabs]="config?.nav?.stretchTabs"
3002
3863
  [color]="config?.nav?.color"
3003
3864
  [backgroundColor]="config?.nav?.backgroundColor"
3004
- [selectedIndex]="currentNavIndex()"
3865
+ [selectedIndex]="selectedVisibleNavIndex()"
3005
3866
  [attr.aria-label]="config?.nav?.ariaLabel || config?.group?.ariaLabel || null"
3006
3867
  [attr.aria-labelledby]="config?.nav?.ariaLabelledby || config?.group?.ariaLabelledby || null"
3007
3868
  [animationDuration]="effectiveAnimationDuration()"
@@ -3010,21 +3871,22 @@ class PraxisTabs {
3010
3871
  >
3011
3872
  <a
3012
3873
  mat-tab-link
3013
- *ngFor="let link of config?.nav?.links; let i = index; trackBy: trackNavLink"
3874
+ *ngFor="let entry of visibleNavLinkEntries(); let i = index; trackBy: trackVisibleNavLink"
3014
3875
  cdkDrag
3015
3876
  [cdkDragDisabled]="!config?.behavior?.reorderable"
3016
3877
  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)"
3878
+ [active]="getNavActive(entry.index)"
3879
+ [disabled]="entry.link.disabled"
3880
+ [disableRipple]="config?.nav?.disableRipple || entry.link.disableRipple"
3881
+ [fitInkBarToContent]="entry.link.fitInkBarToContent || false"
3882
+ [id]="entry.link.id || ''"
3883
+ (click)="onNavClick(entry.index)"
3023
3884
  >
3024
3885
  <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
3025
3886
  <mat-icon fontIcon="drag_indicator"></mat-icon>
3026
3887
  </span>
3027
- {{ link.label }}
3888
+ <mat-icon *ngIf="entry.link.icon" class="tab-label-icon" [praxisIcon]="entry.link.icon"></mat-icon>
3889
+ {{ entry.link.label }}
3028
3890
  </a>
3029
3891
  </nav>
3030
3892
 
@@ -3074,7 +3936,7 @@ class PraxisTabs {
3074
3936
  [fitInkBarToContent]="config?.group?.fitInkBarToContent"
3075
3937
  [headerPosition]="config?.group?.headerPosition ?? 'above'"
3076
3938
  [preserveContent]="config?.group?.preserveContent"
3077
- [selectedIndex]="selectedIndexSignal()"
3939
+ [selectedIndex]="selectedVisibleTabIndex()"
3078
3940
  [mat-stretch-tabs]="config?.group?.stretchTabs"
3079
3941
  [mat-align-tabs]="config?.group?.alignTabs || 'start'"
3080
3942
  [color]="config?.group?.color"
@@ -3084,26 +3946,27 @@ class PraxisTabs {
3084
3946
  [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
3085
3947
  (animationDone)="animationDone.emit()"
3086
3948
  (focusChange)="focusChange.emit($event)"
3087
- (selectedIndexChange)="onSelectedIndexChange($event)"
3949
+ (selectedIndexChange)="onVisibleTabIndexChange($event)"
3088
3950
  (selectedTabChange)="selectedTabChange.emit($event)"
3089
3951
  class="praxis-tabs-group"
3090
3952
  >
3091
3953
  <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"
3954
+ *ngFor="let entry of visibleTabEntries(); let i = index; trackBy: trackVisibleTab"
3955
+ [disabled]="entry.tab.disabled"
3956
+ [labelClass]="entry.tab.labelClass ?? ''"
3957
+ [bodyClass]="entry.tab.bodyClass ?? ''"
3958
+ [id]="entry.tab.id || ''"
3959
+ [attr.aria-label]="entry.tab.ariaLabel || null"
3960
+ [attr.aria-labelledby]="entry.tab.ariaLabelledby || null"
3099
3961
  >
3100
3962
  <ng-template mat-tab-label>
3101
- <span>{{ tab.textLabel }}</span>
3963
+ <mat-icon *ngIf="entry.tab.icon" class="tab-label-icon" [praxisIcon]="entry.tab.icon"></mat-icon>
3964
+ <span>{{ entry.tab.textLabel }}</span>
3102
3965
  <button
3103
3966
  *ngIf="config?.behavior?.closeable"
3104
3967
  mat-icon-button
3105
3968
  type="button"
3106
- (click)="closeTab(i); $event.stopPropagation()"
3969
+ (click)="closeTab(entry.index); $event.stopPropagation()"
3107
3970
  (keydown.enter)="$event.stopPropagation()"
3108
3971
  (keydown.space)="$event.stopPropagation()"
3109
3972
  [attr.aria-label]="t('chrome.closeTab', 'Fechar aba')"
@@ -3114,10 +3977,10 @@ class PraxisTabs {
3114
3977
  <button
3115
3978
  mat-icon-button
3116
3979
  type="button"
3117
- (click)="moveTab(i, -1); $event.stopPropagation()"
3980
+ (click)="moveTab(entry.index, -1); $event.stopPropagation()"
3118
3981
  (keydown.enter)="$event.stopPropagation()"
3119
3982
  (keydown.space)="$event.stopPropagation()"
3120
- [disabled]="i===0"
3983
+ [disabled]="entry.index===0"
3121
3984
  [attr.aria-label]="t('chrome.moveTabLeft', 'Mover aba para esquerda')"
3122
3985
  >
3123
3986
  <mat-icon fontIcon="arrow_back"></mat-icon>
@@ -3125,10 +3988,10 @@ class PraxisTabs {
3125
3988
  <button
3126
3989
  mat-icon-button
3127
3990
  type="button"
3128
- (click)="moveTab(i, 1); $event.stopPropagation()"
3991
+ (click)="moveTab(entry.index, 1); $event.stopPropagation()"
3129
3992
  (keydown.enter)="$event.stopPropagation()"
3130
3993
  (keydown.space)="$event.stopPropagation()"
3131
- [disabled]="i===(config?.tabs?.length||1)-1"
3994
+ [disabled]="entry.index===(config?.tabs?.length||1)-1"
3132
3995
  [attr.aria-label]="t('chrome.moveTabRight', 'Mover aba para direita')"
3133
3996
  >
3134
3997
  <mat-icon fontIcon="arrow_forward"></mat-icon>
@@ -3137,20 +4000,20 @@ class PraxisTabs {
3137
4000
  </ng-template>
3138
4001
 
3139
4002
  <ng-template matTabContent>
3140
- <ng-container *ngIf="(tab.content?.length || tab.widgets?.length) && groupContentReady(i); else emptyTab">
3141
- <ng-container *ngIf="tab.content && form">
4003
+ <ng-container *ngIf="(entry.tab.content?.length || entry.tab.widgets?.length) && groupContentReady(entry.index); else emptyTab">
4004
+ <ng-container *ngIf="entry.tab.content && form">
3142
4005
  <ng-container
3143
4006
  dynamicFieldLoader
3144
- [fields]="tab.content || []"
4007
+ [fields]="entry.tab.content || []"
3145
4008
  [formGroup]="form!"
3146
4009
  ></ng-container>
3147
4010
  </ng-container>
3148
- <ng-container *ngIf="tab.widgets?.length">
3149
- <ng-container *ngFor="let w of tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
4011
+ <ng-container *ngIf="entry.tab.widgets?.length">
4012
+ <ng-container *ngFor="let w of entry.tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3150
4013
  <ng-container
3151
4014
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3152
4015
  [context]="context || {}"
3153
- (widgetEvent)="emitWidgetEvent(tabEventPath(tab.id, i), $event)"
4016
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
3154
4017
  ></ng-container>
3155
4018
  </ng-container>
3156
4019
  </ng-container>
@@ -3191,7 +4054,7 @@ class PraxisTabs {
3191
4054
  <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
3192
4055
  </button>
3193
4056
  </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 });
4057
+ `, 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.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: 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
4058
  }
3196
4059
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabs, decorators: [{
3197
4060
  type: Component,
@@ -3207,7 +4070,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3207
4070
  EmptyStateCardComponent,
3208
4071
  DynamicFieldLoaderDirective,
3209
4072
  DynamicWidgetLoaderDirective,
3210
- PraxisAiAssistantComponent,
4073
+ PraxisAiAssistantShellComponent,
3211
4074
  ], template: `
3212
4075
  <div
3213
4076
  class="praxis-tabs-root"
@@ -3225,9 +4088,51 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3225
4088
  <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
3226
4089
 
3227
4090
  <div class="tabs-ai-assistant" *ngIf="enableCustomization">
3228
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
4091
+ <button
4092
+ mat-mini-fab
4093
+ type="button"
4094
+ color="primary"
4095
+ class="tabs-ai-assistant-trigger"
4096
+ (click)="openAiAssistant()"
4097
+ matTooltip="Copiloto semantico Praxis"
4098
+ aria-label="Abrir copiloto semantico Praxis das abas"
4099
+ data-testid="praxis-tabs-ai-assistant-trigger"
4100
+ >
4101
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
4102
+ </button>
3229
4103
  </div>
3230
4104
 
4105
+ <praxis-ai-assistant-shell
4106
+ *ngIf="aiAssistantOpen && aiAssistantViewState"
4107
+ [labels]="aiAssistantLabels"
4108
+ [mode]="aiAssistantViewState.mode"
4109
+ [state]="aiAssistantViewState.state"
4110
+ [contextItems]="aiAssistantViewState.contextItems"
4111
+ [attachments]="aiAssistantViewState.attachments"
4112
+ [messages]="aiAssistantViewState.messages"
4113
+ [quickReplies]="aiAssistantViewState.quickReplies"
4114
+ [prompt]="aiAssistantPrompt"
4115
+ [statusText]="aiAssistantViewState.statusText"
4116
+ [errorText]="aiAssistantViewState.errorText"
4117
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
4118
+ [canApply]="aiAssistantViewState.canApply"
4119
+ [layout]="aiAssistantLayout"
4120
+ testIdPrefix="praxis-tabs-ai-assistant"
4121
+ panelTestId="praxis-tabs-ai-assistant-panel"
4122
+ submitTestId="praxis-tabs-ai-assistant-submit"
4123
+ applyTestId="praxis-tabs-ai-assistant-apply"
4124
+ (promptChange)="onAiAssistantPromptChange($event)"
4125
+ (submitPrompt)="onAiAssistantSubmit($event)"
4126
+ (apply)="onAiAssistantApply()"
4127
+ (retryTurn)="onAiAssistantRetry()"
4128
+ (cancelTurn)="onAiAssistantCancel()"
4129
+ (quickReply)="onAiAssistantQuickReply($event)"
4130
+ (editMessage)="onAiAssistantEditMessage($event)"
4131
+ (resendMessage)="onAiAssistantResendMessage($event)"
4132
+ (layoutChange)="onAiAssistantLayoutChange($event)"
4133
+ (close)="closeAiAssistant()"
4134
+ ></praxis-ai-assistant-shell>
4135
+
3231
4136
  <!-- Empty state (global) -->
3232
4137
  <ng-container *ngIf="isEmptyGlobal(); else notEmpty">
3233
4138
  <praxis-empty-state-card
@@ -3251,13 +4156,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3251
4156
  cdkDropList
3252
4157
  cdkDropListOrientation="horizontal"
3253
4158
  [cdkDropListDisabled]="!config?.behavior?.reorderable"
3254
- (cdkDropListDropped)="onNavDrop($event)"
4159
+ (cdkDropListDropped)="onVisibleNavDrop($event)"
3255
4160
  [disablePagination]="config?.nav?.disablePagination"
3256
4161
  [fitInkBarToContent]="config?.nav?.fitInkBarToContent"
3257
4162
  [mat-stretch-tabs]="config?.nav?.stretchTabs"
3258
4163
  [color]="config?.nav?.color"
3259
4164
  [backgroundColor]="config?.nav?.backgroundColor"
3260
- [selectedIndex]="currentNavIndex()"
4165
+ [selectedIndex]="selectedVisibleNavIndex()"
3261
4166
  [attr.aria-label]="config?.nav?.ariaLabel || config?.group?.ariaLabel || null"
3262
4167
  [attr.aria-labelledby]="config?.nav?.ariaLabelledby || config?.group?.ariaLabelledby || null"
3263
4168
  [animationDuration]="effectiveAnimationDuration()"
@@ -3266,21 +4171,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3266
4171
  >
3267
4172
  <a
3268
4173
  mat-tab-link
3269
- *ngFor="let link of config?.nav?.links; let i = index; trackBy: trackNavLink"
4174
+ *ngFor="let entry of visibleNavLinkEntries(); let i = index; trackBy: trackVisibleNavLink"
3270
4175
  cdkDrag
3271
4176
  [cdkDragDisabled]="!config?.behavior?.reorderable"
3272
4177
  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)"
4178
+ [active]="getNavActive(entry.index)"
4179
+ [disabled]="entry.link.disabled"
4180
+ [disableRipple]="config?.nav?.disableRipple || entry.link.disableRipple"
4181
+ [fitInkBarToContent]="entry.link.fitInkBarToContent || false"
4182
+ [id]="entry.link.id || ''"
4183
+ (click)="onNavClick(entry.index)"
3279
4184
  >
3280
4185
  <span *ngIf="config?.behavior?.reorderable" class="drag-handle" cdkDragHandle>
3281
4186
  <mat-icon fontIcon="drag_indicator"></mat-icon>
3282
4187
  </span>
3283
- {{ link.label }}
4188
+ <mat-icon *ngIf="entry.link.icon" class="tab-label-icon" [praxisIcon]="entry.link.icon"></mat-icon>
4189
+ {{ entry.link.label }}
3284
4190
  </a>
3285
4191
  </nav>
3286
4192
 
@@ -3330,7 +4236,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3330
4236
  [fitInkBarToContent]="config?.group?.fitInkBarToContent"
3331
4237
  [headerPosition]="config?.group?.headerPosition ?? 'above'"
3332
4238
  [preserveContent]="config?.group?.preserveContent"
3333
- [selectedIndex]="selectedIndexSignal()"
4239
+ [selectedIndex]="selectedVisibleTabIndex()"
3334
4240
  [mat-stretch-tabs]="config?.group?.stretchTabs"
3335
4241
  [mat-align-tabs]="config?.group?.alignTabs || 'start'"
3336
4242
  [color]="config?.group?.color"
@@ -3340,26 +4246,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3340
4246
  [attr.aria-labelledby]="config?.group?.ariaLabelledby || null"
3341
4247
  (animationDone)="animationDone.emit()"
3342
4248
  (focusChange)="focusChange.emit($event)"
3343
- (selectedIndexChange)="onSelectedIndexChange($event)"
4249
+ (selectedIndexChange)="onVisibleTabIndexChange($event)"
3344
4250
  (selectedTabChange)="selectedTabChange.emit($event)"
3345
4251
  class="praxis-tabs-group"
3346
4252
  >
3347
4253
  <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"
4254
+ *ngFor="let entry of visibleTabEntries(); let i = index; trackBy: trackVisibleTab"
4255
+ [disabled]="entry.tab.disabled"
4256
+ [labelClass]="entry.tab.labelClass ?? ''"
4257
+ [bodyClass]="entry.tab.bodyClass ?? ''"
4258
+ [id]="entry.tab.id || ''"
4259
+ [attr.aria-label]="entry.tab.ariaLabel || null"
4260
+ [attr.aria-labelledby]="entry.tab.ariaLabelledby || null"
3355
4261
  >
3356
4262
  <ng-template mat-tab-label>
3357
- <span>{{ tab.textLabel }}</span>
4263
+ <mat-icon *ngIf="entry.tab.icon" class="tab-label-icon" [praxisIcon]="entry.tab.icon"></mat-icon>
4264
+ <span>{{ entry.tab.textLabel }}</span>
3358
4265
  <button
3359
4266
  *ngIf="config?.behavior?.closeable"
3360
4267
  mat-icon-button
3361
4268
  type="button"
3362
- (click)="closeTab(i); $event.stopPropagation()"
4269
+ (click)="closeTab(entry.index); $event.stopPropagation()"
3363
4270
  (keydown.enter)="$event.stopPropagation()"
3364
4271
  (keydown.space)="$event.stopPropagation()"
3365
4272
  [attr.aria-label]="t('chrome.closeTab', 'Fechar aba')"
@@ -3370,10 +4277,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3370
4277
  <button
3371
4278
  mat-icon-button
3372
4279
  type="button"
3373
- (click)="moveTab(i, -1); $event.stopPropagation()"
4280
+ (click)="moveTab(entry.index, -1); $event.stopPropagation()"
3374
4281
  (keydown.enter)="$event.stopPropagation()"
3375
4282
  (keydown.space)="$event.stopPropagation()"
3376
- [disabled]="i===0"
4283
+ [disabled]="entry.index===0"
3377
4284
  [attr.aria-label]="t('chrome.moveTabLeft', 'Mover aba para esquerda')"
3378
4285
  >
3379
4286
  <mat-icon fontIcon="arrow_back"></mat-icon>
@@ -3381,10 +4288,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3381
4288
  <button
3382
4289
  mat-icon-button
3383
4290
  type="button"
3384
- (click)="moveTab(i, 1); $event.stopPropagation()"
4291
+ (click)="moveTab(entry.index, 1); $event.stopPropagation()"
3385
4292
  (keydown.enter)="$event.stopPropagation()"
3386
4293
  (keydown.space)="$event.stopPropagation()"
3387
- [disabled]="i===(config?.tabs?.length||1)-1"
4294
+ [disabled]="entry.index===(config?.tabs?.length||1)-1"
3388
4295
  [attr.aria-label]="t('chrome.moveTabRight', 'Mover aba para direita')"
3389
4296
  >
3390
4297
  <mat-icon fontIcon="arrow_forward"></mat-icon>
@@ -3393,20 +4300,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3393
4300
  </ng-template>
3394
4301
 
3395
4302
  <ng-template matTabContent>
3396
- <ng-container *ngIf="(tab.content?.length || tab.widgets?.length) && groupContentReady(i); else emptyTab">
3397
- <ng-container *ngIf="tab.content && form">
4303
+ <ng-container *ngIf="(entry.tab.content?.length || entry.tab.widgets?.length) && groupContentReady(entry.index); else emptyTab">
4304
+ <ng-container *ngIf="entry.tab.content && form">
3398
4305
  <ng-container
3399
4306
  dynamicFieldLoader
3400
- [fields]="tab.content || []"
4307
+ [fields]="entry.tab.content || []"
3401
4308
  [formGroup]="form!"
3402
4309
  ></ng-container>
3403
4310
  </ng-container>
3404
- <ng-container *ngIf="tab.widgets?.length">
3405
- <ng-container *ngFor="let w of tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
4311
+ <ng-container *ngIf="entry.tab.widgets?.length">
4312
+ <ng-container *ngFor="let w of entry.tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
3406
4313
  <ng-container
3407
4314
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3408
4315
  [context]="context || {}"
3409
- (widgetEvent)="emitWidgetEvent(tabEventPath(tab.id, i), $event)"
4316
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
3410
4317
  ></ng-container>
3411
4318
  </ng-container>
3412
4319
  </ng-container>
@@ -3447,7 +4354,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3447
4354
  <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
3448
4355
  </button>
3449
4356
  </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"] }]
4357
+ `, 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
4358
  }], propDecorators: { config: [{
3452
4359
  type: Input
3453
4360
  }], tabsId: [{
@@ -3455,6 +4362,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3455
4362
  args: [{ required: true }]
3456
4363
  }], componentInstanceId: [{
3457
4364
  type: Input
4365
+ }], selectedIndex: [{
4366
+ type: Input
3458
4367
  }], enableCustomization: [{
3459
4368
  type: Input
3460
4369
  }], form: [{
@@ -3477,6 +4386,108 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3477
4386
  type: Output
3478
4387
  }] } });
3479
4388
 
4389
+ class PraxisTabsWidgetConfigEditor {
4390
+ set inputs(value) {
4391
+ this._inputs = value;
4392
+ this.editorDocument = this.createDocument();
4393
+ }
4394
+ get inputs() {
4395
+ return this._inputs;
4396
+ }
4397
+ set widgetKey(value) {
4398
+ this._widgetKey = value;
4399
+ this.editorDocument = this.createDocument();
4400
+ }
4401
+ get widgetKey() {
4402
+ return this._widgetKey;
4403
+ }
4404
+ tabsEditor;
4405
+ isDirty$ = new BehaviorSubject(false);
4406
+ isValid$ = new BehaviorSubject(true);
4407
+ isBusy$ = new BehaviorSubject(false);
4408
+ subscription = new Subscription();
4409
+ emptyConfig = {};
4410
+ _inputs = null;
4411
+ _widgetKey;
4412
+ editorDocument = this.createDocument();
4413
+ get config() {
4414
+ return this.inputs?.config ?? this.emptyConfig;
4415
+ }
4416
+ get tabsId() {
4417
+ return this.inputs?.tabsId ?? this.widgetKey;
4418
+ }
4419
+ get componentInstanceId() {
4420
+ return this.inputs?.componentInstanceId ?? undefined;
4421
+ }
4422
+ ngAfterViewInit() {
4423
+ if (!this.tabsEditor) {
4424
+ return;
4425
+ }
4426
+ this.subscription.add(this.tabsEditor.isDirty$.subscribe((value) => this.isDirty$.next(value)));
4427
+ this.subscription.add(this.tabsEditor.isValid$.subscribe((value) => this.isValid$.next(value)));
4428
+ this.subscription.add(this.tabsEditor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
4429
+ }
4430
+ ngOnDestroy() {
4431
+ this.subscription.unsubscribe();
4432
+ }
4433
+ getSettingsValue() {
4434
+ return this.buildValue(this.tabsEditor?.getSettingsValue());
4435
+ }
4436
+ onSave() {
4437
+ return this.buildValue(this.tabsEditor?.onSave?.() ?? this.tabsEditor?.getSettingsValue());
4438
+ }
4439
+ reset() {
4440
+ this.tabsEditor?.reset();
4441
+ }
4442
+ buildValue(document) {
4443
+ const bindings = document?.bindings ?? {};
4444
+ return {
4445
+ inputs: {
4446
+ ...(this.inputs ?? {}),
4447
+ config: document?.config ?? this.config,
4448
+ ...(bindings.tabsId ? { tabsId: bindings.tabsId } : this.tabsId ? { tabsId: this.tabsId } : {}),
4449
+ ...(bindings.componentInstanceId ? { componentInstanceId: bindings.componentInstanceId } : {}),
4450
+ },
4451
+ };
4452
+ }
4453
+ createDocument() {
4454
+ return createTabsAuthoringDocument({
4455
+ config: this.config,
4456
+ bindings: {
4457
+ tabsId: this.tabsId,
4458
+ componentInstanceId: this.componentInstanceId,
4459
+ },
4460
+ });
4461
+ }
4462
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabsWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
4463
+ 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: `
4464
+ <section data-testid="tabs-widget-config-editor">
4465
+ <praxis-tabs-config-editor #tabsEditor [document]="editorDocument" />
4466
+ </section>
4467
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisTabsConfigEditor, selector: "praxis-tabs-config-editor", inputs: ["document"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4468
+ }
4469
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabsWidgetConfigEditor, decorators: [{
4470
+ type: Component,
4471
+ args: [{
4472
+ selector: 'praxis-tabs-widget-config-editor',
4473
+ standalone: true,
4474
+ imports: [CommonModule, PraxisTabsConfigEditor],
4475
+ template: `
4476
+ <section data-testid="tabs-widget-config-editor">
4477
+ <praxis-tabs-config-editor #tabsEditor [document]="editorDocument" />
4478
+ </section>
4479
+ `,
4480
+ changeDetection: ChangeDetectionStrategy.OnPush,
4481
+ }]
4482
+ }], propDecorators: { inputs: [{
4483
+ type: Input
4484
+ }], widgetKey: [{
4485
+ type: Input
4486
+ }], tabsEditor: [{
4487
+ type: ViewChild,
4488
+ args: ['tabsEditor']
4489
+ }] } });
4490
+
3480
4491
  const PRAXIS_TABS_PORTS = [
3481
4492
  {
3482
4493
  id: 'context',
@@ -3504,6 +4515,19 @@ const PRAXIS_TABS_PORTS = [
3504
4515
  description: 'Fragmento canonico de configuracao das tabs/nav e dos widgets internos.',
3505
4516
  exposure: { public: true, group: 'config' },
3506
4517
  },
4518
+ {
4519
+ id: 'selectedIndex',
4520
+ label: 'Indice selecionado',
4521
+ direction: 'input',
4522
+ semanticKind: 'value',
4523
+ schema: {
4524
+ id: 'number',
4525
+ kind: 'ts-type',
4526
+ ref: 'number',
4527
+ },
4528
+ description: 'Indice ativo de abas/nav para controle externo por composicao.',
4529
+ exposure: { public: true, group: 'state' },
4530
+ },
3507
4531
  {
3508
4532
  id: 'selectedIndexChange',
3509
4533
  label: 'Troca de indice selecionado',
@@ -3540,6 +4564,14 @@ const PRAXIS_TABS_COMPONENT_METADATA = {
3540
4564
  friendlyName: 'Praxis Tabs',
3541
4565
  description: 'Abas dinâmicas baseadas em metadata, com MatTabGroup/TabNav e suporte a tokens M3 via appearance.',
3542
4566
  icon: 'tab',
4567
+ authoringManifestRef: {
4568
+ componentId: 'praxis-tabs',
4569
+ source: 'PRAXIS_TABS_AUTHORING_MANIFEST',
4570
+ },
4571
+ configEditor: {
4572
+ component: PraxisTabsWidgetConfigEditor,
4573
+ title: 'Configure tabs',
4574
+ },
3543
4575
  inputs: [
3544
4576
  { name: 'config', type: 'TabsMetadata', label: 'Configuração', description: 'Configuração JSON (tabs/nav, aparência e widgets internos)' },
3545
4577
  {
@@ -3554,6 +4586,12 @@ const PRAXIS_TABS_COMPONENT_METADATA = {
3554
4586
  label: 'ID da instância',
3555
4587
  description: 'Identificador opcional para múltiplas instâncias na mesma rota',
3556
4588
  },
4589
+ {
4590
+ name: 'selectedIndex',
4591
+ type: 'number',
4592
+ label: 'Indice selecionado',
4593
+ description: 'Indice ativo de abas/nav controlavel externamente pela composicao',
4594
+ },
3557
4595
  {
3558
4596
  name: 'enableCustomization',
3559
4597
  type: 'boolean',
@@ -3675,6 +4713,341 @@ function providePraxisTabsMetadata() {
3675
4713
  };
3676
4714
  }
3677
4715
 
4716
+ const tabItemSchema = {
4717
+ type: 'object',
4718
+ required: ['id', 'textLabel'],
4719
+ properties: {
4720
+ id: { type: 'string' },
4721
+ textLabel: { type: 'string' },
4722
+ icon: { type: 'string' },
4723
+ disabled: { type: 'boolean' },
4724
+ visible: { type: 'boolean', default: true },
4725
+ content: { type: 'array', items: { type: 'object' } },
4726
+ widgets: { type: 'array', items: { type: 'object' } },
4727
+ },
4728
+ };
4729
+ const tabPatchSchema = {
4730
+ type: 'object',
4731
+ minProperties: 1,
4732
+ properties: {
4733
+ id: { type: 'string' },
4734
+ textLabel: { type: 'string' },
4735
+ icon: { type: 'string' },
4736
+ disabled: { type: 'boolean' },
4737
+ visible: { type: 'boolean' },
4738
+ content: { type: 'array', items: { type: 'object' } },
4739
+ widgets: { type: 'array', items: { type: 'object' } },
4740
+ },
4741
+ };
4742
+ const PRAXIS_TABS_AUTHORING_MANIFEST = {
4743
+ schemaVersion: '1.0.0',
4744
+ componentId: 'praxis-tabs',
4745
+ ownerPackage: '@praxisui/tabs',
4746
+ configSchemaId: 'TabsMetadata',
4747
+ manifestVersion: '1.0.0',
4748
+ runtimeInputs: [
4749
+ { name: 'config', type: 'TabsMetadata', description: 'Canonical tabs/nav configuration.' },
4750
+ { name: 'tabsId', type: 'string', description: 'Stable id used to derive persistence scope.' },
4751
+ { name: 'componentInstanceId', type: 'string', description: 'Optional instance discriminator for persistence scope.' },
4752
+ { name: 'form', type: 'FormGroup', description: 'FormGroup consumed by dynamic field content.' },
4753
+ { name: 'context', type: 'Record<string, any>', description: 'Context passed to nested widgets.' },
4754
+ { name: 'enableCustomization', type: 'boolean', description: 'Enables Settings Panel authoring surfaces.' },
4755
+ ],
4756
+ editableTargets: [
4757
+ { kind: 'tab', resolver: 'tab-by-id-or-label', description: 'A group-mode tab in config.tabs[].' },
4758
+ { kind: 'tabLabel', resolver: 'tab-by-id-or-label', description: 'The text label of a group-mode tab.' },
4759
+ { kind: 'tabIcon', resolver: 'tab-by-id-or-label', description: 'Icon metadata rendered in a group tab label.' },
4760
+ { kind: 'tabContent', resolver: 'tab-or-link-by-id', description: 'Dynamic fields or widgets hosted by a tab or nav link.' },
4761
+ { kind: 'activeTab', resolver: 'tab-index-or-id', description: 'Selected tab or nav link index.' },
4762
+ { kind: 'visibility', resolver: 'tab-or-link-by-id', description: 'Runtime visibility flag for a group tab or nav link.' },
4763
+ { kind: 'disabledState', resolver: 'tab-or-link-by-id', description: 'Disabled state of a tab or nav link.' },
4764
+ { kind: 'layout', resolver: 'tabs-layout-config', description: 'Group/nav mode, header position, density, stretch and behavior settings.' },
4765
+ ],
4766
+ operations: [
4767
+ {
4768
+ operationId: 'tab.add',
4769
+ title: 'Add tab',
4770
+ scope: 'global',
4771
+ targetKind: 'tab',
4772
+ target: { kind: 'tab', resolver: 'tabs-array', ambiguityPolicy: 'fail', required: false },
4773
+ inputSchema: tabItemSchema,
4774
+ effects: [{ kind: 'append-unique', path: 'tabs[]', key: 'id' }],
4775
+ validators: ['tab-id-unique', 'tabs-mode-compatible', 'tab-content-valid'],
4776
+ destructive: false,
4777
+ requiresConfirmation: false,
4778
+ affectedPaths: ['tabs[]'],
4779
+ submissionImpact: 'config-only',
4780
+ preconditions: ['config-initialized'],
4781
+ },
4782
+ {
4783
+ operationId: 'tab.remove',
4784
+ title: 'Remove tab',
4785
+ scope: 'layout',
4786
+ targetKind: 'tab',
4787
+ target: { kind: 'tab', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
4788
+ inputSchema: {
4789
+ type: 'object',
4790
+ properties: {
4791
+ replacementActiveTabId: { type: 'string' },
4792
+ },
4793
+ },
4794
+ effects: [
4795
+ {
4796
+ kind: 'compile-domain-patch',
4797
+ handler: 'tabs.remove-tab-and-reselect',
4798
+ handlerContract: {
4799
+ reads: ['tabs[]', 'group.selectedIndex'],
4800
+ writes: ['tabs[]', 'group.selectedIndex'],
4801
+ identityKeys: ['tabs[].id'],
4802
+ inputSchema: {
4803
+ type: 'object',
4804
+ properties: { replacementActiveTabId: { type: 'string' } },
4805
+ },
4806
+ failureModes: ['target-tab-missing', 'replacement-tab-missing', 'confirmation-missing'],
4807
+ description: 'Removes the target tab by stable id and reselects a safe replacement when the active/default tab is removed.',
4808
+ },
4809
+ },
4810
+ ],
4811
+ destructive: true,
4812
+ requiresConfirmation: true,
4813
+ validators: ['tab-exists', 'active-tab-removal-safe', 'tab-content-removal-confirmed'],
4814
+ affectedPaths: ['tabs[]', 'group.selectedIndex'],
4815
+ submissionImpact: 'config-only',
4816
+ preconditions: ['config-initialized', 'target-tab-exists', 'confirmation-collected'],
4817
+ },
4818
+ {
4819
+ operationId: 'tab.label.set',
4820
+ title: 'Set tab label',
4821
+ scope: 'layout',
4822
+ targetKind: 'tabLabel',
4823
+ target: { kind: 'tabLabel', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
4824
+ inputSchema: { type: 'object', required: ['textLabel'], properties: { textLabel: { type: 'string' } } },
4825
+ effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
4826
+ destructive: false,
4827
+ requiresConfirmation: false,
4828
+ validators: ['tab-exists', 'tab-label-valid'],
4829
+ affectedPaths: ['tabs[].textLabel'],
4830
+ submissionImpact: 'config-only',
4831
+ preconditions: ['config-initialized', 'target-tab-exists'],
4832
+ },
4833
+ {
4834
+ operationId: 'tab.icon.set',
4835
+ title: 'Set tab icon',
4836
+ scope: 'layout',
4837
+ targetKind: 'tabIcon',
4838
+ target: { kind: 'tabIcon', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
4839
+ inputSchema: { type: 'object', required: ['icon'], properties: { icon: { type: 'string' } } },
4840
+ effects: [{ kind: 'merge-by-key', path: 'tabs[]', key: 'id' }],
4841
+ destructive: false,
4842
+ requiresConfirmation: false,
4843
+ validators: ['tab-exists', 'tab-icon-valid'],
4844
+ affectedPaths: ['tabs[].icon'],
4845
+ submissionImpact: 'visual-only',
4846
+ preconditions: ['config-initialized', 'target-tab-exists'],
4847
+ },
4848
+ {
4849
+ operationId: 'tab.order.set',
4850
+ title: 'Reorder tabs',
4851
+ scope: 'layout',
4852
+ targetKind: 'tab',
4853
+ target: { kind: 'tab', resolver: 'tab-by-id-or-label', ambiguityPolicy: 'fail', required: true },
4854
+ inputSchema: { type: 'object', required: ['beforeTabId'], properties: { beforeTabId: { type: 'string' } } },
4855
+ effects: [
4856
+ {
4857
+ kind: 'compile-domain-patch',
4858
+ handler: 'tabs.reorder-tab-and-preserve-selection',
4859
+ handlerContract: {
4860
+ reads: ['tabs[]', 'group.selectedIndex'],
4861
+ writes: ['tabs[]', 'group.selectedIndex'],
4862
+ identityKeys: ['tabs[].id'],
4863
+ inputSchema: { type: 'object', required: ['beforeTabId'], properties: { beforeTabId: { type: 'string' } } },
4864
+ failureModes: ['target-tab-missing', 'before-tab-missing', 'unstable-tab-id'],
4865
+ description: 'Reorders tabs by stable id and remaps group.selectedIndex when the selected tab crosses positions.',
4866
+ },
4867
+ },
4868
+ ],
4869
+ destructive: false,
4870
+ requiresConfirmation: false,
4871
+ validators: ['tab-exists', 'tab-order-deterministic'],
4872
+ affectedPaths: ['tabs[]', 'group.selectedIndex'],
4873
+ submissionImpact: 'config-only',
4874
+ preconditions: ['config-initialized', 'target-tab-exists'],
4875
+ },
4876
+ {
4877
+ operationId: 'tab.disabled.set',
4878
+ title: 'Set tab disabled state',
4879
+ scope: 'interaction',
4880
+ targetKind: 'disabledState',
4881
+ target: { kind: 'disabledState', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
4882
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
4883
+ effects: [
4884
+ {
4885
+ kind: 'compile-domain-patch',
4886
+ handler: 'tabs.set-tab-or-link-disabled',
4887
+ handlerContract: {
4888
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
4889
+ writes: ['tabs[].disabled', 'nav.links[].disabled'],
4890
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
4891
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
4892
+ failureModes: ['target-tab-or-link-missing', 'ambiguous-target', 'active-item-disabled-without-reselection'],
4893
+ description: 'Sets disabled on the resolved group tab or nav link without guessing between modes.',
4894
+ },
4895
+ },
4896
+ ],
4897
+ destructive: false,
4898
+ requiresConfirmation: false,
4899
+ validators: ['tab-or-link-exists', 'active-tab-disabled-safe'],
4900
+ affectedPaths: ['tabs[].disabled', 'nav.links[].disabled'],
4901
+ submissionImpact: 'config-only',
4902
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
4903
+ },
4904
+ {
4905
+ operationId: 'tab.visible.set',
4906
+ title: 'Set tab visibility',
4907
+ scope: 'interaction',
4908
+ targetKind: 'visibility',
4909
+ target: { kind: 'visibility', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
4910
+ inputSchema: { type: 'object', required: ['visible'], properties: { visible: { type: 'boolean' } } },
4911
+ effects: [
4912
+ {
4913
+ kind: 'compile-domain-patch',
4914
+ handler: 'tabs.set-tab-or-link-visible',
4915
+ handlerContract: {
4916
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
4917
+ writes: ['tabs[].visible', 'nav.links[].visible'],
4918
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
4919
+ inputSchema: { type: 'object', required: ['visible'], properties: { visible: { type: 'boolean' } } },
4920
+ failureModes: ['target-tab-or-link-missing', 'ambiguous-target', 'active-item-hidden-without-reselection'],
4921
+ description: 'Sets visible on the resolved group tab or nav link and preserves deterministic visible-index mapping.',
4922
+ },
4923
+ },
4924
+ ],
4925
+ destructive: false,
4926
+ requiresConfirmation: false,
4927
+ validators: ['tab-or-link-exists', 'active-tab-visibility-safe'],
4928
+ affectedPaths: ['tabs[].visible', 'nav.links[].visible'],
4929
+ submissionImpact: 'config-only',
4930
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
4931
+ },
4932
+ {
4933
+ operationId: 'tab.active.set',
4934
+ title: 'Set active tab',
4935
+ scope: 'interaction',
4936
+ targetKind: 'activeTab',
4937
+ target: { kind: 'activeTab', resolver: 'tab-index-or-id', ambiguityPolicy: 'fail', required: true },
4938
+ inputSchema: { type: 'object', required: ['selectedIndex'], properties: { selectedIndex: { type: 'number' }, tabId: { type: 'string' } } },
4939
+ effects: [
4940
+ {
4941
+ kind: 'compile-domain-patch',
4942
+ handler: 'tabs.set-active-item',
4943
+ handlerContract: {
4944
+ reads: ['tabs[]', 'nav.links[]', 'group.selectedIndex', 'nav.selectedIndex'],
4945
+ writes: ['group.selectedIndex', 'nav.selectedIndex'],
4946
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
4947
+ inputSchema: { type: 'object', required: ['selectedIndex'], properties: { selectedIndex: { type: 'number' }, tabId: { type: 'string' } } },
4948
+ failureModes: ['target-tab-or-link-missing', 'selected-index-out-of-range', 'hidden-or-disabled-target'],
4949
+ description: 'Sets the active index for the current primary mode using either selectedIndex or a resolved tab/link id.',
4950
+ },
4951
+ },
4952
+ ],
4953
+ destructive: false,
4954
+ requiresConfirmation: false,
4955
+ validators: ['active-tab-exists', 'selected-index-in-range'],
4956
+ affectedPaths: ['group.selectedIndex', 'nav.selectedIndex'],
4957
+ submissionImpact: 'config-only',
4958
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
4959
+ },
4960
+ {
4961
+ operationId: 'layout.variant.set',
4962
+ title: 'Set tabs layout variant',
4963
+ scope: 'layout',
4964
+ targetKind: 'layout',
4965
+ target: { kind: 'layout', resolver: 'tabs-layout-config', ambiguityPolicy: 'fail', required: true },
4966
+ inputSchema: {
4967
+ type: 'object',
4968
+ required: ['mode'],
4969
+ properties: {
4970
+ mode: { enum: ['group', 'nav'] },
4971
+ density: { enum: ['compact', 'comfortable', 'spacious'] },
4972
+ headerPosition: { enum: ['above', 'below'] },
4973
+ alignTabs: { enum: ['start', 'center', 'end'] },
4974
+ stretchTabs: { type: 'boolean' },
4975
+ lazyLoad: { type: 'boolean' },
4976
+ },
4977
+ },
4978
+ effects: [{ kind: 'merge-object', path: 'appearance' }, { kind: 'merge-object', path: 'group' }, { kind: 'merge-object', path: 'nav' }, { kind: 'merge-object', path: 'behavior' }],
4979
+ destructive: false,
4980
+ requiresConfirmation: false,
4981
+ validators: ['tabs-mode-compatible', 'layout-values-valid', 'editor-runtime-round-trip'],
4982
+ affectedPaths: ['appearance.density', 'group.headerPosition', 'group.alignTabs', 'group.stretchTabs', 'nav.stretchTabs', 'behavior.lazyLoad'],
4983
+ submissionImpact: 'config-only',
4984
+ preconditions: ['config-initialized'],
4985
+ },
4986
+ {
4987
+ operationId: 'tab.content.set',
4988
+ title: 'Set tab content',
4989
+ scope: 'layout',
4990
+ targetKind: 'tabContent',
4991
+ target: { kind: 'tabContent', resolver: 'tab-or-link-by-id', ambiguityPolicy: 'fail', required: true },
4992
+ inputSchema: tabPatchSchema,
4993
+ effects: [
4994
+ {
4995
+ kind: 'compile-domain-patch',
4996
+ handler: 'tabs.set-tab-or-link-content',
4997
+ handlerContract: {
4998
+ reads: ['tabs[]', 'nav.links[]'],
4999
+ writes: ['tabs[].content', 'tabs[].widgets', 'nav.links[].content', 'nav.links[].widgets'],
5000
+ identityKeys: ['tabs[].id', 'nav.links[].id'],
5001
+ inputSchema: tabPatchSchema,
5002
+ failureModes: ['target-tab-or-link-missing', 'invalid-dynamic-field-content', 'invalid-widget-definition'],
5003
+ description: 'Updates content/widgets only on the resolved group tab or nav link while preserving nested widget identity.',
5004
+ },
5005
+ },
5006
+ ],
5007
+ destructive: false,
5008
+ requiresConfirmation: false,
5009
+ validators: ['tab-or-link-exists', 'tab-content-valid', 'widget-event-delegated'],
5010
+ affectedPaths: ['tabs[].content', 'tabs[].widgets', 'nav.links[].content', 'nav.links[].widgets'],
5011
+ submissionImpact: 'config-only',
5012
+ preconditions: ['config-initialized', 'target-tab-or-link-exists'],
5013
+ },
5014
+ ],
5015
+ validators: [
5016
+ { validatorId: 'tab-id-unique', level: 'error', code: 'PTABS001', description: 'Tab ids and nav link ids must be unique within their mode.' },
5017
+ { validatorId: 'tab-exists', level: 'error', code: 'PTABS002', description: 'Target tab must exist before applying the operation.' },
5018
+ { validatorId: 'tab-or-link-exists', level: 'error', code: 'PTABS003', description: 'Target must resolve to an existing group tab or nav link.' },
5019
+ { validatorId: 'active-tab-exists', level: 'error', code: 'PTABS004', description: 'Active tab or nav link selection must reference an existing item.' },
5020
+ { validatorId: 'selected-index-in-range', level: 'error', code: 'PTABS005', description: 'Selected index must be clamped to the target mode item count.' },
5021
+ { validatorId: 'active-tab-removal-safe', level: 'error', code: 'PTABS006', description: 'Removing the active/default tab requires confirmation or a replacement active tab.' },
5022
+ { validatorId: 'tab-content-removal-confirmed', level: 'error', code: 'PTABS007', description: 'Removing a tab or link with content/widgets is destructive and requires confirmation.' },
5023
+ { validatorId: 'tab-label-valid', level: 'error', code: 'PTABS008', description: 'Tab labels must be non-empty text values after localization/domain projection.' },
5024
+ { validatorId: 'tab-icon-valid', level: 'warning', code: 'PTABS009', description: 'Tab icon metadata must remain compatible with the icon directive and editor round-trip.' },
5025
+ { validatorId: 'tab-order-deterministic', level: 'error', code: 'PTABS010', description: 'Tab ordering must use stable ids, not transient array index as identity.' },
5026
+ { validatorId: 'tabs-mode-compatible', level: 'error', code: 'PTABS011', description: 'Authoring must resolve to one primary mode: group tabs or nav links.' },
5027
+ { validatorId: 'layout-values-valid', level: 'error', code: 'PTABS012', description: 'Layout values must match TabsMetadata enums and runtime bindings.' },
5028
+ { 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.' },
5029
+ { validatorId: 'active-tab-disabled-safe', level: 'warning', code: 'PTABS014', description: 'Disabling the active item should move selection or request explicit confirmation.' },
5030
+ { validatorId: 'active-tab-visibility-safe', level: 'warning', code: 'PTABS015', description: 'Hiding the active item should move selection or request explicit confirmation.' },
5031
+ { validatorId: 'tab-content-valid', level: 'error', code: 'PTABS016', description: 'Tab content must be valid DynamicFieldMetadata[] or WidgetDefinition[] and preserve nested widget identity.' },
5032
+ { 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.' },
5033
+ ],
5034
+ roundTripRequirements: [
5035
+ 'Operations must preserve stable tab/link ids; array index may be used only as a resolver fallback, never as canonical identity.',
5036
+ 'Settings Panel, quick setup and JSON editor must round-trip through TabsAuthoringDocument without losing config or bindings.',
5037
+ 'Group and nav modes must remain mutually explicit; authoring cannot silently mix config.tabs and nav.links as competing primary modes.',
5038
+ 'Nested widget events remain delegated through widgetEvent path enrichment and component-port nestedPath semantics.',
5039
+ ],
5040
+ examples: [
5041
+ { id: 'add-overview-tab', request: 'Add an Overview tab before the details tab.', operationId: 'tab.add', params: { id: 'overview', textLabel: 'Overview' }, isPositive: true },
5042
+ { id: 'rename-tab', request: 'Rename the details tab to Account Details.', operationId: 'tab.label.set', target: 'details', params: { textLabel: 'Account Details' }, isPositive: true },
5043
+ { id: 'reorder-tabs', request: 'Move billing before overview.', operationId: 'tab.order.set', target: 'billing', params: { beforeTabId: 'overview' }, isPositive: true },
5044
+ { id: 'disable-tab', request: 'Disable the audit tab until the user has permission.', operationId: 'tab.disabled.set', target: 'audit', params: { disabled: true }, isPositive: true },
5045
+ { id: 'activate-tab', request: 'Open the documents tab by default.', operationId: 'tab.active.set', target: 'documents', params: { tabId: 'documents', selectedIndex: 2 }, isPositive: true },
5046
+ { id: 'reject-duplicate-tab-id', request: 'Add another tab with id overview.', operationId: 'tab.add', params: { id: 'overview', textLabel: 'Duplicate Overview' }, isPositive: false },
5047
+ { id: 'confirm-remove-content-tab', request: 'Remove the details tab that contains widgets.', operationId: 'tab.remove', target: 'details', params: { replacementActiveTabId: 'overview' }, isPositive: true },
5048
+ ],
5049
+ };
5050
+
3678
5051
  /*
3679
5052
  * Public API Surface of praxis-tabs
3680
5053
  */
@@ -3683,4 +5056,4 @@ function providePraxisTabsMetadata() {
3683
5056
  * Generated bundle index. Do not edit.
3684
5057
  */
3685
5058
 
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 };
5059
+ 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 };