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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import * as i1 from '@angular/common';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { EventEmitter, inject, ChangeDetectorRef, DestroyRef, ViewChildren, ViewChild, Output, Input, ChangeDetectionStrategy, Component, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
4
+ import { EventEmitter, inject, ChangeDetectorRef, DestroyRef, effect, ViewChildren, ViewChild, Output, Input, ChangeDetectionStrategy, Component, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
5
5
  import { ActivatedRoute } from '@angular/router';
6
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
7
  import * as i2 from '@angular/material/expansion';
@@ -21,9 +21,9 @@ import { MatTooltipModule } from '@angular/material/tooltip';
21
21
  import { DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';
22
22
  import { deepMerge, ASYNC_CONFIG_STORAGE, DynamicFormService, ComponentKeyService, DynamicWidgetLoaderDirective, PraxisIconDirective, ComponentMetadataRegistry } from '@praxisui/core';
23
23
  import { SettingsPanelService, SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
24
- import { BehaviorSubject } from 'rxjs';
24
+ import { firstValueFrom, BehaviorSubject, Subscription } from 'rxjs';
25
25
  import { take } from 'rxjs/operators';
26
- import { BaseAiAdapter, PraxisAiAssistantComponent } from '@praxisui/ai';
26
+ import { BaseAiAdapter, AiBackendApiService, PraxisAssistantSessionRegistryService, PraxisAssistantTurnOrchestratorService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
27
27
 
28
28
  /**
29
29
  * Capabilities catalog for Praxis Expansion (ExpansionMetadata).
@@ -62,6 +62,7 @@ const CAPS = [
62
62
  { path: 'panels[].id', category: 'panels', valueKind: 'string', description: 'Panel id.' },
63
63
  { path: 'panels[].title', category: 'panels', valueKind: 'string', description: 'Panel title.' },
64
64
  { path: 'panels[].description', category: 'panels', valueKind: 'string', description: 'Panel description.' },
65
+ { path: 'panels[].icon', category: 'panels', valueKind: 'string', description: 'Panel header icon rendered through PraxisIconDirective.' },
65
66
  { path: 'panels[].disabled', category: 'panels', valueKind: 'boolean', description: 'Disable panel.' },
66
67
  { path: 'panels[].expanded', category: 'panels', valueKind: 'boolean', description: 'Expanded state.' },
67
68
  { path: 'panels[].hideToggle', category: 'panels', valueKind: 'boolean', description: 'Hide toggle icon for panel.' },
@@ -104,6 +105,8 @@ const EXPANSION_AI_CAPABILITIES = {
104
105
 
105
106
  class ExpansionAiAdapter extends BaseAiAdapter {
106
107
  expansion;
108
+ componentId = 'praxis-expansion';
109
+ componentType = 'expansion';
107
110
  componentName = 'Praxis Expansion';
108
111
  constructor(expansion) {
109
112
  super();
@@ -124,6 +127,43 @@ class ExpansionAiAdapter extends BaseAiAdapter {
124
127
  multi: this.expansion.config?.accordion?.multi ?? false,
125
128
  };
126
129
  }
130
+ getDataProfile() {
131
+ const panels = this.getCurrentConfig().panels || [];
132
+ return {
133
+ expansionId: this.expansion.expansionId,
134
+ panelCount: panels.length,
135
+ contentPanelCount: panels.filter((panel) => Array.isArray(panel.content) && panel.content.length > 0).length,
136
+ widgetPanelCount: panels.filter((panel) => Array.isArray(panel.widgets) && panel.widgets.length > 0).length,
137
+ actionPanelCount: panels.filter((panel) => Array.isArray(panel.actionButtons) && panel.actionButtons.length > 0).length,
138
+ multi: this.expansion.config?.accordion?.multi ?? false,
139
+ displayMode: this.expansion.config?.accordion?.displayMode || 'default',
140
+ };
141
+ }
142
+ getSchemaFields() {
143
+ return (this.getCurrentConfig().panels || []).map((panel, index) => ({
144
+ name: panel.id || panel.title || `panel-${index + 1}`,
145
+ label: panel.title || panel.id || `Painel ${index + 1}`,
146
+ disabled: !!panel.disabled,
147
+ expanded: !!panel.expanded,
148
+ hasContent: Array.isArray(panel.content) && panel.content.length > 0,
149
+ hasWidgets: Array.isArray(panel.widgets) && panel.widgets.length > 0,
150
+ hasActions: Array.isArray(panel.actionButtons) && panel.actionButtons.length > 0,
151
+ }));
152
+ }
153
+ getAuthoringContext() {
154
+ return {
155
+ authoringManifestRef: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
156
+ runtimeAuthoringPolicy: {
157
+ mode: 'agentic-authoring',
158
+ enableCustomization: !!this.expansion.enableCustomization,
159
+ canApplyLocalPatch: false,
160
+ reason: 'praxis-expansion exige componentEditPlan validado pelo manifesto antes de aplicar mudancas runtime.',
161
+ },
162
+ domainCatalog: {
163
+ recommendedAuthoringFlow: 'component_authoring',
164
+ },
165
+ };
166
+ }
127
167
  createSnapshot() {
128
168
  return this.getCurrentConfig();
129
169
  }
@@ -184,6 +224,280 @@ class ExpansionAiAdapter extends BaseAiAdapter {
184
224
  }
185
225
  }
186
226
 
227
+ class ExpansionAgenticAuthoringTurnFlow {
228
+ adapter;
229
+ aiApi;
230
+ mode = 'agentic-authoring';
231
+ constructor(adapter, aiApi) {
232
+ this.adapter = adapter;
233
+ this.aiApi = aiApi;
234
+ }
235
+ async submit(request) {
236
+ const prompt = (request.prompt ?? '').trim();
237
+ if (!prompt)
238
+ return { state: 'listening', phase: 'capture', statusText: '' };
239
+ const componentId = this.adapter.componentId || request.componentId || 'praxis-expansion';
240
+ const componentType = this.adapter.componentType || request.componentType || 'expansion';
241
+ const currentState = this.toAiJsonObject(this.adapter.getCurrentConfig());
242
+ const dataProfile = this.optionalJsonObject(this.adapter.getDataProfile?.());
243
+ const runtimeState = this.optionalJsonObject(this.adapter.getRuntimeState?.());
244
+ const schemaFields = this.adapter.getSchemaFields?.()
245
+ ?.map((field) => this.toAiJsonObject(field))
246
+ .filter((field) => Object.keys(field).length > 0);
247
+ const contextHints = this.optionalJsonObject(this.adapter.getAuthoringContext?.());
248
+ if (this.shouldRouteToGovernedDecision(prompt, contextHints)) {
249
+ return this.toGovernedDecisionHandoff(prompt, request);
250
+ }
251
+ const response = await firstValueFrom(this.aiApi.getPatch({
252
+ componentId,
253
+ componentType,
254
+ userPrompt: prompt,
255
+ sessionId: request.sessionId,
256
+ clientTurnId: request.clientTurnId,
257
+ messages: this.toChatMessages(request.messages, prompt),
258
+ currentState,
259
+ currentStateDigest: this.buildCurrentStateDigest(dataProfile),
260
+ uiContextRef: { componentId, componentType },
261
+ ...(dataProfile ? { dataProfile } : {}),
262
+ ...(runtimeState ? { runtimeState } : {}),
263
+ ...(schemaFields?.length ? { schemaFields } : {}),
264
+ ...(contextHints ? { contextHints } : {}),
265
+ }));
266
+ return this.toTurnResult(this.compileAdapterResponse(response), request);
267
+ }
268
+ async apply(_request) {
269
+ return {
270
+ state: 'error',
271
+ phase: 'apply',
272
+ assistantMessage: 'O expansion ainda exige componentEditPlan validado pelo manifesto antes de aplicar mudancas locais.',
273
+ errorText: 'Aplicacao local bloqueada ate existir compilacao manifest-backed para praxis-expansion.',
274
+ canApply: false,
275
+ pendingPatch: null,
276
+ };
277
+ }
278
+ cancel() {
279
+ return Promise.resolve({
280
+ state: 'listening',
281
+ phase: 'capture',
282
+ assistantMessage: 'Solicitacao cancelada.',
283
+ statusText: '',
284
+ canApply: false,
285
+ pendingPatch: null,
286
+ pendingClarification: null,
287
+ });
288
+ }
289
+ retry(request) {
290
+ const lastPrompt = [...(request.messages ?? [])].reverse()
291
+ .find((message) => message.role === 'user')?.text;
292
+ return this.submit({ ...request, prompt: lastPrompt ?? request.prompt, action: { kind: 'retry' } });
293
+ }
294
+ toTurnResult(response, request) {
295
+ if (!response) {
296
+ return { state: 'error', phase: 'capture', assistantMessage: 'Resposta vazia da IA.', errorText: 'Resposta vazia da IA.' };
297
+ }
298
+ if (response.type === 'clarification') {
299
+ return {
300
+ state: 'clarification',
301
+ phase: 'clarify',
302
+ sessionId: response.sessionId ?? request.sessionId,
303
+ assistantMessage: response.message || 'Preciso de mais detalhes para continuar.',
304
+ clarificationQuestions: this.toClarificationQuestions(response),
305
+ quickReplies: this.toQuickReplies(response),
306
+ canApply: false,
307
+ };
308
+ }
309
+ if (response.type === 'info') {
310
+ const message = response.message || response.explanation || 'Informacao gerada.';
311
+ return { state: 'success', phase: 'summarize', sessionId: response.sessionId ?? request.sessionId, assistantMessage: message, statusText: message, canApply: false };
312
+ }
313
+ if (response.type === 'error') {
314
+ const message = response.message || 'Falha ao gerar alteracao de expansion.';
315
+ return {
316
+ state: 'error',
317
+ phase: 'capture',
318
+ sessionId: response.sessionId ?? request.sessionId,
319
+ assistantMessage: message,
320
+ errorText: message,
321
+ diagnostics: response.warnings?.length ? { warnings: response.warnings } : undefined,
322
+ };
323
+ }
324
+ if (response.patch && Object.keys(response.patch).length > 0) {
325
+ return {
326
+ state: 'error',
327
+ phase: 'review',
328
+ sessionId: response.sessionId ?? request.sessionId,
329
+ assistantMessage: 'O expansion rejeitou patch livre. Gere um componentEditPlan validado pelo PRAXIS_EXPANSION_AUTHORING_MANIFEST antes de propor alteracao local.',
330
+ errorText: 'Patch livre de expansion rejeitado.',
331
+ canApply: false,
332
+ pendingPatch: null,
333
+ diagnostics: {
334
+ warnings: [
335
+ 'free-expansion-patch-rejected',
336
+ 'Use componentEditPlan validado contra PRAXIS_EXPANSION_AUTHORING_MANIFEST.',
337
+ ],
338
+ },
339
+ };
340
+ }
341
+ return {
342
+ state: 'success',
343
+ phase: 'summarize',
344
+ sessionId: response.sessionId ?? request.sessionId,
345
+ assistantMessage: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
346
+ statusText: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
347
+ canApply: false,
348
+ };
349
+ }
350
+ compileAdapterResponse(response) {
351
+ const compiled = this.adapter.compileAiResponse?.(response);
352
+ if (!compiled)
353
+ return response;
354
+ if (compiled.type === 'error') {
355
+ return {
356
+ type: 'error',
357
+ message: compiled.message || 'O componentEditPlan do expansion nao passou na validacao de capacidades.',
358
+ warnings: compiled.warnings,
359
+ };
360
+ }
361
+ const warnings = [...(response.warnings ?? []), ...(compiled.warnings ?? [])];
362
+ return { ...response, ...compiled, patch: compiled.patch, warnings: warnings.length ? warnings : undefined };
363
+ }
364
+ toChatMessages(messages, prompt) {
365
+ const supported = (messages ?? [])
366
+ .filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'system')
367
+ .map((message) => ({ role: message.role, content: message.text }))
368
+ .filter((message) => message.content.trim().length > 0);
369
+ return supported.length ? supported : [{ role: 'user', content: prompt }];
370
+ }
371
+ toClarificationQuestions(response) {
372
+ const labels = response.questions?.length
373
+ ? response.questions
374
+ : response.message ? [response.message] : ['Qual ajuste voce quer aplicar nos paineis?'];
375
+ const options = this.toQuickReplies(response).map((reply) => ({ id: reply.id, label: reply.label, value: reply.prompt }));
376
+ return labels.map((label, index) => ({
377
+ id: `expansion-clarification-${index + 1}`,
378
+ type: options.length ? 'single-choice' : 'text',
379
+ label,
380
+ allowCustom: true,
381
+ options,
382
+ }));
383
+ }
384
+ toQuickReplies(response) {
385
+ const payloads = response.optionPayloads ?? [];
386
+ if (payloads.length) {
387
+ return payloads.map((option, index) => {
388
+ const label = option.label?.trim() || option.value?.trim() || `Opcao ${index + 1}`;
389
+ const prompt = option.example?.trim() || option.value?.trim() || label;
390
+ return { id: `option-${index + 1}`, label, prompt, kind: 'clarification-option' };
391
+ });
392
+ }
393
+ return (response.options ?? [])
394
+ .filter((option) => !!option?.trim())
395
+ .map((option, index) => ({ id: `option-${index + 1}`, label: option.trim(), prompt: option.trim(), kind: 'clarification-option' }));
396
+ }
397
+ buildCurrentStateDigest(dataProfile) {
398
+ const panelCount = typeof dataProfile?.['panelCount'] === 'number' ? dataProfile['panelCount'] : undefined;
399
+ return panelCount !== undefined ? { rowCount: panelCount } : {};
400
+ }
401
+ shouldRouteToGovernedDecision(prompt, contextHints) {
402
+ const normalized = prompt.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
403
+ const recommendedFlow = this.toRecord(contextHints?.['domainCatalog'])?.['recommendedAuthoringFlow'];
404
+ if (recommendedFlow === 'shared_rule_authoring')
405
+ return true;
406
+ return [
407
+ 'regra',
408
+ 'politica',
409
+ 'policy',
410
+ 'compliance',
411
+ 'lgpd',
412
+ 'privacidade',
413
+ 'aprovacao',
414
+ 'aprovar',
415
+ 'publicar',
416
+ 'materializar',
417
+ 'enforcement',
418
+ 'validacao de negocio',
419
+ 'validar negocio',
420
+ 'elegibilidade',
421
+ 'permissao',
422
+ 'acesso',
423
+ ].some((term) => normalized.includes(term));
424
+ }
425
+ toGovernedDecisionHandoff(prompt, request) {
426
+ const message = 'Esse pedido parece alterar uma decisao de negocio compartilhada. O expansion pode localizar o painel e a experiencia afetada, mas a regra deve seguir pelo fluxo governado de domain-rules antes de qualquer materializacao runtime.';
427
+ return {
428
+ state: 'clarification',
429
+ phase: 'clarify',
430
+ sessionId: request.sessionId,
431
+ assistantMessage: message,
432
+ statusText: 'Handoff governado necessario.',
433
+ canApply: false,
434
+ quickReplies: [
435
+ {
436
+ id: 'shared-rule-handoff',
437
+ label: 'Continuar como regra governada',
438
+ prompt,
439
+ kind: 'shared-rule-handoff',
440
+ description: 'Criar intake de domain-rules em vez de aplicar patch local no expansion.',
441
+ icon: 'rule',
442
+ tone: 'warning',
443
+ contextHints: {
444
+ flowId: 'shared_rule_authoring',
445
+ source: 'praxis-expansion',
446
+ recommendedAction: 'domain-rules/intake',
447
+ },
448
+ },
449
+ ],
450
+ clarificationQuestions: [
451
+ {
452
+ id: 'expansion-governed-rule-confirmation',
453
+ type: 'confirm',
454
+ label: 'Deseja continuar pelo fluxo governado de regras compartilhadas?',
455
+ description: 'Esse caminho permite intake, simulacao, aprovacao/publicacao, materializacao e validacao de enforcement.',
456
+ required: true,
457
+ options: [
458
+ {
459
+ id: 'shared-rule-handoff',
460
+ label: 'Sim, continuar governado',
461
+ value: prompt,
462
+ description: 'Nao aplicar como patch local do expansion.',
463
+ contextHints: { flowId: 'shared_rule_authoring', source: 'praxis-expansion' },
464
+ },
465
+ ],
466
+ },
467
+ ],
468
+ diagnostics: {
469
+ governedDecisionHandoff: {
470
+ flowId: 'shared_rule_authoring',
471
+ sourcePrompt: prompt,
472
+ sourceComponent: 'praxis-expansion',
473
+ },
474
+ },
475
+ };
476
+ }
477
+ optionalJsonObject(value) {
478
+ if (value === undefined || value === null)
479
+ return undefined;
480
+ const object = this.toAiJsonObject(value);
481
+ return Object.keys(object).length ? object : undefined;
482
+ }
483
+ toAiJsonObject(value) {
484
+ const record = this.toRecord(value);
485
+ if (!record)
486
+ return {};
487
+ try {
488
+ return JSON.parse(JSON.stringify(record));
489
+ }
490
+ catch {
491
+ return {};
492
+ }
493
+ }
494
+ toRecord(value) {
495
+ return value && typeof value === 'object' && !Array.isArray(value)
496
+ ? value
497
+ : null;
498
+ }
499
+ }
500
+
187
501
  class PraxisExpansion {
188
502
  config;
189
503
  expansionId;
@@ -211,9 +525,35 @@ class PraxisExpansion {
211
525
  catch {
212
526
  return undefined;
213
527
  } })();
528
+ aiApi = inject(AiBackendApiService);
529
+ assistantSessions = inject(PraxisAssistantSessionRegistryService);
530
+ aiTurnOrchestrator = inject(PraxisAssistantTurnOrchestratorService);
531
+ aiAssistantSessionEffect = effect(() => {
532
+ const session = this.assistantSessions.activeSession();
533
+ if (!session || session.id !== this.resolveAiAssistantSessionId())
534
+ return;
535
+ if (!this.aiAssistantOpen) {
536
+ this.openAiAssistantFromSession(session);
537
+ }
538
+ }, ...(ngDevMode ? [{ debugName: "aiAssistantSessionEffect" }] : []));
214
539
  warnedMissingId = false;
215
540
  panelForms = new Map();
216
541
  aiAdapter = new ExpansionAiAdapter(this);
542
+ aiAssistantOpen = false;
543
+ aiAssistantPrompt = '';
544
+ aiAssistantViewState = null;
545
+ aiAssistantLayout = createPraxisAssistantViewportLayout();
546
+ aiAssistantLabels = {
547
+ title: 'Copiloto semantico Praxis',
548
+ subtitle: 'Converse, revise e governe ajustes dos paineis.',
549
+ prompt: 'Mensagem',
550
+ promptPlaceholder: 'Descreva o ajuste que voce precisa nos paineis.',
551
+ emptyConversation: 'Diga o que voce quer alterar no expansion.',
552
+ submit: 'Interpretar pedido',
553
+ apply: 'Aplicar ajuste',
554
+ };
555
+ aiAssistantController = null;
556
+ aiAssistantStateSubscription = null;
217
557
  accordionRef;
218
558
  panels;
219
559
  injectedDefaults = inject(MAT_EXPANSION_PANEL_DEFAULT_OPTIONS, { optional: true });
@@ -237,6 +577,10 @@ class PraxisExpansion {
237
577
  this.persistConfig(this.config);
238
578
  }
239
579
  }
580
+ ngOnDestroy() {
581
+ this.assistantSessions.removeContextSession(this.buildAiAssistantContextSnapshot().identity);
582
+ this.aiAssistantStateSubscription?.unsubscribe();
583
+ }
240
584
  styleCss() {
241
585
  const t = this.config?.appearance?.tokens;
242
586
  const appearance = this.config?.appearance;
@@ -476,6 +820,306 @@ class PraxisExpansion {
476
820
  }
477
821
  this.cdr.markForCheck();
478
822
  }
823
+ openAiAssistant() {
824
+ this.initializeAiAssistantController();
825
+ this.aiAssistantOpen = true;
826
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
827
+ this.syncAiAssistantSession('active');
828
+ this.cdr.markForCheck();
829
+ }
830
+ openAiAssistantFromSession(session) {
831
+ if (session.id !== this.resolveAiAssistantSessionId())
832
+ return;
833
+ this.initializeAiAssistantController();
834
+ this.aiAssistantOpen = true;
835
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
836
+ this.syncAiAssistantSession('active');
837
+ this.cdr.markForCheck();
838
+ }
839
+ closeAiAssistant() {
840
+ this.aiAssistantOpen = false;
841
+ this.syncAiAssistantSession('minimized');
842
+ this.cdr.markForCheck();
843
+ }
844
+ onAiAssistantPromptChange(prompt) {
845
+ this.aiAssistantPrompt = prompt;
846
+ this.syncAiAssistantSession();
847
+ }
848
+ onAiAssistantSubmit(prompt) {
849
+ this.aiAssistantController?.submitPrompt(prompt).subscribe((state) => {
850
+ this.aiAssistantPrompt = '';
851
+ this.aiAssistantViewState = state;
852
+ this.syncAiAssistantSession();
853
+ this.cdr.markForCheck();
854
+ });
855
+ }
856
+ onAiAssistantApply() {
857
+ this.aiAssistantController?.apply().subscribe((state) => {
858
+ this.aiAssistantViewState = state;
859
+ this.syncAiAssistantSession();
860
+ this.cdr.markForCheck();
861
+ });
862
+ }
863
+ onAiAssistantRetry() {
864
+ this.aiAssistantController?.retry().subscribe((state) => {
865
+ this.aiAssistantViewState = state;
866
+ this.syncAiAssistantSession();
867
+ this.cdr.markForCheck();
868
+ });
869
+ }
870
+ onAiAssistantCancel() {
871
+ this.aiAssistantController?.cancel().subscribe((state) => {
872
+ this.aiAssistantPrompt = '';
873
+ this.aiAssistantViewState = state;
874
+ this.syncAiAssistantSession();
875
+ this.cdr.markForCheck();
876
+ });
877
+ }
878
+ onAiAssistantQuickReply(reply) {
879
+ const controller = this.aiAssistantController;
880
+ if (!controller)
881
+ return;
882
+ const state = controller.snapshot();
883
+ const next$ = state.state === 'clarification'
884
+ ? controller.answerClarification(reply.prompt)
885
+ : controller.submitPrompt(reply.prompt, {
886
+ kind: reply.kind || 'quick-reply',
887
+ id: reply.id,
888
+ value: reply.prompt,
889
+ });
890
+ next$.subscribe((nextState) => {
891
+ this.aiAssistantPrompt = '';
892
+ this.aiAssistantViewState = nextState;
893
+ this.syncAiAssistantSession();
894
+ this.cdr.markForCheck();
895
+ });
896
+ }
897
+ onAiAssistantEditMessage(message) {
898
+ this.aiAssistantPrompt = message.text;
899
+ this.cdr.markForCheck();
900
+ }
901
+ onAiAssistantResendMessage(message) {
902
+ this.aiAssistantController?.resendMessage(message.id).subscribe((state) => {
903
+ this.aiAssistantPrompt = '';
904
+ this.aiAssistantViewState = state;
905
+ this.syncAiAssistantSession();
906
+ this.cdr.markForCheck();
907
+ });
908
+ }
909
+ onAiAssistantLayoutChange(layout) {
910
+ this.aiAssistantLayout = layout;
911
+ }
912
+ buildAiAssistantContextItems() {
913
+ const panels = this.config?.panels ?? [];
914
+ const items = [
915
+ {
916
+ id: 'component',
917
+ label: 'Componente',
918
+ value: 'Expansion',
919
+ kind: 'component',
920
+ icon: 'unfold_more',
921
+ },
922
+ {
923
+ id: 'expansion-id',
924
+ label: 'Expansion',
925
+ value: this.safeAiAssistantExpansionId(),
926
+ kind: 'custom',
927
+ icon: 'tag',
928
+ },
929
+ {
930
+ id: 'panels',
931
+ label: 'Paineis',
932
+ value: String(panels.length),
933
+ kind: 'custom',
934
+ icon: 'view_agenda',
935
+ },
936
+ ];
937
+ const expanded = panels.filter((panel) => panel.expanded).length;
938
+ if (expanded > 0) {
939
+ items.push({
940
+ id: 'expanded-panels',
941
+ label: 'Expandidos',
942
+ value: String(expanded),
943
+ kind: 'custom',
944
+ icon: 'expand_less',
945
+ });
946
+ }
947
+ return items;
948
+ }
949
+ initializeAiAssistantController() {
950
+ if (this.aiAssistantController)
951
+ return;
952
+ const flow = new ExpansionAgenticAuthoringTurnFlow(this.aiAdapter, this.aiApi);
953
+ const controller = this.aiTurnOrchestrator.createController(flow, {
954
+ componentId: this.aiAdapter.componentId || 'praxis-expansion',
955
+ componentType: this.aiAdapter.componentType || 'expansion',
956
+ contextItems: this.buildAiAssistantContextItems(),
957
+ });
958
+ this.aiAssistantController = controller;
959
+ this.aiAssistantViewState = controller.snapshot();
960
+ this.aiAssistantStateSubscription?.unsubscribe();
961
+ this.aiAssistantStateSubscription = controller.state$.subscribe((state) => {
962
+ this.aiAssistantViewState = state;
963
+ this.syncAiAssistantSession();
964
+ this.cdr.markForCheck();
965
+ });
966
+ this.cdr.markForCheck();
967
+ }
968
+ buildAiAssistantContextSnapshot() {
969
+ const counts = this.collectAiAssistantCounts();
970
+ const panelNames = this.collectAiAssistantPanelNames();
971
+ return {
972
+ identity: {
973
+ sessionId: this.resolveAiAssistantSessionId(),
974
+ ownerId: this.resolveAiAssistantOwnerId(),
975
+ ownerType: 'expansion',
976
+ componentId: 'praxis-expansion',
977
+ componentType: 'expansion',
978
+ routeKey: this.resolveAiAssistantRouteKey(),
979
+ },
980
+ target: {
981
+ kind: 'component',
982
+ id: this.resolveAiAssistantOwnerId(),
983
+ label: this.safeAiAssistantExpansionId(),
984
+ metadata: {
985
+ expansionId: this.safeAiAssistantExpansionId(),
986
+ hasCustomization: !!this.enableCustomization,
987
+ },
988
+ },
989
+ contextItems: this.buildAiAssistantContextItems().map((item) => ({
990
+ id: item.id,
991
+ label: item.label,
992
+ value: item.value || '',
993
+ kind: item.kind,
994
+ })),
995
+ mode: 'agentic-authoring',
996
+ authoringManifestRef: {
997
+ componentId: 'praxis-expansion',
998
+ source: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
999
+ },
1000
+ schemaFields: panelNames.length ? panelNames : undefined,
1001
+ dataProfileDigest: {
1002
+ summary: `${counts.panelCount} painel(is), ${counts.contentPanelCount} com campo(s), ${counts.widgetPanelCount} com widget(s)`,
1003
+ counts,
1004
+ },
1005
+ runtimeStateDigest: {
1006
+ summary: `Expansion ${this.config?.accordion?.multi ? 'multi' : 'single'}, ${counts.expandedCount} painel(is) expandido(s)`,
1007
+ fields: [
1008
+ 'panels',
1009
+ 'accordion',
1010
+ 'content',
1011
+ 'widgets',
1012
+ ],
1013
+ },
1014
+ capabilityRefs: [
1015
+ {
1016
+ id: 'expansion.component-edit-plan',
1017
+ label: 'Plano de edicao de paineis',
1018
+ source: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
1019
+ risk: 'medium',
1020
+ },
1021
+ ],
1022
+ governanceHints: [
1023
+ {
1024
+ kind: 'business-rule-boundary',
1025
+ label: 'Regras compartilhadas exigem governanca',
1026
+ reason: 'Politicas de acesso, validacoes reutilizaveis e compliance nao devem ser aplicadas como patch local do expansion.',
1027
+ risk: 'high',
1028
+ },
1029
+ ],
1030
+ };
1031
+ }
1032
+ syncAiAssistantSession(visibility = null) {
1033
+ if (!this.enableCustomization)
1034
+ return;
1035
+ if (!this.aiAssistantOpen && !this.hasAiAssistantSessionState() && visibility !== 'minimized')
1036
+ return;
1037
+ const state = this.aiAssistantViewState;
1038
+ this.assistantSessions.upsertContextSession(this.buildAiAssistantContextSnapshot(), {
1039
+ title: 'Copiloto semantico Praxis',
1040
+ summary: this.resolveAiAssistantSummary(),
1041
+ mode: state?.mode || 'agentic-authoring',
1042
+ state: state?.state || 'idle',
1043
+ visibility: visibility ?? (this.aiAssistantOpen ? 'active' : 'minimized'),
1044
+ badge: this.resolveAiAssistantBadge(),
1045
+ icon: this.resolveAiAssistantIcon(),
1046
+ });
1047
+ }
1048
+ hasAiAssistantSessionState() {
1049
+ return !!this.aiAssistantPrompt.trim()
1050
+ || !!this.aiAssistantViewState?.messages?.length
1051
+ || !!this.aiAssistantViewState?.quickReplies?.length
1052
+ || !!this.aiAssistantViewState?.pendingPatch
1053
+ || !!this.aiAssistantViewState?.statusText?.trim()
1054
+ || !!this.aiAssistantViewState?.errorText?.trim();
1055
+ }
1056
+ resolveAiAssistantSessionId() {
1057
+ return `expansion:${this.resolveAiAssistantRouteKey()}:${this.resolveAiAssistantOwnerId()}`;
1058
+ }
1059
+ resolveAiAssistantOwnerId() {
1060
+ return (this.componentInstanceId || this.safeAiAssistantExpansionId() || 'expansion').trim() || 'expansion';
1061
+ }
1062
+ safeAiAssistantExpansionId() {
1063
+ return String(this.expansionId || '').trim();
1064
+ }
1065
+ resolveAiAssistantRouteKey() {
1066
+ const routePath = this.route?.snapshot?.routeConfig?.path?.trim();
1067
+ return routePath || 'local';
1068
+ }
1069
+ resolveAiAssistantSummary() {
1070
+ const status = this.aiAssistantViewState?.statusText?.trim();
1071
+ if (status)
1072
+ return status;
1073
+ const error = this.aiAssistantViewState?.errorText?.trim();
1074
+ if (error)
1075
+ return error;
1076
+ const prompt = this.aiAssistantPrompt.trim();
1077
+ if (prompt)
1078
+ return prompt.length > 96 ? `${prompt.slice(0, 93)}...` : prompt;
1079
+ const lastMessage = [...(this.aiAssistantViewState?.messages ?? [])].reverse()
1080
+ .find((message) => message.role === 'assistant' || message.role === 'user');
1081
+ if (lastMessage?.text) {
1082
+ return lastMessage.text.length > 96 ? `${lastMessage.text.slice(0, 93)}...` : lastMessage.text;
1083
+ }
1084
+ return 'Assistente contextual dos paineis.';
1085
+ }
1086
+ resolveAiAssistantBadge() {
1087
+ const state = this.aiAssistantViewState?.state;
1088
+ if (state === 'error')
1089
+ return 'erro';
1090
+ if (state === 'clarification')
1091
+ return 'revisar';
1092
+ if (state === 'review')
1093
+ return 'preview';
1094
+ if (state === 'success')
1095
+ return 'ok';
1096
+ return undefined;
1097
+ }
1098
+ resolveAiAssistantIcon() {
1099
+ const state = this.aiAssistantViewState?.state;
1100
+ if (state === 'error')
1101
+ return 'error';
1102
+ if (state === 'clarification')
1103
+ return 'rule';
1104
+ if (state === 'review')
1105
+ return 'rate_review';
1106
+ return 'auto_awesome';
1107
+ }
1108
+ collectAiAssistantPanelNames() {
1109
+ return Array.from(new Set((this.config?.panels ?? [])
1110
+ .map((panel, index) => panel.id || panel.title || `panel-${index + 1}`)
1111
+ .filter((name) => typeof name === 'string' && !!name.trim())));
1112
+ }
1113
+ collectAiAssistantCounts() {
1114
+ const panels = this.config?.panels ?? [];
1115
+ return {
1116
+ panelCount: panels.length,
1117
+ contentPanelCount: panels.filter((panel) => Array.isArray(panel.content) && panel.content.length > 0).length,
1118
+ widgetPanelCount: panels.filter((panel) => Array.isArray(panel.widgets) && panel.widgets.length > 0).length,
1119
+ actionPanelCount: panels.filter((panel) => Array.isArray(panel.actionButtons) && panel.actionButtons.length > 0).length,
1120
+ expandedCount: panels.filter((panel) => !!panel.expanded).length,
1121
+ };
1122
+ }
479
1123
  openEditor() {
480
1124
  const key = this.storageKey() || this.expansionId || 'default';
481
1125
  const ref = this.settings.open({
@@ -573,10 +1217,48 @@ class PraxisExpansion {
573
1217
 
574
1218
  @if (enableCustomization) {
575
1219
  <div class="expansion-ai-assistant">
576
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
1220
+ <button
1221
+ mat-mini-fab
1222
+ color="primary"
1223
+ type="button"
1224
+ class="expansion-ai-assistant-trigger"
1225
+ aria-label="Abrir copiloto semantico Praxis dos paineis"
1226
+ data-testid="praxis-expansion-ai-assistant-trigger"
1227
+ (click)="openAiAssistant()"
1228
+ >
1229
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
1230
+ </button>
577
1231
  </div>
578
1232
  }
579
1233
 
1234
+ <praxis-ai-assistant-shell
1235
+ *ngIf="aiAssistantOpen && aiAssistantViewState"
1236
+ [labels]="aiAssistantLabels"
1237
+ [mode]="aiAssistantViewState.mode"
1238
+ [state]="aiAssistantViewState.state"
1239
+ [contextItems]="aiAssistantViewState.contextItems"
1240
+ [attachments]="aiAssistantViewState.attachments"
1241
+ [messages]="aiAssistantViewState.messages"
1242
+ [quickReplies]="aiAssistantViewState.quickReplies"
1243
+ [prompt]="aiAssistantPrompt"
1244
+ [statusText]="aiAssistantViewState.statusText"
1245
+ [errorText]="aiAssistantViewState.errorText"
1246
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
1247
+ [canApply]="aiAssistantViewState.canApply"
1248
+ [layout]="aiAssistantLayout"
1249
+ testIdPrefix="praxis-expansion-ai-assistant"
1250
+ (promptChange)="onAiAssistantPromptChange($event)"
1251
+ (submitPrompt)="onAiAssistantSubmit($event)"
1252
+ (apply)="onAiAssistantApply()"
1253
+ (retryTurn)="onAiAssistantRetry()"
1254
+ (cancelTurn)="onAiAssistantCancel()"
1255
+ (quickReply)="onAiAssistantQuickReply($event)"
1256
+ (editMessage)="onAiAssistantEditMessage($event)"
1257
+ (resendMessage)="onAiAssistantResendMessage($event)"
1258
+ (layoutChange)="onAiAssistantLayoutChange($event)"
1259
+ (close)="closeAiAssistant()"
1260
+ ></praxis-ai-assistant-shell>
1261
+
580
1262
  @if (hasMultiple()) {
581
1263
  <mat-accordion
582
1264
  #accordion
@@ -603,8 +1285,9 @@ class PraxisExpansion {
603
1285
  >
604
1286
  <mat-expansion-panel-header
605
1287
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
606
- [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
607
- >
1288
+ [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
1289
+ >
1290
+ <mat-icon *ngIf="p.icon" [praxisIcon]="p.icon"></mat-icon>
608
1291
  <mat-panel-title>{{ p.title }}</mat-panel-title>
609
1292
  <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
610
1293
  </mat-expansion-panel-header>
@@ -660,8 +1343,9 @@ class PraxisExpansion {
660
1343
  >
661
1344
  <mat-expansion-panel-header
662
1345
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
663
- [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
664
- >
1346
+ [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
1347
+ >
1348
+ <mat-icon *ngIf="p.icon" [praxisIcon]="p.icon"></mat-icon>
665
1349
  <mat-panel-title>{{ p.title }}</mat-panel-title>
666
1350
  <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
667
1351
  </mat-expansion-panel-header>
@@ -718,7 +1402,7 @@ class PraxisExpansion {
718
1402
  >
719
1403
  <mat-icon fontIcon="edit"></mat-icon>
720
1404
  </button>
721
- `, isInline: true, styles: [":host{display:block;position:relative;color:var(--md-sys-color-on-surface)}.praxis-expansion-root{display:block;--p-exp-surface: var(--md-sys-color-surface);--p-exp-surface-container: var(--md-sys-color-surface-container);--p-exp-border: var(--md-sys-color-outline-variant);--p-exp-text: var(--md-sys-color-on-surface);--p-exp-text-muted: var(--md-sys-color-on-surface-variant);--p-exp-focus: var(--md-sys-color-primary);--p-exp-radius: 12px}.mat-expansion-panel{background:var(--p-exp-surface);border:1px solid var(--p-exp-border);border-radius:var(--p-exp-radius);overflow:hidden}.mat-expansion-panel:not(:last-child){margin-bottom:var(--p-exp-panel-gap, 12px)}.mat-expansion-panel-header{background:var(--p-exp-surface-container);color:var(--p-exp-text)}.mat-expansion-panel-header:focus-visible{outline:2px solid var(--p-exp-focus);outline-offset:-2px}.mat-expansion-panel-header-title{font-weight:600}.mat-expansion-panel-header-description{color:var(--p-exp-text-muted)}.mat-expansion-panel-body{padding:12px 16px 16px}.mat-action-row{border-top:1px solid var(--p-exp-border)}.density-compact .mat-expansion-panel-body{padding:8px 12px 12px}.density-compact .mat-expansion-panel-header{min-height:40px;padding:0 12px}.density-comfortable .mat-expansion-panel-body{padding:12px 16px 16px}.density-comfortable .mat-expansion-panel-header{min-height:48px;padding:0 16px}.density-spacious .mat-expansion-panel-body{padding:16px 20px 20px}.density-spacious .mat-expansion-panel-header{min-height:56px;padding:0 20px}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px 12px;color:var(--p-exp-text-muted)}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.expansion-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i2.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "directive", type: i2.MatExpansionPanelActionRow, selector: "mat-action-row" }, { kind: "component", type: i2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i2.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i2.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "directive", type: i2.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { 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: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisAiAssistantComponent, selector: "praxis-ai-assistant", inputs: ["adapter", "riskPolicy", "allowManualPatchEdit"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1405
+ `, isInline: true, styles: [":host{display:block;position:relative;color:var(--md-sys-color-on-surface)}.praxis-expansion-root{display:block;--p-exp-surface: var(--md-sys-color-surface);--p-exp-surface-container: var(--md-sys-color-surface-container);--p-exp-border: var(--md-sys-color-outline-variant);--p-exp-text: var(--md-sys-color-on-surface);--p-exp-text-muted: var(--md-sys-color-on-surface-variant);--p-exp-focus: var(--md-sys-color-primary);--p-exp-radius: 12px}.mat-expansion-panel{background:var(--p-exp-surface);border:1px solid var(--p-exp-border);border-radius:var(--p-exp-radius);overflow:hidden}.mat-expansion-panel:not(:last-child){margin-bottom:var(--p-exp-panel-gap, 12px)}.mat-expansion-panel-header{background:var(--p-exp-surface-container);color:var(--p-exp-text)}.mat-expansion-panel-header:focus-visible{outline:2px solid var(--p-exp-focus);outline-offset:-2px}.mat-expansion-panel-header-title{font-weight:600}.mat-expansion-panel-header-description{color:var(--p-exp-text-muted)}.mat-expansion-panel-body{padding:12px 16px 16px}.mat-action-row{border-top:1px solid var(--p-exp-border)}.density-compact .mat-expansion-panel-body{padding:8px 12px 12px}.density-compact .mat-expansion-panel-header{min-height:40px;padding:0 12px}.density-comfortable .mat-expansion-panel-body{padding:12px 16px 16px}.density-comfortable .mat-expansion-panel-header{min-height:48px;padding:0 16px}.density-spacious .mat-expansion-panel-body{padding:16px 20px 20px}.density-spacious .mat-expansion-panel-header{min-height:56px;padding:0 20px}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px 12px;color:var(--p-exp-text-muted)}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.expansion-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}.expansion-ai-assistant-trigger{box-shadow:var(--md-sys-elevation-level2, 0 4px 12px rgba(0, 0, 0, .18))}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i2.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "directive", type: i2.MatExpansionPanelActionRow, selector: "mat-action-row" }, { kind: "component", type: i2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i2.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i2.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "directive", type: i2.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatMiniFabButton, selector: "button[mat-mini-fab], a[mat-mini-fab], button[matMiniFab], a[matMiniFab]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatFabButton, selector: "button[mat-fab], a[mat-fab], button[matFab], a[matFab]", inputs: ["extended"], exportAs: ["matButton", "matAnchor"] }, { 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: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { 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 });
722
1406
  }
723
1407
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansion, decorators: [{
724
1408
  type: Component,
@@ -730,7 +1414,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
730
1414
  DynamicFieldLoaderDirective,
731
1415
  DynamicWidgetLoaderDirective,
732
1416
  PraxisIconDirective,
733
- PraxisAiAssistantComponent,
1417
+ PraxisAiAssistantShellComponent,
734
1418
  ], template: `
735
1419
  <div
736
1420
  class="praxis-expansion-root"
@@ -745,10 +1429,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
745
1429
 
746
1430
  @if (enableCustomization) {
747
1431
  <div class="expansion-ai-assistant">
748
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
1432
+ <button
1433
+ mat-mini-fab
1434
+ color="primary"
1435
+ type="button"
1436
+ class="expansion-ai-assistant-trigger"
1437
+ aria-label="Abrir copiloto semantico Praxis dos paineis"
1438
+ data-testid="praxis-expansion-ai-assistant-trigger"
1439
+ (click)="openAiAssistant()"
1440
+ >
1441
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
1442
+ </button>
749
1443
  </div>
750
1444
  }
751
1445
 
1446
+ <praxis-ai-assistant-shell
1447
+ *ngIf="aiAssistantOpen && aiAssistantViewState"
1448
+ [labels]="aiAssistantLabels"
1449
+ [mode]="aiAssistantViewState.mode"
1450
+ [state]="aiAssistantViewState.state"
1451
+ [contextItems]="aiAssistantViewState.contextItems"
1452
+ [attachments]="aiAssistantViewState.attachments"
1453
+ [messages]="aiAssistantViewState.messages"
1454
+ [quickReplies]="aiAssistantViewState.quickReplies"
1455
+ [prompt]="aiAssistantPrompt"
1456
+ [statusText]="aiAssistantViewState.statusText"
1457
+ [errorText]="aiAssistantViewState.errorText"
1458
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
1459
+ [canApply]="aiAssistantViewState.canApply"
1460
+ [layout]="aiAssistantLayout"
1461
+ testIdPrefix="praxis-expansion-ai-assistant"
1462
+ (promptChange)="onAiAssistantPromptChange($event)"
1463
+ (submitPrompt)="onAiAssistantSubmit($event)"
1464
+ (apply)="onAiAssistantApply()"
1465
+ (retryTurn)="onAiAssistantRetry()"
1466
+ (cancelTurn)="onAiAssistantCancel()"
1467
+ (quickReply)="onAiAssistantQuickReply($event)"
1468
+ (editMessage)="onAiAssistantEditMessage($event)"
1469
+ (resendMessage)="onAiAssistantResendMessage($event)"
1470
+ (layoutChange)="onAiAssistantLayoutChange($event)"
1471
+ (close)="closeAiAssistant()"
1472
+ ></praxis-ai-assistant-shell>
1473
+
752
1474
  @if (hasMultiple()) {
753
1475
  <mat-accordion
754
1476
  #accordion
@@ -775,8 +1497,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
775
1497
  >
776
1498
  <mat-expansion-panel-header
777
1499
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
778
- [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
779
- >
1500
+ [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
1501
+ >
1502
+ <mat-icon *ngIf="p.icon" [praxisIcon]="p.icon"></mat-icon>
780
1503
  <mat-panel-title>{{ p.title }}</mat-panel-title>
781
1504
  <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
782
1505
  </mat-expansion-panel-header>
@@ -832,8 +1555,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
832
1555
  >
833
1556
  <mat-expansion-panel-header
834
1557
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
835
- [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
836
- >
1558
+ [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
1559
+ >
1560
+ <mat-icon *ngIf="p.icon" [praxisIcon]="p.icon"></mat-icon>
837
1561
  <mat-panel-title>{{ p.title }}</mat-panel-title>
838
1562
  <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
839
1563
  </mat-expansion-panel-header>
@@ -890,7 +1614,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
890
1614
  >
891
1615
  <mat-icon fontIcon="edit"></mat-icon>
892
1616
  </button>
893
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;color:var(--md-sys-color-on-surface)}.praxis-expansion-root{display:block;--p-exp-surface: var(--md-sys-color-surface);--p-exp-surface-container: var(--md-sys-color-surface-container);--p-exp-border: var(--md-sys-color-outline-variant);--p-exp-text: var(--md-sys-color-on-surface);--p-exp-text-muted: var(--md-sys-color-on-surface-variant);--p-exp-focus: var(--md-sys-color-primary);--p-exp-radius: 12px}.mat-expansion-panel{background:var(--p-exp-surface);border:1px solid var(--p-exp-border);border-radius:var(--p-exp-radius);overflow:hidden}.mat-expansion-panel:not(:last-child){margin-bottom:var(--p-exp-panel-gap, 12px)}.mat-expansion-panel-header{background:var(--p-exp-surface-container);color:var(--p-exp-text)}.mat-expansion-panel-header:focus-visible{outline:2px solid var(--p-exp-focus);outline-offset:-2px}.mat-expansion-panel-header-title{font-weight:600}.mat-expansion-panel-header-description{color:var(--p-exp-text-muted)}.mat-expansion-panel-body{padding:12px 16px 16px}.mat-action-row{border-top:1px solid var(--p-exp-border)}.density-compact .mat-expansion-panel-body{padding:8px 12px 12px}.density-compact .mat-expansion-panel-header{min-height:40px;padding:0 12px}.density-comfortable .mat-expansion-panel-body{padding:12px 16px 16px}.density-comfortable .mat-expansion-panel-header{min-height:48px;padding:0 16px}.density-spacious .mat-expansion-panel-body{padding:16px 20px 20px}.density-spacious .mat-expansion-panel-header{min-height:56px;padding:0 20px}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px 12px;color:var(--p-exp-text-muted)}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.expansion-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}\n"] }]
1617
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative;color:var(--md-sys-color-on-surface)}.praxis-expansion-root{display:block;--p-exp-surface: var(--md-sys-color-surface);--p-exp-surface-container: var(--md-sys-color-surface-container);--p-exp-border: var(--md-sys-color-outline-variant);--p-exp-text: var(--md-sys-color-on-surface);--p-exp-text-muted: var(--md-sys-color-on-surface-variant);--p-exp-focus: var(--md-sys-color-primary);--p-exp-radius: 12px}.mat-expansion-panel{background:var(--p-exp-surface);border:1px solid var(--p-exp-border);border-radius:var(--p-exp-radius);overflow:hidden}.mat-expansion-panel:not(:last-child){margin-bottom:var(--p-exp-panel-gap, 12px)}.mat-expansion-panel-header{background:var(--p-exp-surface-container);color:var(--p-exp-text)}.mat-expansion-panel-header:focus-visible{outline:2px solid var(--p-exp-focus);outline-offset:-2px}.mat-expansion-panel-header-title{font-weight:600}.mat-expansion-panel-header-description{color:var(--p-exp-text-muted)}.mat-expansion-panel-body{padding:12px 16px 16px}.mat-action-row{border-top:1px solid var(--p-exp-border)}.density-compact .mat-expansion-panel-body{padding:8px 12px 12px}.density-compact .mat-expansion-panel-header{min-height:40px;padding:0 12px}.density-comfortable .mat-expansion-panel-body{padding:12px 16px 16px}.density-comfortable .mat-expansion-panel-header{min-height:48px;padding:0 16px}.density-spacious .mat-expansion-panel-body{padding:16px 20px 20px}.density-spacious .mat-expansion-panel-header{min-height:56px;padding:0 20px}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px 12px;color:var(--p-exp-text-muted)}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}.expansion-ai-assistant{position:absolute;right:12px;bottom:72px;z-index:3}.expansion-ai-assistant-trigger{box-shadow:var(--md-sys-elevation-level2, 0 4px 12px rgba(0, 0, 0, .18))}\n"] }]
894
1618
  }], propDecorators: { config: [{
895
1619
  type: Input
896
1620
  }], expansionId: [{
@@ -1099,7 +1823,7 @@ class PraxisExpansionConfigEditor {
1099
1823
  }
1100
1824
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansionConfigEditor, deps: [{ token: SETTINGS_PANEL_DATA }], target: i0.ɵɵFactoryTarget.Component });
1101
1825
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisExpansionConfigEditor, isStandalone: true, selector: "praxis-expansion-config-editor", inputs: { config: "config", expansionId: "expansionId" }, usesOnChanges: true, ngImport: i0, template: `
1102
- <div class="pdx-expansion-editor">
1826
+ <div class="pdx-expansion-editor" data-testid="expansion-config-editor">
1103
1827
  <section class="sec">
1104
1828
  <h4>Aparência</h4>
1105
1829
  <div class="grid two">
@@ -1321,6 +2045,9 @@ class PraxisExpansionConfigEditor {
1321
2045
  <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
1322
2046
  <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
1323
2047
  </mat-form-field>
2048
+ <mat-form-field appearance="outline"><mat-label>Ícone</mat-label>
2049
+ <input matInput [value]="p.icon || ''" (input)="setPanel(i, 'icon', $any($event.target).value)" placeholder="info" />
2050
+ </mat-form-field>
1324
2051
  <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
1325
2052
  <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
1326
2053
  </mat-form-field>
@@ -1349,7 +2076,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1349
2076
  MatTooltipModule,
1350
2077
  PraxisIconDirective,
1351
2078
  ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1352
- <div class="pdx-expansion-editor">
2079
+ <div class="pdx-expansion-editor" data-testid="expansion-config-editor">
1353
2080
  <section class="sec">
1354
2081
  <h4>Aparência</h4>
1355
2082
  <div class="grid two">
@@ -1571,6 +2298,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1571
2298
  <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
1572
2299
  <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
1573
2300
  </mat-form-field>
2301
+ <mat-form-field appearance="outline"><mat-label>Ícone</mat-label>
2302
+ <input matInput [value]="p.icon || ''" (input)="setPanel(i, 'icon', $any($event.target).value)" placeholder="info" />
2303
+ </mat-form-field>
1574
2304
  <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
1575
2305
  <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
1576
2306
  </mat-form-field>
@@ -1595,6 +2325,116 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1595
2325
  type: Input
1596
2326
  }] } });
1597
2327
 
2328
+ class PraxisExpansionWidgetConfigEditor {
2329
+ inputs = null;
2330
+ widgetKey;
2331
+ expansionEditor;
2332
+ isDirty$ = new BehaviorSubject(false);
2333
+ isValid$ = new BehaviorSubject(true);
2334
+ isBusy$ = new BehaviorSubject(false);
2335
+ subscription = new Subscription();
2336
+ emptyConfig = {};
2337
+ get config() {
2338
+ return this.inputs?.config ?? this.emptyConfig;
2339
+ }
2340
+ get expansionId() {
2341
+ return this.inputs?.expansionId ?? this.widgetKey;
2342
+ }
2343
+ ngAfterViewInit() {
2344
+ if (!this.expansionEditor) {
2345
+ return;
2346
+ }
2347
+ this.subscription.add(this.expansionEditor.isDirty$.subscribe((value) => this.isDirty$.next(value)));
2348
+ this.subscription.add(this.expansionEditor.isValid$.subscribe((value) => this.isValid$.next(value)));
2349
+ this.subscription.add(this.expansionEditor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
2350
+ }
2351
+ ngOnDestroy() {
2352
+ this.subscription.unsubscribe();
2353
+ }
2354
+ getSettingsValue() {
2355
+ return this.buildValue(this.expansionEditor?.getSettingsValue());
2356
+ }
2357
+ onSave() {
2358
+ return this.buildValue(this.expansionEditor?.getSettingsValue());
2359
+ }
2360
+ reset() {
2361
+ this.expansionEditor?.reset();
2362
+ }
2363
+ buildValue(config) {
2364
+ return {
2365
+ inputs: {
2366
+ ...(this.inputs ?? {}),
2367
+ config: config ?? this.config,
2368
+ ...(this.expansionId ? { expansionId: this.expansionId } : {}),
2369
+ },
2370
+ };
2371
+ }
2372
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansionWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
2373
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisExpansionWidgetConfigEditor, isStandalone: true, selector: "praxis-expansion-widget-config-editor", inputs: { inputs: "inputs", widgetKey: "widgetKey" }, viewQueries: [{ propertyName: "expansionEditor", first: true, predicate: ["expansionEditor"], descendants: true }], ngImport: i0, template: `
2374
+ <section data-testid="expansion-widget-config-editor">
2375
+ <praxis-expansion-config-editor
2376
+ #expansionEditor
2377
+ [config]="config"
2378
+ [expansionId]="expansionId"
2379
+ />
2380
+ </section>
2381
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PraxisExpansionConfigEditor, selector: "praxis-expansion-config-editor", inputs: ["config", "expansionId"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2382
+ }
2383
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansionWidgetConfigEditor, decorators: [{
2384
+ type: Component,
2385
+ args: [{
2386
+ selector: 'praxis-expansion-widget-config-editor',
2387
+ standalone: true,
2388
+ imports: [CommonModule, PraxisExpansionConfigEditor],
2389
+ template: `
2390
+ <section data-testid="expansion-widget-config-editor">
2391
+ <praxis-expansion-config-editor
2392
+ #expansionEditor
2393
+ [config]="config"
2394
+ [expansionId]="expansionId"
2395
+ />
2396
+ </section>
2397
+ `,
2398
+ changeDetection: ChangeDetectionStrategy.OnPush,
2399
+ }]
2400
+ }], propDecorators: { inputs: [{
2401
+ type: Input
2402
+ }], widgetKey: [{
2403
+ type: Input
2404
+ }], expansionEditor: [{
2405
+ type: ViewChild,
2406
+ args: ['expansionEditor']
2407
+ }] } });
2408
+
2409
+ const PRAXIS_EXPANSION_PORTS = [
2410
+ {
2411
+ id: 'config',
2412
+ label: 'Configuracao',
2413
+ direction: 'input',
2414
+ semanticKind: 'config-fragment',
2415
+ schema: {
2416
+ id: 'ExpansionMetadata',
2417
+ kind: 'ts-type',
2418
+ ref: 'ExpansionMetadata',
2419
+ },
2420
+ description: 'Fragmento canonico de configuracao dos paineis e dos widgets internos.',
2421
+ exposure: { public: true, group: 'config' },
2422
+ },
2423
+ {
2424
+ id: 'widgetEvent',
2425
+ label: 'Evento interno de widget',
2426
+ direction: 'output',
2427
+ semanticKind: 'event',
2428
+ schema: {
2429
+ id: 'WidgetEventEnvelope',
2430
+ kind: 'ts-type',
2431
+ ref: 'WidgetEventEnvelope',
2432
+ },
2433
+ cardinality: 'stream',
2434
+ description: 'Bridge composta para transporte de eventos internos. As portas canonicas dos filhos sao resolvidas por component-port + nestedPath.',
2435
+ exposure: { public: true, advanced: true, group: 'composite' },
2436
+ },
2437
+ ];
1598
2438
  const PRAXIS_EXPANSION_COMPONENT_METADATA = {
1599
2439
  id: 'praxis-expansion',
1600
2440
  selector: 'praxis-expansion',
@@ -1602,6 +2442,14 @@ const PRAXIS_EXPANSION_COMPONENT_METADATA = {
1602
2442
  friendlyName: 'Praxis Expansion Panel',
1603
2443
  description: 'Acordeão/Painéis de expansão configuráveis por metadata, com aparência e tokens M3.',
1604
2444
  icon: 'unfold_more',
2445
+ authoringManifestRef: {
2446
+ componentId: 'praxis-expansion',
2447
+ source: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
2448
+ },
2449
+ configEditor: {
2450
+ component: PraxisExpansionWidgetConfigEditor,
2451
+ title: 'Configure expansion',
2452
+ },
1605
2453
  inputs: [
1606
2454
  { name: 'config', type: 'ExpansionMetadata', label: 'Configuração', description: 'Configuração JSON (acordeão, aparência e painéis)' },
1607
2455
  { name: 'expansionId', type: 'string', label: 'ID', description: 'Identificador para persistência (obrigatório)' },
@@ -1676,6 +2524,7 @@ const PRAXIS_EXPANSION_COMPONENT_METADATA = {
1676
2524
  ],
1677
2525
  tags: ['widget', 'expansion', 'accordion', 'configurable'],
1678
2526
  lib: '@praxisui/expansion',
2527
+ ports: PRAXIS_EXPANSION_PORTS,
1679
2528
  };
1680
2529
  function providePraxisExpansionMetadata() {
1681
2530
  return {
@@ -1691,6 +2540,315 @@ function providePraxisExpansionDefaults(opts) {
1691
2540
  return { provide: MAT_EXPANSION_PANEL_DEFAULT_OPTIONS, useValue: opts };
1692
2541
  }
1693
2542
 
2543
+ const panelItemSchema = {
2544
+ type: 'object',
2545
+ required: ['id', 'title'],
2546
+ properties: {
2547
+ id: { type: 'string' },
2548
+ title: { type: 'string' },
2549
+ description: { type: 'string' },
2550
+ icon: { type: 'string' },
2551
+ disabled: { type: 'boolean' },
2552
+ expanded: { type: 'boolean' },
2553
+ hideToggle: { type: 'boolean' },
2554
+ collapsedHeight: { type: 'string' },
2555
+ expandedHeight: { type: 'string' },
2556
+ content: { type: 'array', items: { type: 'object' } },
2557
+ widgets: { type: 'array', items: { type: 'object' } },
2558
+ actionButtons: { type: 'array', items: { type: 'object' } },
2559
+ },
2560
+ };
2561
+ const panelPatchSchema = {
2562
+ type: 'object',
2563
+ minProperties: 1,
2564
+ properties: {
2565
+ id: { type: 'string' },
2566
+ title: { type: 'string' },
2567
+ description: { type: 'string' },
2568
+ icon: { type: 'string' },
2569
+ disabled: { type: 'boolean' },
2570
+ expanded: { type: 'boolean' },
2571
+ hideToggle: { type: 'boolean' },
2572
+ collapsedHeight: { type: 'string' },
2573
+ expandedHeight: { type: 'string' },
2574
+ content: { type: 'array', items: { type: 'object' } },
2575
+ widgets: { type: 'array', items: { type: 'object' } },
2576
+ actionButtons: { type: 'array', items: { type: 'object' } },
2577
+ },
2578
+ };
2579
+ const PRAXIS_EXPANSION_AUTHORING_MANIFEST = {
2580
+ schemaVersion: '1.0.0',
2581
+ componentId: 'praxis-expansion',
2582
+ ownerPackage: '@praxisui/expansion',
2583
+ configSchemaId: 'ExpansionMetadata',
2584
+ manifestVersion: '1.0.0',
2585
+ runtimeInputs: [
2586
+ { name: 'config', type: 'ExpansionMetadata', description: 'Canonical accordion and panel configuration.' },
2587
+ { name: 'expansionId', type: 'string', description: 'Stable id used to derive expansion config persistence scope.' },
2588
+ { name: 'componentInstanceId', type: 'string', description: 'Optional instance discriminator for persistence scope.' },
2589
+ { name: 'context', type: 'Record<string, any>', description: 'Context passed to nested widgets.' },
2590
+ { name: 'strictValidation', type: 'boolean', description: 'Controls nested widget validation strictness.' },
2591
+ { name: 'defaultOptions', type: 'MatExpansionPanelDefaultOptions', description: 'Instance-level Material expansion defaults.' },
2592
+ { name: 'enableCustomization', type: 'boolean', description: 'Enables Settings Panel authoring surfaces.' },
2593
+ ],
2594
+ editableTargets: [
2595
+ { kind: 'panel', resolver: 'panel-by-id-or-title', description: 'A panel in config.panels[].' },
2596
+ { kind: 'panelHeader', resolver: 'panel-by-id-or-title', description: 'Header title, description, icon and heights for a panel.' },
2597
+ { kind: 'panelContent', resolver: 'panel-content-by-id', description: 'Lazy panel content hosted through fields, widgets or action buttons.' },
2598
+ { kind: 'expandedState', resolver: 'panel-by-id-or-title', description: 'Panel expanded state and default expanded selection.' },
2599
+ { kind: 'disabledState', resolver: 'panel-by-id-or-title', description: 'Panel disabled state.' },
2600
+ { kind: 'layout', resolver: 'expansion-layout-config', description: 'Accordion display mode, toggle position, density and visual layout.' },
2601
+ { kind: 'behavior', resolver: 'expansion-behavior-config', description: 'Accordion multi-expand and hide-toggle behavior.' },
2602
+ ],
2603
+ operations: [
2604
+ {
2605
+ operationId: 'panel.add',
2606
+ title: 'Add panel',
2607
+ scope: 'global',
2608
+ targetKind: 'panel',
2609
+ target: { kind: 'panel', resolver: 'panels-array', ambiguityPolicy: 'fail', required: false },
2610
+ inputSchema: panelItemSchema,
2611
+ effects: [{ kind: 'append-unique', path: 'panels[]', key: 'id' }],
2612
+ destructive: false,
2613
+ requiresConfirmation: false,
2614
+ validators: ['panel-id-unique', 'panel-order-deterministic', 'panel-content-valid'],
2615
+ affectedPaths: ['panels[]'],
2616
+ submissionImpact: 'config-only',
2617
+ preconditions: ['config-initialized'],
2618
+ },
2619
+ {
2620
+ operationId: 'panel.remove',
2621
+ title: 'Remove panel',
2622
+ scope: 'layout',
2623
+ targetKind: 'panel',
2624
+ target: { kind: 'panel', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2625
+ inputSchema: {
2626
+ type: 'object',
2627
+ properties: {
2628
+ replacementExpandedPanelId: { type: 'string' },
2629
+ },
2630
+ },
2631
+ effects: [{
2632
+ kind: 'compile-domain-patch',
2633
+ handler: 'expansion-panel-remove',
2634
+ handlerContract: {
2635
+ reads: ['accordion.multi', 'panels[]', 'panels[].id', 'panels[].expanded'],
2636
+ writes: ['panels[]', 'panels[].expanded'],
2637
+ identityKeys: ['panels[].id'],
2638
+ inputSchema: {
2639
+ type: 'object',
2640
+ properties: {
2641
+ replacementExpandedPanelId: { type: 'string' },
2642
+ },
2643
+ },
2644
+ failureModes: ['panel-not-found', 'replacement-panel-not-found', 'content-removal-not-confirmed', 'single-expand-conflict'],
2645
+ description: 'Removes the target panel by stable id and applies replacement expanded state when the removed panel was expanded.',
2646
+ },
2647
+ }],
2648
+ destructive: true,
2649
+ requiresConfirmation: true,
2650
+ validators: ['panel-exists', 'default-expanded-removal-safe', 'panel-content-removal-confirmed'],
2651
+ affectedPaths: ['panels[]', 'panels[].expanded'],
2652
+ submissionImpact: 'config-only',
2653
+ preconditions: ['config-initialized', 'target-panel-exists', 'confirmation-collected'],
2654
+ },
2655
+ {
2656
+ operationId: 'panel.title.set',
2657
+ title: 'Set panel title',
2658
+ scope: 'layout',
2659
+ targetKind: 'panelHeader',
2660
+ target: { kind: 'panelHeader', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2661
+ inputSchema: { type: 'object', required: ['title'], properties: { title: { type: 'string' } } },
2662
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2663
+ destructive: false,
2664
+ requiresConfirmation: false,
2665
+ validators: ['panel-exists', 'panel-title-valid'],
2666
+ affectedPaths: ['panels[].title'],
2667
+ submissionImpact: 'config-only',
2668
+ preconditions: ['config-initialized', 'target-panel-exists'],
2669
+ },
2670
+ {
2671
+ operationId: 'panel.description.set',
2672
+ title: 'Set panel description',
2673
+ scope: 'layout',
2674
+ targetKind: 'panelHeader',
2675
+ target: { kind: 'panelHeader', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2676
+ inputSchema: { type: 'object', required: ['description'], properties: { description: { type: 'string' } } },
2677
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2678
+ destructive: false,
2679
+ requiresConfirmation: false,
2680
+ validators: ['panel-exists', 'panel-description-valid'],
2681
+ affectedPaths: ['panels[].description'],
2682
+ submissionImpact: 'config-only',
2683
+ preconditions: ['config-initialized', 'target-panel-exists'],
2684
+ },
2685
+ {
2686
+ operationId: 'panel.icon.set',
2687
+ title: 'Set panel icon',
2688
+ scope: 'layout',
2689
+ targetKind: 'panelHeader',
2690
+ target: { kind: 'panelHeader', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2691
+ inputSchema: { type: 'object', required: ['icon'], properties: { icon: { type: 'string' } } },
2692
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2693
+ destructive: false,
2694
+ requiresConfirmation: false,
2695
+ validators: ['panel-exists', 'panel-icon-valid', 'editor-runtime-round-trip'],
2696
+ affectedPaths: ['panels[].icon'],
2697
+ submissionImpact: 'config-only',
2698
+ preconditions: ['config-initialized', 'target-panel-exists'],
2699
+ },
2700
+ {
2701
+ operationId: 'panel.order.set',
2702
+ title: 'Reorder panels',
2703
+ scope: 'layout',
2704
+ targetKind: 'panel',
2705
+ target: { kind: 'panel', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2706
+ inputSchema: { type: 'object', required: ['beforePanelId'], properties: { beforePanelId: { type: 'string' } } },
2707
+ effects: [{ kind: 'reorder-by-key', path: 'panels[]', key: 'id' }],
2708
+ destructive: false,
2709
+ requiresConfirmation: false,
2710
+ validators: ['panel-exists', 'panel-order-deterministic'],
2711
+ affectedPaths: ['panels[]'],
2712
+ submissionImpact: 'config-only',
2713
+ preconditions: ['config-initialized', 'target-panel-exists'],
2714
+ },
2715
+ {
2716
+ operationId: 'panel.disabled.set',
2717
+ title: 'Set disabled state',
2718
+ scope: 'interaction',
2719
+ targetKind: 'disabledState',
2720
+ target: { kind: 'disabledState', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2721
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
2722
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2723
+ destructive: false,
2724
+ requiresConfirmation: false,
2725
+ validators: ['panel-exists', 'disabled-expanded-compatible'],
2726
+ affectedPaths: ['panels[].disabled', 'panels[].expanded'],
2727
+ submissionImpact: 'config-only',
2728
+ preconditions: ['config-initialized', 'target-panel-exists'],
2729
+ },
2730
+ {
2731
+ operationId: 'behavior.multiExpand.set',
2732
+ title: 'Set multi-expand behavior',
2733
+ scope: 'interaction',
2734
+ targetKind: 'behavior',
2735
+ target: { kind: 'behavior', resolver: 'expansion-behavior-config', ambiguityPolicy: 'fail', required: true },
2736
+ inputSchema: { type: 'object', required: ['multi'], properties: { multi: { type: 'boolean' } } },
2737
+ effects: [{
2738
+ kind: 'compile-domain-patch',
2739
+ handler: 'expansion-multi-expand-set',
2740
+ handlerContract: {
2741
+ reads: ['accordion.multi', 'panels[]', 'panels[].id', 'panels[].expanded'],
2742
+ writes: ['accordion.multi', 'panels[].expanded'],
2743
+ identityKeys: ['panels[].id'],
2744
+ inputSchema: { type: 'object', required: ['multi'], properties: { multi: { type: 'boolean' } } },
2745
+ failureModes: ['multiple-expanded-panels-conflict', 'panel-id-missing'],
2746
+ description: 'Sets accordion.multi and collapses competing expanded panels when switching to single-expand behavior.',
2747
+ },
2748
+ }],
2749
+ destructive: false,
2750
+ requiresConfirmation: false,
2751
+ validators: ['multi-expand-default-compatible', 'accordion-values-valid', 'editor-runtime-round-trip'],
2752
+ affectedPaths: ['accordion.multi', 'panels[].expanded'],
2753
+ submissionImpact: 'config-only',
2754
+ preconditions: ['config-initialized'],
2755
+ },
2756
+ {
2757
+ operationId: 'behavior.defaultExpanded.set',
2758
+ title: 'Set default expanded panel',
2759
+ scope: 'interaction',
2760
+ targetKind: 'expandedState',
2761
+ target: { kind: 'expandedState', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2762
+ inputSchema: {
2763
+ type: 'object',
2764
+ required: ['expanded'],
2765
+ properties: {
2766
+ expanded: { type: 'boolean' },
2767
+ collapseOthers: { type: 'boolean' },
2768
+ },
2769
+ },
2770
+ effects: [{
2771
+ kind: 'compile-domain-patch',
2772
+ handler: 'expansion-default-expanded-upsert',
2773
+ handlerContract: {
2774
+ reads: ['accordion.multi', 'panels[]', 'panels[].id', 'panels[].expanded', 'panels[].disabled'],
2775
+ writes: ['panels[].expanded'],
2776
+ identityKeys: ['panels[].id'],
2777
+ inputSchema: {
2778
+ type: 'object',
2779
+ required: ['panelId', 'expanded'],
2780
+ properties: {
2781
+ panelId: { type: 'string' },
2782
+ expanded: { type: 'boolean' },
2783
+ collapseOthers: { type: 'boolean' },
2784
+ },
2785
+ },
2786
+ failureModes: ['panel-not-found', 'panel-disabled', 'single-expand-conflict'],
2787
+ description: 'Sets a panel expanded state and collapses competing panels when accordion.multi is false.',
2788
+ },
2789
+ }],
2790
+ validators: ['panel-exists', 'default-expanded-panel-exists', 'multi-expand-default-compatible', 'disabled-expanded-compatible'],
2791
+ destructive: false,
2792
+ requiresConfirmation: false,
2793
+ affectedPaths: ['panels[].expanded', 'accordion.multi'],
2794
+ submissionImpact: 'config-only',
2795
+ preconditions: ['config-initialized', 'target-panel-exists'],
2796
+ },
2797
+ {
2798
+ operationId: 'panel.content.set',
2799
+ title: 'Set panel content',
2800
+ scope: 'layout',
2801
+ targetKind: 'panelContent',
2802
+ target: { kind: 'panelContent', resolver: 'panel-content-by-id', ambiguityPolicy: 'fail', required: true },
2803
+ inputSchema: panelPatchSchema,
2804
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2805
+ destructive: false,
2806
+ requiresConfirmation: false,
2807
+ validators: ['panel-exists', 'panel-content-valid', 'lazy-content-compatible', 'nested-widget-contract-delegated'],
2808
+ affectedPaths: ['panels[].content', 'panels[].widgets', 'panels[].actionButtons'],
2809
+ submissionImpact: 'affects-schema-backed-data',
2810
+ preconditions: ['config-initialized', 'target-panel-exists'],
2811
+ },
2812
+ ],
2813
+ validators: [
2814
+ { validatorId: 'panel-id-unique', level: 'error', code: 'PEXP001', description: 'Panel ids must be unique and stable within config.panels[].' },
2815
+ { validatorId: 'panel-exists', level: 'error', code: 'PEXP002', description: 'Target panel must exist before applying the operation.' },
2816
+ { validatorId: 'panel-order-deterministic', level: 'error', code: 'PEXP003', description: 'Panel ordering must use stable ids, not transient array index as identity.' },
2817
+ { validatorId: 'panel-title-valid', level: 'error', code: 'PEXP004', description: 'Panel title must be a non-empty text value after localization/domain projection.' },
2818
+ { validatorId: 'panel-description-valid', level: 'warning', code: 'PEXP005', description: 'Panel description should remain plain header-support text and not replace panel content.' },
2819
+ { validatorId: 'panel-icon-valid', level: 'warning', code: 'PEXP006', description: 'Panel icon metadata must remain compatible with PraxisIconDirective and editor round-trip.' },
2820
+ { validatorId: 'panel-content-valid', level: 'error', code: 'PEXP007', description: 'Panel content must remain valid FieldMetadata[], WidgetDefinition[] or action button metadata.' },
2821
+ { validatorId: 'panel-content-removal-confirmed', level: 'error', code: 'PEXP008', description: 'Removing a panel with fields, widgets or action buttons is destructive and requires confirmation.' },
2822
+ { validatorId: 'default-expanded-panel-exists', level: 'error', code: 'PEXP009', description: 'Default expanded state must reference an existing panel id.' },
2823
+ { validatorId: 'default-expanded-removal-safe', level: 'error', code: 'PEXP010', description: 'Removing an expanded panel requires deterministic replacement state or explicit confirmation.' },
2824
+ { validatorId: 'multi-expand-default-compatible', level: 'error', code: 'PEXP011', description: 'When accordion.multi is false, at most one panel may be marked expanded by default.' },
2825
+ { validatorId: 'disabled-expanded-compatible', level: 'warning', code: 'PEXP012', description: 'A disabled panel should not be the only expanded/default focus target without explicit intent.' },
2826
+ { validatorId: 'accordion-values-valid', level: 'error', code: 'PEXP013', description: 'Accordion behavior values must match ExpansionMetadata and Angular Material expansion bindings.' },
2827
+ { validatorId: 'lazy-content-compatible', level: 'info', code: 'PEXP015', description: 'Panel content remains lazy through matExpansionPanelContent and should not require eager child runtime state.' },
2828
+ { validatorId: 'nested-widget-contract-delegated', level: 'info', code: 'PEXP016', description: 'Nested widget content remains governed by child component contracts and component-port nestedPath semantics.' },
2829
+ { validatorId: 'editor-runtime-round-trip', level: 'error', code: 'PEXP017', description: 'Settings Panel editor, runtime persistence and registry projection must preserve panel ids, order, icons and expanded state.' },
2830
+ ],
2831
+ roundTripRequirements: [
2832
+ 'Operations must preserve stable panel ids; array index may be used only as a resolver fallback, never as canonical identity.',
2833
+ 'Settings Panel editor, runtime persistence and registry projection must round-trip ExpansionMetadata without losing panel ids, order, icons or expanded state.',
2834
+ 'When accordion.multi is false, authoring must collapse competing panels or fail validation instead of producing multiple default-expanded panels.',
2835
+ 'Lazy panel content remains represented by panels[].content, panels[].widgets and panels[].actionButtons; authoring cannot require eager child component instances.',
2836
+ 'Nested widgets remain delegated through WidgetDefinition and component-port nestedPath semantics instead of being redefined by the expansion contract.',
2837
+ ],
2838
+ examples: [
2839
+ { id: 'add-summary-panel', request: 'Add a summary panel before the audit panel.', operationId: 'panel.add', params: { id: 'summary', title: 'Summary' }, isPositive: true },
2840
+ { id: 'rename-details-panel', request: 'Rename details to Account details.', operationId: 'panel.title.set', target: 'details', params: { title: 'Account details' }, isPositive: true },
2841
+ { id: 'describe-audit-panel', request: 'Set audit panel description to Recent changes.', operationId: 'panel.description.set', target: 'audit', params: { description: 'Recent changes' }, isPositive: true },
2842
+ { id: 'set-panel-icon', request: 'Use the info icon on the summary panel.', operationId: 'panel.icon.set', target: 'summary', params: { icon: 'info' }, isPositive: true },
2843
+ { id: 'set-default-expanded-panel', request: 'Open the summary panel by default.', operationId: 'behavior.defaultExpanded.set', target: 'summary', params: { expanded: true, collapseOthers: true }, isPositive: true },
2844
+ { id: 'enable-multi-expand', request: 'Allow multiple panels to stay open.', operationId: 'behavior.multiExpand.set', params: { multi: true }, isPositive: true },
2845
+ { id: 'disable-archive-panel', request: 'Disable the archive panel.', operationId: 'panel.disabled.set', target: 'archive', params: { disabled: true }, isPositive: true },
2846
+ { id: 'reject-missing-default-expanded', request: 'Open the missing panel by default.', operationId: 'behavior.defaultExpanded.set', target: 'missing', params: { expanded: true }, isPositive: false },
2847
+ { id: 'reject-duplicate-panel-id', request: 'Add another panel with id summary.', operationId: 'panel.add', params: { id: 'summary', title: 'Duplicate summary' }, isPositive: false },
2848
+ { id: 'confirm-remove-content-panel', request: 'Remove the details panel that contains widgets.', operationId: 'panel.remove', target: 'details', params: { replacementExpandedPanelId: 'summary' }, isPositive: true },
2849
+ ],
2850
+ };
2851
+
1694
2852
  /*
1695
2853
  * Public API Surface of praxis-expansion
1696
2854
  */
@@ -1699,4 +2857,4 @@ function providePraxisExpansionDefaults(opts) {
1699
2857
  * Generated bundle index. Do not edit.
1700
2858
  */
1701
2859
 
1702
- export { EXPANSION_AI_CAPABILITIES, PRAXIS_EXPANSION_COMPONENT_METADATA, PraxisExpansion, PraxisExpansionConfigEditor, providePraxisExpansionDefaults, providePraxisExpansionMetadata };
2860
+ export { EXPANSION_AI_CAPABILITIES, PRAXIS_EXPANSION_AUTHORING_MANIFEST, PRAXIS_EXPANSION_COMPONENT_METADATA, PraxisExpansion, PraxisExpansionConfigEditor, PraxisExpansionWidgetConfigEditor, providePraxisExpansionDefaults, providePraxisExpansionMetadata };