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