@praxisui/tabs 8.0.0-beta.26 → 8.0.0-beta.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,6 +82,7 @@ Outputs
82
82
  - `selectedIndexChange: number` Índice selecionado atualizado (ambos modos).
83
83
  - `selectedTabChange: MatTabChangeEvent` Evento nativo do MatTabGroup.
84
84
  - `focusChange, animationDone, indexFocused, selectFocusedIndex` Eventos nativos do Angular Material.
85
+ - `configChange: TabsConfigChangeEvent` Emite `inputPatch.config` quando autoria assistida altera `TabsMetadata`, permitindo que hosts persistam a nova configuração no manifesto/página canônica.
85
86
  - `widgetEvent: WidgetEventEnvelope` Bridge avançada/legado para transporte de eventos dos widgets internos com contexto da aba/link. Para conexões novas de widgets internos, use `composition.links` com `component-port + nestedPath`.
86
87
 
87
88
  ## Uso Controlado por Composição
@@ -187,6 +188,7 @@ Quick Setup
187
188
  - **Operation families:** `tab.add`, `tab.remove`, `tab.label.set`, `tab.icon.set`, `tab.order.set`, `tab.disabled.set`, `tab.visible.set`, `tab.active.set`, `layout.variant.set` e `tab.content.set`.
188
189
  - **Validation:** ids de abas/links devem ser estáveis e únicos, remoção destrutiva exige confirmação, `group` e `nav` não devem virar modos primários concorrentes, e o round-trip precisa preservar ordem, ids e selected index.
189
190
  - **Runtime/editor parity:** `tabs[].icon`, `tabs[].visible`, `nav.links[].icon` e `nav.links[].visible` são campos canônicos editáveis; itens com `visible: false` não são renderizados na navegação.
191
+ - **Current-area authoring:** pedidos como “nesta aba”, “aba atual” ou “dentro da aba” devem materializar `tab.content.set` no item ativo/resolvido, não `tab.add`; criar nova aba exige intenção explícita como “nova aba” ou “adicionar aba”.
190
192
  - **Registry projection:** o manifesto é exportado no `public-api` e projetado em `components['praxis-tabs'].authoringManifest` pelo AI Registry.
191
193
 
192
194
  ### Praxis Semantic Assistant
@@ -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';
@@ -28,8 +28,8 @@ import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
28
28
  import { BehaviorSubject, firstValueFrom, Subject, Subscription } from 'rxjs';
29
29
  import { produce } from 'immer';
30
30
  import { MatSnackBar } from '@angular/material/snack-bar';
31
- import { take, takeUntil } from 'rxjs/operators';
32
- import { BaseAiAdapter, AiBackendApiService, PraxisAssistantSessionRegistryService, PraxisAssistantTurnOrchestratorService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
31
+ import { takeUntil, take } from 'rxjs/operators';
32
+ import { BaseAiAdapter, shouldRoutePromptToGovernedDecision, AiBackendApiService, PraxisAssistantSessionRegistryService, PraxisAssistantTurnOrchestratorService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
33
33
 
34
34
  const DOCUMENT_KIND = 'praxis.tabs.editor';
35
35
  const DOCUMENT_VERSION = 1;
@@ -375,6 +375,13 @@ const PRAXIS_TABS_PT_BR = {
375
375
  'quickSetup.mode.nav': 'Navegação por links',
376
376
  'quickSetup.fields.addLabelPlaceholder': 'Ex.: Dados Gerais',
377
377
  'quickSetup.hints.emptyLabels': 'Adicione um ou mais rótulos para criar as abas.',
378
+ 'ai.review.ready': 'Plano de edição das abas pronto para revisar.',
379
+ 'ai.review.unsupportedPlan': 'O componentEditPlan das abas não possui operações suportadas para aplicar.',
380
+ 'ai.review.addTab': 'Adicionar aba: {{label}}.',
381
+ 'ai.review.renameTab': 'Renomear aba para: {{label}}.',
382
+ 'ai.review.singleAdjustment': 'Revisar ajuste da aba: {{label}}.',
383
+ 'ai.review.multipleAdjustments': 'Revisar {{count}} ajustes nas abas: {{items}}',
384
+ 'ai.review.multipleAdjustmentsShort': 'Revisar {{count}} ajustes nas abas.',
378
385
  };
379
386
  const PRAXIS_TABS_EN_US = {
380
387
  'settings.title': 'Configure tabs',
@@ -513,6 +520,13 @@ const PRAXIS_TABS_EN_US = {
513
520
  'quickSetup.mode.nav': 'Link navigation',
514
521
  'quickSetup.fields.addLabelPlaceholder': 'e.g. General Data',
515
522
  'quickSetup.hints.emptyLabels': 'Add one or more labels to create the tabs.',
523
+ 'ai.review.ready': 'Tabs edit plan ready to review.',
524
+ 'ai.review.unsupportedPlan': 'The tabs componentEditPlan does not contain supported operations to apply.',
525
+ 'ai.review.addTab': 'Add tab: {{label}}.',
526
+ 'ai.review.renameTab': 'Rename tab to: {{label}}.',
527
+ 'ai.review.singleAdjustment': 'Review tab adjustment: {{label}}.',
528
+ 'ai.review.multipleAdjustments': 'Review {{count}} tab adjustments: {{items}}',
529
+ 'ai.review.multipleAdjustmentsShort': 'Review {{count}} tab adjustments.',
516
530
  };
517
531
  function createPraxisTabsI18nConfig() {
518
532
  return {
@@ -2295,12 +2309,80 @@ const TABS_AI_CAPABILITIES = {
2295
2309
 
2296
2310
  class TabsAiAdapter extends BaseAiAdapter {
2297
2311
  tabs;
2312
+ translate;
2298
2313
  componentName = 'Praxis Tabs';
2299
2314
  componentId = 'praxis-tabs';
2300
2315
  componentType = 'tabs';
2301
- constructor(tabs) {
2316
+ constructor(tabs, translate) {
2302
2317
  super();
2303
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
+ };
2304
2386
  }
2305
2387
  getCurrentConfig() {
2306
2388
  return this.cloneConfig(this.tabs.config || {});
@@ -2402,16 +2484,40 @@ class TabsAiAdapter extends BaseAiAdapter {
2402
2484
  smartMergeTabsConfig(base, patch) {
2403
2485
  const result = deepMerge(base, patch);
2404
2486
  if (patch.tabs && Array.isArray(patch.tabs)) {
2405
- const merged = this.mergeByKey(base.tabs || [], patch.tabs, (t) => t.id || t.textLabel || '');
2487
+ const patchTabs = this.coalesceByKey(patch.tabs, (t) => t.id || t.textLabel || '');
2488
+ const merged = this.mergeByKey(base.tabs || [], patchTabs, (t) => t.id || t.textLabel || '');
2406
2489
  result.tabs = merged;
2407
2490
  }
2408
2491
  const patchLinks = patch.nav?.links;
2409
2492
  if (patchLinks && Array.isArray(patchLinks)) {
2410
- const merged = this.mergeByKey(base.nav?.links || [], patchLinks, (l) => l.id || l.label || '');
2493
+ const normalizedPatchLinks = this.coalesceByKey(patchLinks, (l) => l.id || l.label || '');
2494
+ const merged = this.mergeByKey(base.nav?.links || [], normalizedPatchLinks, (l) => l.id || l.label || '');
2411
2495
  result.nav = { ...(result.nav || {}), links: merged };
2412
2496
  }
2413
2497
  return result;
2414
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
+ }
2415
2521
  mergeByKey(baseArr, patchArr, keyFn) {
2416
2522
  const merged = baseArr.map((orig) => {
2417
2523
  const key = keyFn(orig);
@@ -2434,6 +2540,138 @@ class TabsAiAdapter extends BaseAiAdapter {
2434
2540
  return JSON.parse(JSON.stringify(config));
2435
2541
  }
2436
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
+ }
2437
2675
  }
2438
2676
 
2439
2677
  class TabsAgenticAuthoringTurnFlow {
@@ -2485,12 +2723,35 @@ class TabsAgenticAuthoringTurnFlow {
2485
2723
  }));
2486
2724
  return this.toTurnResult(this.compileAdapterResponse(response), request);
2487
2725
  }
2488
- async apply(_request) {
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
+ }
2489
2750
  return {
2490
2751
  state: 'error',
2491
2752
  phase: 'apply',
2492
- assistantMessage: 'As abas ainda exigem componentEditPlan validado pelo manifesto antes de aplicar mudancas locais.',
2493
- errorText: 'Aplicacao local bloqueada ate existir compilacao manifest-backed para praxis-tabs.',
2753
+ assistantMessage: 'Nao ha alteracao de abas pronta para aplicar.',
2754
+ errorText: 'Nao ha alteracao de abas pronta para aplicar.',
2494
2755
  canApply: false,
2495
2756
  pendingPatch: null,
2496
2757
  };
@@ -2554,10 +2815,29 @@ class TabsAgenticAuthoringTurnFlow {
2554
2815
  sessionId: response.sessionId ?? request.sessionId,
2555
2816
  assistantMessage: message,
2556
2817
  errorText: message,
2818
+ canApply: false,
2819
+ pendingPatch: null,
2557
2820
  diagnostics: response.warnings?.length ? { warnings: response.warnings } : undefined,
2558
2821
  };
2559
2822
  }
2560
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
+ }
2561
2841
  return {
2562
2842
  state: 'error',
2563
2843
  phase: 'review',
@@ -2585,6 +2865,16 @@ class TabsAgenticAuthoringTurnFlow {
2585
2865
  }
2586
2866
  compileAdapterResponse(response) {
2587
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
+ }
2588
2878
  if (!compiled) {
2589
2879
  return response;
2590
2880
  }
@@ -2666,28 +2956,7 @@ class TabsAgenticAuthoringTurnFlow {
2666
2956
  return rowCount > 0 ? { rowCount } : {};
2667
2957
  }
2668
2958
  shouldRouteToGovernedDecision(prompt, contextHints) {
2669
- const normalized = prompt.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
2670
- const recommendedFlow = this.toRecord(contextHints?.['domainCatalog'])?.['recommendedAuthoringFlow'];
2671
- if (recommendedFlow === 'shared_rule_authoring')
2672
- return true;
2673
- return [
2674
- 'regra',
2675
- 'politica',
2676
- 'policy',
2677
- 'compliance',
2678
- 'lgpd',
2679
- 'privacidade',
2680
- 'aprovacao',
2681
- 'aprovar',
2682
- 'publicar',
2683
- 'materializar',
2684
- 'enforcement',
2685
- 'validacao de negocio',
2686
- 'validar negocio',
2687
- 'elegibilidade',
2688
- 'permissao',
2689
- 'acesso',
2690
- ].some((term) => normalized.includes(term));
2959
+ return shouldRoutePromptToGovernedDecision(prompt, contextHints);
2691
2960
  }
2692
2961
  toGovernedDecisionHandoff(prompt, request) {
2693
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.';
@@ -2770,6 +3039,7 @@ class TabsAgenticAuthoringTurnFlow {
2770
3039
  }
2771
3040
  }
2772
3041
 
3042
+ const AGENTIC_PAGE_COMPOSITION_REQUEST_OUTPUT = 'agenticPageCompositionRequested';
2773
3043
  class PraxisTabs {
2774
3044
  i18n = inject(PraxisI18nService);
2775
3045
  settings = inject(SettingsPanelService);
@@ -2796,9 +3066,11 @@ class PraxisTabs {
2796
3066
  }
2797
3067
  }, ...(ngDevMode ? [{ debugName: "aiAssistantSessionEffect" }] : []));
2798
3068
  warnedMissingId = false;
3069
+ loadedStorageKey = null;
2799
3070
  config = null;
2800
3071
  tabsId;
2801
3072
  componentInstanceId;
3073
+ configPersistenceStrategy = 'storage-first';
2802
3074
  set selectedIndex(index) {
2803
3075
  if (index == null)
2804
3076
  return;
@@ -2814,8 +3086,9 @@ class PraxisTabs {
2814
3086
  selectedTabChange = new EventEmitter();
2815
3087
  indexFocused = new EventEmitter();
2816
3088
  selectFocusedIndex = new EventEmitter();
3089
+ configChange = new EventEmitter();
2817
3090
  widgetEvent = new EventEmitter();
2818
- aiAdapter = new TabsAiAdapter(this);
3091
+ aiAdapter = new TabsAiAdapter(this, (key, fallback, params) => this.i18n.t(key, params, fallback, PRAXIS_TABS_I18N_NAMESPACE));
2819
3092
  aiAssistantOpen = false;
2820
3093
  aiAssistantPrompt = '';
2821
3094
  aiAssistantViewState = null;
@@ -2839,25 +3112,21 @@ class PraxisTabs {
2839
3112
  controlledSelectedIndex;
2840
3113
  destroy$ = new Subject();
2841
3114
  widgetDefinitionCache = new WeakMap();
3115
+ generatedContentForm = new FormGroup({});
2842
3116
  ngOnInit() {
2843
3117
  this.syncSelectionFromConfig();
2844
- // Load stored config if tabsId provided
2845
- const key = this.storageKey();
2846
- if (key) {
2847
- this.storage.loadConfig(key).pipe(take(1)).subscribe((stored) => {
2848
- if (stored) {
2849
- this.config = stored;
2850
- }
2851
- this.syncSelectionFromConfig();
2852
- this.reapplyControlledSelectedIndex();
2853
- });
2854
- }
3118
+ this.syncGeneratedContentForm();
3119
+ this.loadStoredConfigForCurrentIdentity({ warnMissingId: false });
2855
3120
  }
2856
3121
  ngOnChanges(changes) {
3122
+ if (changes['tabsId'] || changes['componentInstanceId'] || changes['configPersistenceStrategy']) {
3123
+ this.loadStoredConfigForCurrentIdentity({ warnMissingId: false });
3124
+ }
2857
3125
  if (changes['config'] && this.config) {
2858
3126
  // Reset loaded caches on config change and seed with current selections
2859
3127
  this.syncSelectionFromConfig();
2860
- // Persist when tabsId provided
3128
+ this.syncGeneratedContentForm();
3129
+ // Persist when tabsId provided and the instance is storage-owned.
2861
3130
  this.persistConfig(this.config);
2862
3131
  this.reapplyControlledSelectedIndex();
2863
3132
  }
@@ -2900,6 +3169,9 @@ class PraxisTabs {
2900
3169
  const index = entries.findIndex((entry) => entry.index === this.selectedIndexSignal());
2901
3170
  return index >= 0 ? index : 0;
2902
3171
  }
3172
+ effectiveContentForm() {
3173
+ return this.form || this.generatedContentForm;
3174
+ }
2903
3175
  onVisibleTabIndexChange(index) {
2904
3176
  const entry = this.visibleTabEntries()[index];
2905
3177
  if (!entry)
@@ -3129,6 +3401,12 @@ class PraxisTabs {
3129
3401
  this.syncAiAssistantSession();
3130
3402
  }
3131
3403
  onAiAssistantSubmit(prompt) {
3404
+ if (this.shouldDelegateAiAssistantPromptToPageBuilder(prompt)) {
3405
+ this.emitAgenticPageCompositionRequest(prompt);
3406
+ this.aiAssistantPrompt = '';
3407
+ this.closeAiAssistant();
3408
+ return;
3409
+ }
3132
3410
  this.aiAssistantController?.submitPrompt(prompt).subscribe((state) => {
3133
3411
  this.aiAssistantPrompt = '';
3134
3412
  this.aiAssistantViewState = state;
@@ -3415,6 +3693,7 @@ class PraxisTabs {
3415
3693
  draft.group.selectedIndex = (draft.tabs.length || 1) - 1;
3416
3694
  });
3417
3695
  this.config = next;
3696
+ this.syncGeneratedContentForm();
3418
3697
  this.persistConfig(this.config);
3419
3698
  this.selectedIndexSignal.set(this.config?.group?.selectedIndex ?? 0);
3420
3699
  }
@@ -3476,6 +3755,7 @@ class PraxisTabs {
3476
3755
  }
3477
3756
  this.config = plan.canonicalConfig;
3478
3757
  this.syncSelectionFromConfig();
3758
+ this.syncGeneratedContentForm();
3479
3759
  if (plan.runtime.rebuildLazyState) {
3480
3760
  this.groupLoaded.clear();
3481
3761
  this.navLoaded.clear();
@@ -3492,10 +3772,27 @@ class PraxisTabs {
3492
3772
  this.persistConfig(this.config);
3493
3773
  }
3494
3774
  }
3495
- storageKey() {
3496
- const id = this.componentKeyId();
3775
+ storageKey(options = {}) {
3776
+ const id = this.componentKeyId(options);
3497
3777
  return id ? `tabs:${id}` : null;
3498
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
+ }
3499
3796
  syncSelectionFromConfig() {
3500
3797
  const tabsLength = this.config?.tabs?.length ?? 0;
3501
3798
  const linksLength = this.config?.nav?.links?.length ?? 0;
@@ -3518,12 +3815,17 @@ class PraxisTabs {
3518
3815
  }
3519
3816
  }
3520
3817
  persistConfig(config) {
3818
+ if (this.usesInputFirstConfig())
3819
+ return;
3521
3820
  const key = this.storageKey();
3522
3821
  if (!key || !config)
3523
3822
  return;
3524
3823
  this.storage.saveConfig(key, config).pipe(take(1)).subscribe({ error: () => { } });
3525
3824
  }
3526
- componentKeyId() {
3825
+ usesInputFirstConfig() {
3826
+ return this.configPersistenceStrategy === 'input-first' && !!this.config;
3827
+ }
3828
+ componentKeyId(options = {}) {
3527
3829
  const key = this.componentKeys.buildComponentId({
3528
3830
  componentType: 'praxis-tabs',
3529
3831
  componentId: this.tabsId,
@@ -3532,7 +3834,7 @@ class PraxisTabs {
3532
3834
  route: this.route,
3533
3835
  requireComponentId: true,
3534
3836
  });
3535
- if (!key)
3837
+ if (!key && options.warnMissingId !== false)
3536
3838
  this.warnMissingId();
3537
3839
  return key;
3538
3840
  }
@@ -3562,7 +3864,9 @@ class PraxisTabs {
3562
3864
  applyConfigFromAdapter(next) {
3563
3865
  this.config = next;
3564
3866
  this.syncSelectionFromConfig();
3867
+ this.syncGeneratedContentForm();
3565
3868
  this.persistConfig(this.config);
3869
+ this.configChange.emit({ inputPatch: { config: this.cloneTabsMetadata(this.config) } });
3566
3870
  }
3567
3871
  // =====================
3568
3872
  // Lazy load helpers
@@ -3596,6 +3900,12 @@ class PraxisTabs {
3596
3900
  trackWidgetDefinition(index, widget) {
3597
3901
  return widget.childWidgetKey || widget.id || `widget:${index}`;
3598
3902
  }
3903
+ safeWidgetDefinitions(widgets) {
3904
+ if (!Array.isArray(widgets)) {
3905
+ return [];
3906
+ }
3907
+ return widgets.filter((widget) => (!!widget && typeof widget === 'object'));
3908
+ }
3599
3909
  resolveWidgetDefinition(widget) {
3600
3910
  const cached = this.widgetDefinitionCache.get(widget);
3601
3911
  if (cached) {
@@ -3605,12 +3915,67 @@ class PraxisTabs {
3605
3915
  this.widgetDefinitionCache.set(widget, clone);
3606
3916
  return clone;
3607
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
+ }
3608
3940
  emitWidgetEvent(path, ev) {
3609
3941
  this.widgetEvent.emit({
3610
3942
  ...ev,
3611
3943
  path: [...path, ...(ev.path || [])],
3612
3944
  });
3613
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
+ }
3614
3979
  tabEventPath(tabId, tabIndex) {
3615
3980
  return [
3616
3981
  { kind: 'tabs', id: this.tabsId },
@@ -3770,8 +4135,17 @@ class PraxisTabs {
3770
4135
  catch { }
3771
4136
  return JSON.parse(JSON.stringify(widget));
3772
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
+ }
3773
4147
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
3774
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisTabs, isStandalone: true, selector: "praxis-tabs", inputs: { config: "config", tabsId: "tabsId", componentInstanceId: "componentInstanceId", selectedIndex: "selectedIndex", enableCustomization: "enableCustomization", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", widgetEvent: "widgetEvent" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], usesOnChanges: true, ngImport: i0, template: `
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: `
3775
4149
  <div
3776
4150
  class="praxis-tabs-root"
3777
4151
  [class.density-compact]="config?.appearance?.density === 'compact'"
@@ -3895,16 +4269,16 @@ class PraxisTabs {
3895
4269
  <ng-container
3896
4270
  *ngIf="config?.nav?.links?.[currentNavIndex()] as l"
3897
4271
  >
3898
- <ng-container *ngIf="(l.content?.length || l.widgets?.length) && navContentReady(currentNavIndex()); else emptyNav">
3899
- <ng-container *ngIf="l.content && form">
4272
+ <ng-container *ngIf="(l.content?.length || safeWidgetDefinitions(l.widgets).length) && navContentReady(currentNavIndex()); else emptyNav">
4273
+ <ng-container *ngIf="l.content">
3900
4274
  <ng-container
3901
4275
  dynamicFieldLoader
3902
4276
  [fields]="l.content || []"
3903
- [formGroup]="form!"
4277
+ [formGroup]="effectiveContentForm()"
3904
4278
  ></ng-container>
3905
4279
  </ng-container>
3906
- <ng-container *ngIf="l.widgets?.length">
3907
- <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">
3908
4282
  <ng-container
3909
4283
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
3910
4284
  [context]="context || {}"
@@ -3999,34 +4373,32 @@ class PraxisTabs {
3999
4373
  </ng-container>
4000
4374
  </ng-template>
4001
4375
 
4002
- <ng-template matTabContent>
4003
- <ng-container *ngIf="(entry.tab.content?.length || entry.tab.widgets?.length) && groupContentReady(entry.index); else emptyTab">
4004
- <ng-container *ngIf="entry.tab.content && form">
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">
4005
4386
  <ng-container
4006
- dynamicFieldLoader
4007
- [fields]="entry.tab.content || []"
4008
- [formGroup]="form!"
4387
+ [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
4388
+ [context]="context || {}"
4389
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
4009
4390
  ></ng-container>
4010
4391
  </ng-container>
4011
- <ng-container *ngIf="entry.tab.widgets?.length">
4012
- <ng-container *ngFor="let w of entry.tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
4013
- <ng-container
4014
- [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
4015
- [context]="context || {}"
4016
- (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
4017
- ></ng-container>
4018
- </ng-container>
4019
- </ng-container>
4020
4392
  </ng-container>
4021
- <ng-template #emptyTab>
4022
- <praxis-empty-state-card
4023
- [inline]="true"
4024
- icon="dashboard_customize"
4025
- [title]="t('emptyState.tabTitle', 'Sem conteudo nesta aba')"
4026
- [description]="t('emptyState.tabDescription', 'Adicione conteudo ou use o editor para configurar.')"
4027
- [primaryAction]="{ label: t('emptyState.openEditor', 'Abrir editor'), icon: 'tune', action: openEditor.bind(this) }"
4028
- ></praxis-empty-state-card>
4029
- </ng-template>
4393
+ </ng-container>
4394
+ <ng-template #emptyTab>
4395
+ <praxis-empty-state-card
4396
+ [inline]="true"
4397
+ icon="dashboard_customize"
4398
+ [title]="t('emptyState.tabTitle', 'Sem conteudo nesta aba')"
4399
+ [description]="t('emptyState.tabDescription', 'Adicione conteudo ou use o editor para configurar.')"
4400
+ [primaryAction]="{ label: t('emptyState.openEditor', 'Abrir editor'), icon: 'tune', action: openEditor.bind(this) }"
4401
+ ></praxis-empty-state-card>
4030
4402
  </ng-template>
4031
4403
  </mat-tab>
4032
4404
  </mat-tab-group>
@@ -4054,7 +4426,7 @@ class PraxisTabs {
4054
4426
  <mat-icon [praxisIcon]="'restart_alt'"></mat-icon>
4055
4427
  </button>
4056
4428
  </div>
4057
- `, isInline: true, styles: [".praxis-tabs-root{position:relative;display:block}.praxis-tabs-group.align-start .mat-mdc-tab-header{justify-content:flex-start}.praxis-tabs-group.align-center .mat-mdc-tab-header{justify-content:center}.praxis-tabs-group.align-end .mat-mdc-tab-header{justify-content:flex-end}.density-compact .mat-mdc-tab-body-content{padding:8px}.density-comfortable .mat-mdc-tab-body-content{padding:16px}.density-spacious .mat-mdc-tab-body-content{padding:24px}.tabs-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}.tabs-ai-assistant-trigger{box-shadow:var(--md-sys-elevation-level2)}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.edit-fab-secondary{right:56px}.tab-empty{padding:16px;color:var(--md-sys-color-on-surface-variant);font-style:italic}.high-contrast{filter:contrast(1.2)}.reduce-motion{--mat-animation-duration: 0ms}.drag-handle{display:inline-flex;align-items:center;vertical-align:middle;margin-right:4px;cursor:grab}.tab-label-icon{font-size:18px;width:18px;height:18px;margin-right:6px;vertical-align:middle}:host-context(.pdx-gridster-item) .praxis-tabs-root{display:flex;flex-direction:column;height:100%;min-height:0}:host-context(.pdx-gridster-item) .praxis-tabs-group,:host-context(.pdx-gridster-item) .mat-mdc-tab-group{flex:1 1 auto;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-wrapper,:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{height:100%;min-height:0}:host-context(.pdx-gridster-item) .mat-mdc-tab-body-content{overflow:auto}:host-context(.pdx-gridster-item) .praxis-tabnav-content{flex:1 1 auto;min-height:0;overflow:auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i3$1.MatTabContent, selector: "[matTabContent]" }, { kind: "directive", type: i3$1.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i3$1.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i3$1.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "component", type: i3$1.MatTabNav, selector: "[mat-tab-nav-bar]", inputs: ["fitInkBarToContent", "mat-stretch-tabs", "animationDuration", "backgroundColor", "disableRipple", "color", "tabPanel"], exportAs: ["matTabNavBar", "matTabNav"] }, { kind: "component", type: i3$1.MatTabNavPanel, selector: "mat-tab-nav-panel", inputs: ["id"], exportAs: ["matTabNavPanel"] }, { kind: "component", type: i3$1.MatTabLink, selector: "[mat-tab-link], [matTabLink]", inputs: ["active", "disabled", "disableRipple", "tabIndex", "id"], exportAs: ["matTabLink"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i7.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6$1.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6$1.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i10.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i10.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i10.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone"] }, { kind: "directive", type: DynamicFieldLoaderDirective, selector: "[dynamicFieldLoader]", inputs: ["fields", "formGroup", "enableExternalControlBinding", "itemTemplate", "debugTrace", "debugTraceLabel", "readonlyMode", "disabledMode", "presentationMode", "visible", "canvasMode"], outputs: ["componentsCreated", "fieldCreated", "fieldDestroyed", "renderError", "canvasMouseEnter", "canvasMouseLeave", "canvasClick"] }, { kind: "directive", type: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "ownerWidgetKey", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent", "widgetDiagnostic"], exportAs: ["dynamicWidgetLoader"] }, { kind: "component", type: PraxisAiAssistantShellComponent, selector: "praxis-ai-assistant-shell", inputs: ["labels", "mode", "state", "contextItems", "attachments", "messages", "quickReplies", "prompt", "statusText", "errorText", "testIdPrefix", "panelTestId", "submitTestId", "applyTestId", "primaryAction", "secondaryActions", "governanceActions", "busy", "canSubmit", "canApply", "submitOnEnter", "showAttachAction", "enablePastedAttachments", "enableFileAttachments", "attachmentAccept", "attachmentMultiple", "draggable", "resizable", "minWidth", "minHeight", "margin", "layout"], outputs: ["promptChange", "submitPrompt", "apply", "retryTurn", "cancelTurn", "shellAction", "close", "attach", "attachmentsPasted", "attachmentsSelected", "removeAttachment", "messageAction", "editMessage", "resendMessage", "quickReply", "layoutChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 });
4058
4430
  }
4059
4431
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisTabs, decorators: [{
4060
4432
  type: Component,
@@ -4195,16 +4567,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4195
4567
  <ng-container
4196
4568
  *ngIf="config?.nav?.links?.[currentNavIndex()] as l"
4197
4569
  >
4198
- <ng-container *ngIf="(l.content?.length || l.widgets?.length) && navContentReady(currentNavIndex()); else emptyNav">
4199
- <ng-container *ngIf="l.content && form">
4570
+ <ng-container *ngIf="(l.content?.length || safeWidgetDefinitions(l.widgets).length) && navContentReady(currentNavIndex()); else emptyNav">
4571
+ <ng-container *ngIf="l.content">
4200
4572
  <ng-container
4201
4573
  dynamicFieldLoader
4202
4574
  [fields]="l.content || []"
4203
- [formGroup]="form!"
4575
+ [formGroup]="effectiveContentForm()"
4204
4576
  ></ng-container>
4205
4577
  </ng-container>
4206
- <ng-container *ngIf="l.widgets?.length">
4207
- <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">
4208
4580
  <ng-container
4209
4581
  [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
4210
4582
  [context]="context || {}"
@@ -4299,34 +4671,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4299
4671
  </ng-container>
4300
4672
  </ng-template>
4301
4673
 
4302
- <ng-template matTabContent>
4303
- <ng-container *ngIf="(entry.tab.content?.length || entry.tab.widgets?.length) && groupContentReady(entry.index); else emptyTab">
4304
- <ng-container *ngIf="entry.tab.content && form">
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">
4305
4684
  <ng-container
4306
- dynamicFieldLoader
4307
- [fields]="entry.tab.content || []"
4308
- [formGroup]="form!"
4685
+ [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
4686
+ [context]="context || {}"
4687
+ (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
4309
4688
  ></ng-container>
4310
4689
  </ng-container>
4311
- <ng-container *ngIf="entry.tab.widgets?.length">
4312
- <ng-container *ngFor="let w of entry.tab.widgets; let wi = index; trackBy: trackWidgetDefinition">
4313
- <ng-container
4314
- [dynamicWidgetLoader]="resolveWidgetDefinition(w)"
4315
- [context]="context || {}"
4316
- (widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
4317
- ></ng-container>
4318
- </ng-container>
4319
- </ng-container>
4320
4690
  </ng-container>
4321
- <ng-template #emptyTab>
4322
- <praxis-empty-state-card
4323
- [inline]="true"
4324
- icon="dashboard_customize"
4325
- [title]="t('emptyState.tabTitle', 'Sem conteudo nesta aba')"
4326
- [description]="t('emptyState.tabDescription', 'Adicione conteudo ou use o editor para configurar.')"
4327
- [primaryAction]="{ label: t('emptyState.openEditor', 'Abrir editor'), icon: 'tune', action: openEditor.bind(this) }"
4328
- ></praxis-empty-state-card>
4329
- </ng-template>
4691
+ </ng-container>
4692
+ <ng-template #emptyTab>
4693
+ <praxis-empty-state-card
4694
+ [inline]="true"
4695
+ icon="dashboard_customize"
4696
+ [title]="t('emptyState.tabTitle', 'Sem conteudo nesta aba')"
4697
+ [description]="t('emptyState.tabDescription', 'Adicione conteudo ou use o editor para configurar.')"
4698
+ [primaryAction]="{ label: t('emptyState.openEditor', 'Abrir editor'), icon: 'tune', action: openEditor.bind(this) }"
4699
+ ></praxis-empty-state-card>
4330
4700
  </ng-template>
4331
4701
  </mat-tab>
4332
4702
  </mat-tab-group>
@@ -4362,6 +4732,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4362
4732
  args: [{ required: true }]
4363
4733
  }], componentInstanceId: [{
4364
4734
  type: Input
4735
+ }], configPersistenceStrategy: [{
4736
+ type: Input
4365
4737
  }], selectedIndex: [{
4366
4738
  type: Input
4367
4739
  }], enableCustomization: [{
@@ -4382,6 +4754,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4382
4754
  type: Output
4383
4755
  }], selectFocusedIndex: [{
4384
4756
  type: Output
4757
+ }], configChange: [{
4758
+ type: Output
4385
4759
  }], widgetEvent: [{
4386
4760
  type: Output
4387
4761
  }] } });
@@ -4542,6 +4916,20 @@ const PRAXIS_TABS_PORTS = [
4542
4916
  description: 'Evento canonico emitido quando a selecao de aba muda.',
4543
4917
  exposure: { public: true, group: 'events' },
4544
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
+ },
4545
4933
  {
4546
4934
  id: 'widgetEvent',
4547
4935
  label: 'Evento interno de widget',
@@ -4586,6 +4974,13 @@ const PRAXIS_TABS_COMPONENT_METADATA = {
4586
4974
  label: 'ID da instância',
4587
4975
  description: 'Identificador opcional para múltiplas instâncias na mesma rota',
4588
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
+ },
4589
4984
  {
4590
4985
  name: 'selectedIndex',
4591
4986
  type: 'number',
@@ -4614,6 +5009,12 @@ const PRAXIS_TABS_COMPONENT_METADATA = {
4614
5009
  ],
4615
5010
  outputs: [
4616
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
+ },
4617
5018
  { name: 'selectedTabChange', type: 'MatTabChangeEvent', label: 'Troca de aba' },
4618
5019
  { name: 'animationDone', type: 'void' },
4619
5020
  { name: 'focusChange', type: 'MatTabChangeEvent', label: 'Foco alterado' },
@@ -4749,6 +5150,11 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
4749
5150
  { name: 'config', type: 'TabsMetadata', description: 'Canonical tabs/nav configuration.' },
4750
5151
  { name: 'tabsId', type: 'string', description: 'Stable id used to derive persistence scope.' },
4751
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
+ },
4752
5158
  { name: 'form', type: 'FormGroup', description: 'FormGroup consumed by dynamic field content.' },
4753
5159
  { name: 'context', type: 'Record<string, any>', description: 'Context passed to nested widgets.' },
4754
5160
  { name: 'enableCustomization', type: 'boolean', description: 'Enables Settings Panel authoring surfaces.' },
@@ -5039,11 +5445,14 @@ const PRAXIS_TABS_AUTHORING_MANIFEST = {
5039
5445
  ],
5040
5446
  examples: [
5041
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 },
5042
5450
  { id: 'rename-tab', request: 'Rename the details tab to Account Details.', operationId: 'tab.label.set', target: 'details', params: { textLabel: 'Account Details' }, isPositive: true },
5043
5451
  { id: 'reorder-tabs', request: 'Move billing before overview.', operationId: 'tab.order.set', target: 'billing', params: { beforeTabId: 'overview' }, isPositive: true },
5044
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 },
5045
5453
  { id: 'activate-tab', request: 'Open the documents tab by default.', operationId: 'tab.active.set', target: 'documents', params: { tabId: 'documents', selectedIndex: 2 }, isPositive: true },
5046
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 },
5047
5456
  { id: 'confirm-remove-content-tab', request: 'Remove the details tab that contains widgets.', operationId: 'tab.remove', target: 'details', params: { replacementActiveTabId: 'overview' }, isPositive: true },
5048
5457
  ],
5049
5458
  };
package/index.d.ts CHANGED
@@ -4,16 +4,20 @@ import { MatTabChangeEvent } from '@angular/material/tabs';
4
4
  import { AiCapability, WidgetDefinition, WidgetEventEnvelope, WidgetEventPathSegment, PraxisI18nConfig, ComponentDocMeta, ComponentMetadataRegistry, SettingsValueProvider as SettingsValueProvider$1, AiCapabilityCategory, AiValueKind, AiCapabilityCatalog, ComponentAuthoringManifest } from '@praxisui/core';
5
5
  import { FormGroup } from '@angular/forms';
6
6
  import { CdkDragDrop } from '@angular/cdk/drag-drop';
7
- import { BaseAiAdapter, PatchResult, PraxisAssistantTurnViewState, PraxisAssistantShellLayout, PraxisAssistantShellLabels, PraxisAssistantSessionSnapshot, PraxisAssistantShellQuickReply, PraxisAssistantShellMessage } from '@praxisui/ai';
7
+ import { BaseAiAdapter, AiResponseCompileResult, PatchResult, PraxisAssistantTurnViewState, PraxisAssistantShellLayout, PraxisAssistantShellLabels, PraxisAssistantSessionSnapshot, PraxisAssistantShellQuickReply, PraxisAssistantShellMessage } from '@praxisui/ai';
8
8
  import { BehaviorSubject } from 'rxjs';
9
9
  import { SettingsValueProvider } from '@praxisui/settings-panel';
10
10
 
11
+ type TabsAiSummaryParams = Record<string, string | number | boolean | null | undefined>;
12
+ type TabsAiTranslate = (key: string, fallback: string, params?: TabsAiSummaryParams) => string;
11
13
  declare class TabsAiAdapter extends BaseAiAdapter<TabsMetadata> {
12
14
  private tabs;
15
+ private readonly translate?;
13
16
  componentName: string;
14
17
  componentId: string;
15
18
  componentType: string;
16
- constructor(tabs: PraxisTabs);
19
+ constructor(tabs: PraxisTabs, translate?: TabsAiTranslate | undefined);
20
+ compileAiResponse(response: Record<string, unknown>): AiResponseCompileResult | null;
17
21
  getCurrentConfig(): TabsMetadata;
18
22
  getCapabilities(): AiCapability[];
19
23
  getDataProfile(): Record<string, any>;
@@ -25,8 +29,19 @@ declare class TabsAiAdapter extends BaseAiAdapter<TabsMetadata> {
25
29
  applyPatch(patch: Partial<TabsMetadata>): Promise<PatchResult>;
26
30
  private applyConfig;
27
31
  private smartMergeTabsConfig;
32
+ private coalesceByKey;
28
33
  private mergeByKey;
29
34
  private cloneConfig;
35
+ private resolveTargetId;
36
+ private resolveTextLabel;
37
+ private resolveTabAddInput;
38
+ private resolveTabContentInput;
39
+ private summarizeOperations;
40
+ private t;
41
+ private interpolate;
42
+ private toRecord;
43
+ private toText;
44
+ private normalizeToken;
30
45
  }
31
46
 
32
47
  interface TabsMetadata {
@@ -39,6 +54,12 @@ interface TabsMetadata {
39
54
  tabs?: TabMetadata[];
40
55
  nav?: TabNavMetadata;
41
56
  }
57
+ interface TabsConfigChangeEvent {
58
+ inputPatch: {
59
+ config: TabsMetadata;
60
+ };
61
+ }
62
+ type TabsConfigPersistenceStrategy = 'storage-first' | 'input-first';
42
63
  interface TabsAppearanceConfig {
43
64
  density?: 'compact' | 'comfortable' | 'spacious';
44
65
  themeClass?: string;
@@ -151,9 +172,11 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
151
172
  private readonly route;
152
173
  private readonly aiAssistantSessionEffect;
153
174
  private warnedMissingId;
175
+ private loadedStorageKey;
154
176
  config: TabsMetadata | null;
155
177
  tabsId: string;
156
178
  componentInstanceId?: string;
179
+ configPersistenceStrategy: TabsConfigPersistenceStrategy;
157
180
  set selectedIndex(index: number | null | undefined);
158
181
  enableCustomization: boolean;
159
182
  form: FormGroup | null;
@@ -164,6 +187,7 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
164
187
  selectedTabChange: EventEmitter<MatTabChangeEvent>;
165
188
  indexFocused: EventEmitter<number>;
166
189
  selectFocusedIndex: EventEmitter<number>;
190
+ configChange: EventEmitter<TabsConfigChangeEvent>;
167
191
  widgetEvent: EventEmitter<WidgetEventEnvelope>;
168
192
  aiAdapter: TabsAiAdapter;
169
193
  aiAssistantOpen: boolean;
@@ -180,6 +204,7 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
180
204
  private controlledSelectedIndex?;
181
205
  private readonly destroy$;
182
206
  private readonly widgetDefinitionCache;
207
+ private readonly generatedContentForm;
183
208
  ngOnInit(): void;
184
209
  ngOnChanges(changes: SimpleChanges): void;
185
210
  ngOnDestroy(): void;
@@ -196,6 +221,7 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
196
221
  }>;
197
222
  protected selectedVisibleNavIndex(): number;
198
223
  protected selectedVisibleTabIndex(): number;
224
+ protected effectiveContentForm(): FormGroup;
199
225
  protected onVisibleTabIndexChange(index: number): void;
200
226
  onNavClick(i: number): void;
201
227
  onNavDrop(event: CdkDragDrop<any>): void;
@@ -239,9 +265,11 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
239
265
  t(key: string, fallback: string): string;
240
266
  private applyTabsAuthoringPlan;
241
267
  private storageKey;
268
+ private loadStoredConfigForCurrentIdentity;
242
269
  private syncSelectionFromConfig;
243
270
  private pruneLoadedIndexes;
244
271
  private persistConfig;
272
+ private usesInputFirstConfig;
245
273
  private componentKeyId;
246
274
  private warnMissingId;
247
275
  private clampIndex;
@@ -261,8 +289,15 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
261
289
  protected trackNavLink(index: number, link: TabLinkMetadata): string;
262
290
  protected trackTab(index: number, tab: TabMetadata): string;
263
291
  protected trackWidgetDefinition(index: number, widget: WidgetDefinition): string;
292
+ protected safeWidgetDefinitions(widgets: unknown): WidgetDefinition[];
264
293
  protected resolveWidgetDefinition(widget: WidgetDefinition): WidgetDefinition;
294
+ private syncGeneratedContentForm;
295
+ private collectContentFields;
296
+ private resolveContentFieldName;
265
297
  protected emitWidgetEvent(path: WidgetEventPathSegment[], ev: WidgetEventEnvelope): void;
298
+ private emitAgenticPageCompositionRequest;
299
+ private shouldDelegateAiAssistantPromptToPageBuilder;
300
+ private normalizeAssistantPrompt;
266
301
  protected tabEventPath(tabId: string | undefined, tabIndex: number): WidgetEventPathSegment[];
267
302
  protected linkEventPath(linkId: string | undefined, linkIndex: number): WidgetEventPathSegment[];
268
303
  protected styleScopeId(): string;
@@ -270,8 +305,9 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
270
305
  private sanitizeCssValue;
271
306
  protected styleCss(): string | null;
272
307
  private cloneWidgetDefinition;
308
+ private cloneTabsMetadata;
273
309
  static ɵfac: i0.ɵɵFactoryDeclaration<PraxisTabs, never>;
274
- static ɵcmp: i0.ɵɵComponentDeclaration<PraxisTabs, "praxis-tabs", never, { "config": { "alias": "config"; "required": false; }; "tabsId": { "alias": "tabsId"; "required": true; }; "componentInstanceId": { "alias": "componentInstanceId"; "required": false; }; "selectedIndex": { "alias": "selectedIndex"; "required": false; }; "enableCustomization": { "alias": "enableCustomization"; "required": false; }; "form": { "alias": "form"; "required": false; }; "context": { "alias": "context"; "required": false; }; }, { "animationDone": "animationDone"; "focusChange": "focusChange"; "selectedIndexChange": "selectedIndexChange"; "selectedTabChange": "selectedTabChange"; "indexFocused": "indexFocused"; "selectFocusedIndex": "selectFocusedIndex"; "widgetEvent": "widgetEvent"; }, never, never, true, never>;
310
+ static ɵcmp: i0.ɵɵComponentDeclaration<PraxisTabs, "praxis-tabs", never, { "config": { "alias": "config"; "required": false; }; "tabsId": { "alias": "tabsId"; "required": true; }; "componentInstanceId": { "alias": "componentInstanceId"; "required": false; }; "configPersistenceStrategy": { "alias": "configPersistenceStrategy"; "required": false; }; "selectedIndex": { "alias": "selectedIndex"; "required": false; }; "enableCustomization": { "alias": "enableCustomization"; "required": false; }; "form": { "alias": "form"; "required": false; }; "context": { "alias": "context"; "required": false; }; }, { "animationDone": "animationDone"; "focusChange": "focusChange"; "selectedIndexChange": "selectedIndexChange"; "selectedTabChange": "selectedTabChange"; "indexFocused": "indexFocused"; "selectFocusedIndex": "selectFocusedIndex"; "configChange": "configChange"; "widgetEvent": "widgetEvent"; }, never, never, true, never>;
275
311
  }
276
312
 
277
313
  declare const PRAXIS_TABS_I18N_NAMESPACE = "praxisTabs";
@@ -525,4 +561,4 @@ declare const TABS_AI_CAPABILITIES: CapabilityCatalog;
525
561
  declare const PRAXIS_TABS_AUTHORING_MANIFEST: ComponentAuthoringManifest;
526
562
 
527
563
  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 };
528
- export type { Capability, CapabilityCatalog, CapabilityCategory, PraxisTabsWidgetEditorInputs, PraxisTabsWidgetEditorValue, TabGroupMetadata, TabLinkMetadata, TabMetadata, TabNavMetadata, TabsAccessibilityConfig, TabsAppearanceConfig, TabsApplyPlan, TabsAuthoringBindings, TabsAuthoringDocument, TabsBehaviorConfig, TabsBindingsDiff, TabsEditorDiagnostic, TabsEditorDiagnosticLevel, TabsEditorDocumentKind, TabsEventConfig, TabsMetadata, TabsRuntimeContext, TabsRuntimePlan, TabsStyleTokens, ValueKind };
564
+ export type { Capability, CapabilityCatalog, CapabilityCategory, PraxisTabsWidgetEditorInputs, PraxisTabsWidgetEditorValue, TabGroupMetadata, TabLinkMetadata, TabMetadata, TabNavMetadata, TabsAccessibilityConfig, TabsAppearanceConfig, TabsApplyPlan, TabsAuthoringBindings, TabsAuthoringDocument, TabsBehaviorConfig, TabsBindingsDiff, TabsConfigChangeEvent, TabsConfigPersistenceStrategy, TabsEditorDiagnostic, TabsEditorDiagnosticLevel, TabsEditorDocumentKind, TabsEventConfig, TabsMetadata, TabsRuntimeContext, TabsRuntimePlan, TabsStyleTokens, ValueKind };
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@praxisui/tabs",
3
- "version": "8.0.0-beta.26",
3
+ "version": "8.0.0-beta.28",
4
4
  "description": "Configurable tabs (group and nav) for Praxis UI with metadata-driven content and runtime editor.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^20.0.0",
7
7
  "@angular/core": "^20.0.0",
8
8
  "@angular/material": "^20.0.0",
9
9
  "@angular/cdk": "^20.0.0",
10
- "@praxisui/core": "^8.0.0-beta.26",
11
- "@praxisui/dynamic-fields": "^8.0.0-beta.26",
12
- "@praxisui/settings-panel": "^8.0.0-beta.26",
10
+ "@praxisui/core": "^8.0.0-beta.28",
11
+ "@praxisui/dynamic-fields": "^8.0.0-beta.28",
12
+ "@praxisui/settings-panel": "^8.0.0-beta.28",
13
13
  "@angular/forms": "^20.0.0",
14
14
  "@angular/router": "^20.0.0",
15
- "@praxisui/ai": "^8.0.0-beta.26",
15
+ "@praxisui/ai": "^8.0.0-beta.28",
16
16
  "rxjs": "~7.8.0"
17
17
  },
18
18
  "dependencies": {
@@ -175,6 +175,7 @@ Este arquivo foi adaptado para o padrao canonico atual sem remover conteudo tecn
175
175
  | `selectedTabChange` | `MatTabChangeEvent` | Troca de aba no `mat-tab-group` | Partial | Preservado da documentação anterior. |
176
176
  | `indexFocused` | `number` | Evento nativo de foco indexado (Material) | Partial | Preservado da documentação anterior. |
177
177
  | `selectFocusedIndex` | `number` | Evento nativo de selecao por foco (Material) | Partial | Preservado da documentação anterior. |
178
+ | `configChange` | `{ inputPatch: { config: TabsMetadata } }` | Autoria assistida aplica nova configuracao de abas via adapter | Partial | Evento canonico para hosts persistirem `inputs.config` no manifesto/pagina. |
178
179
  | `widgetEvent` | `{ tabId?, tabIndex?, linkId?, linkIndex?, sourceId, output?, payload? }` | Reemissao de evento vindo de widget interno com contexto de localizacao | Partial | Preservado da documentação anterior. |
179
180
 
180
181
  ### External side channels
@@ -425,6 +426,7 @@ Notas operacionais:
425
426
  | `selectedTabChange` | `MatTabChangeEvent` | Troca de aba no `mat-tab-group` | Active |
426
427
  | `indexFocused` | `number` | Evento nativo de foco indexado (Material) | Active |
427
428
  | `selectFocusedIndex` | `number` | Evento nativo de selecao por foco (Material) | Active |
429
+ | `configChange` | `{ inputPatch: { config: TabsMetadata } }` | Autoria assistida aplica nova configuracao de abas via adapter | Active |
428
430
  | `widgetEvent` | `{ tabId?, tabIndex?, linkId?, linkIndex?, sourceId, output?, payload? }` | Reemissao de evento vindo de widget interno com contexto de localizacao | Active |
429
431
 
430
432
  #### Precedencia de modo e fluxo de render