@praxisui/expansion 8.0.0-beta.7 → 8.0.0-beta.71

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, shouldRoutePromptToGovernedDecision, 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,286 @@ 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.mergeJsonObjects(this.optionalJsonObject(this.adapter.getAuthoringContext?.()), this.optionalJsonObject(request.action?.contextHints));
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
+ observationId: response.observationId ?? request.observationId ?? null,
304
+ assistantMessage: response.message || 'Preciso de mais detalhes para continuar.',
305
+ clarificationQuestions: this.toClarificationQuestions(response),
306
+ quickReplies: this.toQuickReplies(response),
307
+ canApply: false,
308
+ };
309
+ }
310
+ if (response.type === 'info') {
311
+ const message = response.message || response.explanation || 'Informacao gerada.';
312
+ return { state: 'success', phase: 'summarize', sessionId: response.sessionId ?? request.sessionId, assistantMessage: message, statusText: message, canApply: false };
313
+ }
314
+ if (response.type === 'error') {
315
+ const message = response.message || 'Falha ao gerar alteracao de expansion.';
316
+ return {
317
+ state: 'error',
318
+ phase: 'capture',
319
+ sessionId: response.sessionId ?? request.sessionId,
320
+ observationId: response.observationId ?? request.observationId ?? null,
321
+ assistantMessage: message,
322
+ errorText: message,
323
+ diagnostics: response.warnings?.length ? { warnings: response.warnings } : undefined,
324
+ };
325
+ }
326
+ if (response.patch && Object.keys(response.patch).length > 0) {
327
+ return {
328
+ state: 'error',
329
+ phase: 'review',
330
+ sessionId: response.sessionId ?? request.sessionId,
331
+ observationId: response.observationId ?? request.observationId ?? null,
332
+ assistantMessage: 'O expansion rejeitou patch livre. Gere um componentEditPlan validado pelo PRAXIS_EXPANSION_AUTHORING_MANIFEST antes de propor alteracao local.',
333
+ errorText: 'Patch livre de expansion rejeitado.',
334
+ canApply: false,
335
+ pendingPatch: null,
336
+ diagnostics: {
337
+ warnings: [
338
+ 'free-expansion-patch-rejected',
339
+ 'Use componentEditPlan validado contra PRAXIS_EXPANSION_AUTHORING_MANIFEST.',
340
+ ],
341
+ },
342
+ };
343
+ }
344
+ return {
345
+ state: 'success',
346
+ phase: 'summarize',
347
+ sessionId: response.sessionId ?? request.sessionId,
348
+ observationId: response.observationId ?? request.observationId ?? null,
349
+ assistantMessage: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
350
+ statusText: response.message || response.explanation || 'Nenhuma alteracao necessaria.',
351
+ canApply: false,
352
+ };
353
+ }
354
+ compileAdapterResponse(response) {
355
+ const compiled = this.adapter.compileAiResponse?.(response);
356
+ if (!compiled)
357
+ return response;
358
+ if (compiled.type === 'error') {
359
+ return {
360
+ type: 'error',
361
+ message: compiled.message || 'O componentEditPlan do expansion nao passou na validacao de capacidades.',
362
+ warnings: compiled.warnings,
363
+ };
364
+ }
365
+ const warnings = [...(response.warnings ?? []), ...(compiled.warnings ?? [])];
366
+ return { ...response, ...compiled, patch: compiled.patch, warnings: warnings.length ? warnings : undefined };
367
+ }
368
+ toChatMessages(messages, prompt) {
369
+ const supported = (messages ?? [])
370
+ .filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'system')
371
+ .map((message) => ({ role: message.role, content: message.text }))
372
+ .filter((message) => message.content.trim().length > 0);
373
+ return supported.length ? supported : [{ role: 'user', content: prompt }];
374
+ }
375
+ toClarificationQuestions(response) {
376
+ const labels = response.questions?.length
377
+ ? response.questions
378
+ : response.message ? [response.message] : ['Qual ajuste voce quer aplicar nos paineis?'];
379
+ const options = this.toQuickReplies(response).map((reply) => ({
380
+ id: reply.id,
381
+ label: reply.label,
382
+ value: reply.prompt,
383
+ description: reply.description ?? undefined,
384
+ contextHints: reply.contextHints ? { ...reply.contextHints } : undefined,
385
+ }));
386
+ return labels.map((label, index) => ({
387
+ id: `expansion-clarification-${index + 1}`,
388
+ type: options.length ? 'single-choice' : 'text',
389
+ label,
390
+ allowCustom: true,
391
+ options,
392
+ }));
393
+ }
394
+ toQuickReplies(response) {
395
+ const payloads = response.optionPayloads ?? [];
396
+ if (payloads.length) {
397
+ return payloads.map((option, index) => {
398
+ const label = option.label?.trim() || option.value?.trim() || `Opcao ${index + 1}`;
399
+ const prompt = option.value?.trim() || option.example?.trim() || label;
400
+ return {
401
+ id: `option-${index + 1}`,
402
+ label,
403
+ prompt,
404
+ kind: 'clarification-option',
405
+ description: option.example?.trim() || undefined,
406
+ contextHints: this.optionalJsonObject(option.contextHints),
407
+ };
408
+ });
409
+ }
410
+ return (response.options ?? [])
411
+ .filter((option) => !!option?.trim())
412
+ .map((option, index) => ({ id: `option-${index + 1}`, label: option.trim(), prompt: option.trim(), kind: 'clarification-option' }));
413
+ }
414
+ buildCurrentStateDigest(dataProfile) {
415
+ const panelCount = typeof dataProfile?.['panelCount'] === 'number' ? dataProfile['panelCount'] : undefined;
416
+ return panelCount !== undefined ? { rowCount: panelCount } : {};
417
+ }
418
+ shouldRouteToGovernedDecision(prompt, contextHints) {
419
+ return shouldRoutePromptToGovernedDecision(prompt, contextHints);
420
+ }
421
+ toGovernedDecisionHandoff(prompt, request) {
422
+ 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.';
423
+ return {
424
+ state: 'clarification',
425
+ phase: 'clarify',
426
+ sessionId: request.sessionId,
427
+ assistantMessage: message,
428
+ statusText: 'Handoff governado necessario.',
429
+ canApply: false,
430
+ quickReplies: [
431
+ {
432
+ id: 'shared-rule-handoff',
433
+ label: 'Continuar como regra governada',
434
+ prompt,
435
+ kind: 'shared-rule-handoff',
436
+ description: 'Criar intake de domain-rules em vez de aplicar patch local no expansion.',
437
+ icon: 'rule',
438
+ tone: 'warning',
439
+ contextHints: {
440
+ flowId: 'shared_rule_authoring',
441
+ source: 'praxis-expansion',
442
+ recommendedAction: 'domain-rules/intake',
443
+ },
444
+ },
445
+ ],
446
+ clarificationQuestions: [
447
+ {
448
+ id: 'expansion-governed-rule-confirmation',
449
+ type: 'confirm',
450
+ label: 'Deseja continuar pelo fluxo governado de regras compartilhadas?',
451
+ description: 'Esse caminho permite intake, simulacao, aprovacao/publicacao, materializacao e validacao de enforcement.',
452
+ required: true,
453
+ options: [
454
+ {
455
+ id: 'shared-rule-handoff',
456
+ label: 'Sim, continuar governado',
457
+ value: prompt,
458
+ description: 'Nao aplicar como patch local do expansion.',
459
+ contextHints: { flowId: 'shared_rule_authoring', source: 'praxis-expansion' },
460
+ },
461
+ ],
462
+ },
463
+ ],
464
+ diagnostics: {
465
+ governedDecisionHandoff: {
466
+ flowId: 'shared_rule_authoring',
467
+ sourcePrompt: prompt,
468
+ sourceComponent: 'praxis-expansion',
469
+ },
470
+ },
471
+ };
472
+ }
473
+ optionalJsonObject(value) {
474
+ if (value === undefined || value === null)
475
+ return undefined;
476
+ const object = this.toAiJsonObject(value);
477
+ return Object.keys(object).length ? object : undefined;
478
+ }
479
+ mergeJsonObjects(base, overlay) {
480
+ if (!base)
481
+ return overlay;
482
+ if (!overlay)
483
+ return base;
484
+ return {
485
+ ...base,
486
+ ...overlay,
487
+ };
488
+ }
489
+ toAiJsonObject(value) {
490
+ const record = this.toRecord(value);
491
+ if (!record)
492
+ return {};
493
+ try {
494
+ return JSON.parse(JSON.stringify(record));
495
+ }
496
+ catch {
497
+ return {};
498
+ }
499
+ }
500
+ toRecord(value) {
501
+ return value && typeof value === 'object' && !Array.isArray(value)
502
+ ? value
503
+ : null;
504
+ }
505
+ }
506
+
187
507
  class PraxisExpansion {
188
508
  config;
189
509
  expansionId;
@@ -211,9 +531,35 @@ class PraxisExpansion {
211
531
  catch {
212
532
  return undefined;
213
533
  } })();
534
+ aiApi = inject(AiBackendApiService);
535
+ assistantSessions = inject(PraxisAssistantSessionRegistryService);
536
+ aiTurnOrchestrator = inject(PraxisAssistantTurnOrchestratorService);
537
+ aiAssistantSessionEffect = effect(() => {
538
+ const session = this.assistantSessions.activeSession();
539
+ if (!session || session.id !== this.resolveAiAssistantSessionId())
540
+ return;
541
+ if (!this.aiAssistantOpen) {
542
+ this.openAiAssistantFromSession(session);
543
+ }
544
+ }, ...(ngDevMode ? [{ debugName: "aiAssistantSessionEffect" }] : /* istanbul ignore next */ []));
214
545
  warnedMissingId = false;
215
546
  panelForms = new Map();
216
547
  aiAdapter = new ExpansionAiAdapter(this);
548
+ aiAssistantOpen = false;
549
+ aiAssistantPrompt = '';
550
+ aiAssistantViewState = null;
551
+ aiAssistantLayout = createPraxisAssistantViewportLayout();
552
+ aiAssistantLabels = {
553
+ title: 'Copiloto semantico Praxis',
554
+ subtitle: 'Converse, revise e governe ajustes dos paineis.',
555
+ prompt: 'Mensagem',
556
+ promptPlaceholder: 'Descreva o ajuste que voce precisa nos paineis.',
557
+ emptyConversation: 'Diga o que voce quer alterar no expansion.',
558
+ submit: 'Interpretar pedido',
559
+ apply: 'Aplicar ajuste',
560
+ };
561
+ aiAssistantController = null;
562
+ aiAssistantStateSubscription = null;
217
563
  accordionRef;
218
564
  panels;
219
565
  injectedDefaults = inject(MAT_EXPANSION_PANEL_DEFAULT_OPTIONS, { optional: true });
@@ -237,6 +583,10 @@ class PraxisExpansion {
237
583
  this.persistConfig(this.config);
238
584
  }
239
585
  }
586
+ ngOnDestroy() {
587
+ this.assistantSessions.removeContextSession(this.buildAiAssistantContextSnapshot().identity);
588
+ this.aiAssistantStateSubscription?.unsubscribe();
589
+ }
240
590
  styleCss() {
241
591
  const t = this.config?.appearance?.tokens;
242
592
  const appearance = this.config?.appearance;
@@ -476,6 +826,315 @@ class PraxisExpansion {
476
826
  }
477
827
  this.cdr.markForCheck();
478
828
  }
829
+ openAiAssistant() {
830
+ this.initializeAiAssistantController();
831
+ this.aiAssistantOpen = true;
832
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
833
+ this.syncAiAssistantSession('active');
834
+ this.cdr.markForCheck();
835
+ }
836
+ openAiAssistantFromSession(session) {
837
+ if (session.id !== this.resolveAiAssistantSessionId())
838
+ return;
839
+ this.initializeAiAssistantController();
840
+ this.aiAssistantOpen = true;
841
+ this.aiAssistantController?.setContextItems(this.buildAiAssistantContextItems());
842
+ this.syncAiAssistantSession('active');
843
+ this.cdr.markForCheck();
844
+ }
845
+ closeAiAssistant() {
846
+ this.aiAssistantOpen = false;
847
+ this.syncAiAssistantSession('minimized');
848
+ this.cdr.markForCheck();
849
+ }
850
+ onAiAssistantPromptChange(prompt) {
851
+ this.aiAssistantPrompt = prompt;
852
+ this.syncAiAssistantSession();
853
+ }
854
+ onAiAssistantSubmit(prompt) {
855
+ this.aiAssistantController?.submitPrompt(prompt).subscribe((state) => {
856
+ this.aiAssistantPrompt = '';
857
+ this.aiAssistantViewState = state;
858
+ this.syncAiAssistantSession();
859
+ this.cdr.markForCheck();
860
+ });
861
+ }
862
+ onAiAssistantApply() {
863
+ this.aiAssistantController?.apply().subscribe((state) => {
864
+ this.aiAssistantViewState = state;
865
+ this.syncAiAssistantSession();
866
+ this.cdr.markForCheck();
867
+ });
868
+ }
869
+ onAiAssistantRetry() {
870
+ this.aiAssistantController?.retry().subscribe((state) => {
871
+ this.aiAssistantViewState = state;
872
+ this.syncAiAssistantSession();
873
+ this.cdr.markForCheck();
874
+ });
875
+ }
876
+ onAiAssistantCancel() {
877
+ this.aiAssistantController?.cancel().subscribe((state) => {
878
+ this.aiAssistantPrompt = '';
879
+ this.aiAssistantViewState = state;
880
+ this.syncAiAssistantSession();
881
+ this.cdr.markForCheck();
882
+ });
883
+ }
884
+ onAiAssistantQuickReply(reply) {
885
+ const controller = this.aiAssistantController;
886
+ if (!controller)
887
+ return;
888
+ const state = controller.snapshot();
889
+ const contextHints = reply.contextHints ? { ...reply.contextHints } : undefined;
890
+ const next$ = state.state === 'clarification'
891
+ ? controller.answerClarification({
892
+ id: reply.id,
893
+ label: reply.label,
894
+ value: reply.prompt,
895
+ description: reply.description ?? undefined,
896
+ contextHints,
897
+ })
898
+ : controller.submitPrompt(reply.prompt, {
899
+ kind: reply.kind || 'quick-reply',
900
+ id: reply.id,
901
+ value: reply.prompt,
902
+ displayPrompt: reply.label,
903
+ contextHints,
904
+ });
905
+ next$.subscribe((nextState) => {
906
+ this.aiAssistantPrompt = '';
907
+ this.aiAssistantViewState = nextState;
908
+ this.syncAiAssistantSession();
909
+ this.cdr.markForCheck();
910
+ });
911
+ }
912
+ onAiAssistantEditMessage(message) {
913
+ this.aiAssistantPrompt = message.text;
914
+ this.cdr.markForCheck();
915
+ }
916
+ onAiAssistantResendMessage(message) {
917
+ this.aiAssistantController?.resendMessage(message.id).subscribe((state) => {
918
+ this.aiAssistantPrompt = '';
919
+ this.aiAssistantViewState = state;
920
+ this.syncAiAssistantSession();
921
+ this.cdr.markForCheck();
922
+ });
923
+ }
924
+ onAiAssistantLayoutChange(layout) {
925
+ this.aiAssistantLayout = layout;
926
+ }
927
+ buildAiAssistantContextItems() {
928
+ const panels = this.config?.panels ?? [];
929
+ const items = [
930
+ {
931
+ id: 'component',
932
+ label: 'Componente',
933
+ value: 'Expansion',
934
+ kind: 'component',
935
+ icon: 'unfold_more',
936
+ },
937
+ {
938
+ id: 'expansion-id',
939
+ label: 'Expansion',
940
+ value: this.safeAiAssistantExpansionId(),
941
+ kind: 'custom',
942
+ icon: 'tag',
943
+ },
944
+ {
945
+ id: 'panels',
946
+ label: 'Paineis',
947
+ value: String(panels.length),
948
+ kind: 'custom',
949
+ icon: 'view_agenda',
950
+ },
951
+ ];
952
+ const expanded = panels.filter((panel) => panel.expanded).length;
953
+ if (expanded > 0) {
954
+ items.push({
955
+ id: 'expanded-panels',
956
+ label: 'Expandidos',
957
+ value: String(expanded),
958
+ kind: 'custom',
959
+ icon: 'expand_less',
960
+ });
961
+ }
962
+ return items;
963
+ }
964
+ initializeAiAssistantController() {
965
+ if (this.aiAssistantController)
966
+ return;
967
+ const flow = new ExpansionAgenticAuthoringTurnFlow(this.aiAdapter, this.aiApi);
968
+ const controller = this.aiTurnOrchestrator.createController(flow, {
969
+ componentId: this.aiAdapter.componentId || 'praxis-expansion',
970
+ componentType: this.aiAdapter.componentType || 'expansion',
971
+ contextItems: this.buildAiAssistantContextItems(),
972
+ });
973
+ this.aiAssistantController = controller;
974
+ this.aiAssistantViewState = controller.snapshot();
975
+ this.aiAssistantStateSubscription?.unsubscribe();
976
+ this.aiAssistantStateSubscription = controller.state$.subscribe((state) => {
977
+ this.aiAssistantViewState = state;
978
+ this.syncAiAssistantSession();
979
+ this.cdr.markForCheck();
980
+ });
981
+ this.cdr.markForCheck();
982
+ }
983
+ buildAiAssistantContextSnapshot() {
984
+ const counts = this.collectAiAssistantCounts();
985
+ const panelNames = this.collectAiAssistantPanelNames();
986
+ return {
987
+ identity: {
988
+ sessionId: this.resolveAiAssistantSessionId(),
989
+ ownerId: this.resolveAiAssistantOwnerId(),
990
+ ownerType: 'expansion',
991
+ componentId: 'praxis-expansion',
992
+ componentType: 'expansion',
993
+ routeKey: this.resolveAiAssistantRouteKey(),
994
+ },
995
+ target: {
996
+ kind: 'component',
997
+ id: this.resolveAiAssistantOwnerId(),
998
+ label: this.safeAiAssistantExpansionId(),
999
+ metadata: {
1000
+ expansionId: this.safeAiAssistantExpansionId(),
1001
+ hasCustomization: !!this.enableCustomization,
1002
+ },
1003
+ },
1004
+ contextItems: this.buildAiAssistantContextItems().map((item) => ({
1005
+ id: item.id,
1006
+ label: item.label,
1007
+ value: item.value || '',
1008
+ kind: item.kind,
1009
+ })),
1010
+ mode: 'agentic-authoring',
1011
+ authoringManifestRef: {
1012
+ componentId: 'praxis-expansion',
1013
+ source: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
1014
+ },
1015
+ schemaFields: panelNames.length ? panelNames : undefined,
1016
+ dataProfileDigest: {
1017
+ summary: `${counts.panelCount} painel(is), ${counts.contentPanelCount} com campo(s), ${counts.widgetPanelCount} com widget(s)`,
1018
+ counts,
1019
+ },
1020
+ runtimeStateDigest: {
1021
+ summary: `Expansion ${this.config?.accordion?.multi ? 'multi' : 'single'}, ${counts.expandedCount} painel(is) expandido(s)`,
1022
+ fields: [
1023
+ 'panels',
1024
+ 'accordion',
1025
+ 'content',
1026
+ 'widgets',
1027
+ ],
1028
+ },
1029
+ capabilityRefs: [
1030
+ {
1031
+ id: 'expansion.component-edit-plan',
1032
+ label: 'Plano de edicao de paineis',
1033
+ source: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
1034
+ risk: 'medium',
1035
+ },
1036
+ ],
1037
+ governanceHints: [
1038
+ {
1039
+ kind: 'business-rule-boundary',
1040
+ label: 'Regras compartilhadas exigem governanca',
1041
+ reason: 'Politicas de acesso, validacoes reutilizaveis e compliance nao devem ser aplicadas como patch local do expansion.',
1042
+ risk: 'high',
1043
+ },
1044
+ ],
1045
+ };
1046
+ }
1047
+ syncAiAssistantSession(visibility = null) {
1048
+ if (!this.enableCustomization)
1049
+ return;
1050
+ if (!this.aiAssistantOpen && !this.hasAiAssistantSessionState() && visibility !== 'minimized')
1051
+ return;
1052
+ const state = this.aiAssistantViewState;
1053
+ this.assistantSessions.upsertContextSession(this.buildAiAssistantContextSnapshot(), {
1054
+ title: 'Copiloto semantico Praxis',
1055
+ summary: this.resolveAiAssistantSummary(),
1056
+ mode: state?.mode || 'agentic-authoring',
1057
+ state: state?.state || 'idle',
1058
+ visibility: visibility ?? (this.aiAssistantOpen ? 'active' : 'minimized'),
1059
+ badge: this.resolveAiAssistantBadge(),
1060
+ icon: this.resolveAiAssistantIcon(),
1061
+ });
1062
+ }
1063
+ hasAiAssistantSessionState() {
1064
+ return !!this.aiAssistantPrompt.trim()
1065
+ || !!this.aiAssistantViewState?.messages?.length
1066
+ || !!this.aiAssistantViewState?.quickReplies?.length
1067
+ || !!this.aiAssistantViewState?.pendingPatch
1068
+ || !!this.aiAssistantViewState?.statusText?.trim()
1069
+ || !!this.aiAssistantViewState?.errorText?.trim();
1070
+ }
1071
+ resolveAiAssistantSessionId() {
1072
+ return `expansion:${this.resolveAiAssistantRouteKey()}:${this.resolveAiAssistantOwnerId()}`;
1073
+ }
1074
+ resolveAiAssistantOwnerId() {
1075
+ return (this.componentInstanceId || this.safeAiAssistantExpansionId() || 'expansion').trim() || 'expansion';
1076
+ }
1077
+ safeAiAssistantExpansionId() {
1078
+ return String(this.expansionId || '').trim();
1079
+ }
1080
+ resolveAiAssistantRouteKey() {
1081
+ const routePath = this.route?.snapshot?.routeConfig?.path?.trim();
1082
+ return routePath || 'local';
1083
+ }
1084
+ resolveAiAssistantSummary() {
1085
+ const status = this.aiAssistantViewState?.statusText?.trim();
1086
+ if (status)
1087
+ return status;
1088
+ const error = this.aiAssistantViewState?.errorText?.trim();
1089
+ if (error)
1090
+ return error;
1091
+ const prompt = this.aiAssistantPrompt.trim();
1092
+ if (prompt)
1093
+ return prompt.length > 96 ? `${prompt.slice(0, 93)}...` : prompt;
1094
+ const lastMessage = [...(this.aiAssistantViewState?.messages ?? [])].reverse()
1095
+ .find((message) => message.role === 'assistant' || message.role === 'user');
1096
+ if (lastMessage?.text) {
1097
+ return lastMessage.text.length > 96 ? `${lastMessage.text.slice(0, 93)}...` : lastMessage.text;
1098
+ }
1099
+ return 'Assistente contextual dos paineis.';
1100
+ }
1101
+ resolveAiAssistantBadge() {
1102
+ const state = this.aiAssistantViewState?.state;
1103
+ if (state === 'error')
1104
+ return 'erro';
1105
+ if (state === 'clarification')
1106
+ return 'revisar';
1107
+ if (state === 'review')
1108
+ return 'preview';
1109
+ if (state === 'success')
1110
+ return 'ok';
1111
+ return undefined;
1112
+ }
1113
+ resolveAiAssistantIcon() {
1114
+ const state = this.aiAssistantViewState?.state;
1115
+ if (state === 'error')
1116
+ return 'error';
1117
+ if (state === 'clarification')
1118
+ return 'rule';
1119
+ if (state === 'review')
1120
+ return 'rate_review';
1121
+ return 'auto_awesome';
1122
+ }
1123
+ collectAiAssistantPanelNames() {
1124
+ return Array.from(new Set((this.config?.panels ?? [])
1125
+ .map((panel, index) => panel.id || panel.title || `panel-${index + 1}`)
1126
+ .filter((name) => typeof name === 'string' && !!name.trim())));
1127
+ }
1128
+ collectAiAssistantCounts() {
1129
+ const panels = this.config?.panels ?? [];
1130
+ return {
1131
+ panelCount: panels.length,
1132
+ contentPanelCount: panels.filter((panel) => Array.isArray(panel.content) && panel.content.length > 0).length,
1133
+ widgetPanelCount: panels.filter((panel) => Array.isArray(panel.widgets) && panel.widgets.length > 0).length,
1134
+ actionPanelCount: panels.filter((panel) => Array.isArray(panel.actionButtons) && panel.actionButtons.length > 0).length,
1135
+ expandedCount: panels.filter((panel) => !!panel.expanded).length,
1136
+ };
1137
+ }
479
1138
  openEditor() {
480
1139
  const key = this.storageKey() || this.expansionId || 'default';
481
1140
  const ref = this.settings.open({
@@ -558,8 +1217,8 @@ class PraxisExpansion {
558
1217
  this.warnedMissingId = true;
559
1218
  console.warn('[PraxisExpansion] expansionId is required for config persistence.');
560
1219
  }
561
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansion, deps: [], target: i0.ɵɵFactoryTarget.Component });
562
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: PraxisExpansion, isStandalone: true, selector: "praxis-expansion", inputs: { config: "config", expansionId: "expansionId", componentInstanceId: "componentInstanceId", context: "context", strictValidation: "strictValidation", enableCustomization: "enableCustomization", defaultOptions: "defaultOptions" }, outputs: { opened: "opened", closed: "closed", expandedChange: "expandedChange", afterExpand: "afterExpand", afterCollapse: "afterCollapse", destroyed: "destroyed", widgetEvent: "widgetEvent" }, viewQueries: [{ propertyName: "accordionRef", first: true, predicate: ["accordion"], descendants: true }, { propertyName: "panels", predicate: ["panel"], descendants: true, read: MatExpansionPanel }], usesOnChanges: true, ngImport: i0, template: `
1220
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisExpansion, deps: [], target: i0.ɵɵFactoryTarget.Component });
1221
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisExpansion, isStandalone: true, selector: "praxis-expansion", inputs: { config: "config", expansionId: "expansionId", componentInstanceId: "componentInstanceId", context: "context", strictValidation: "strictValidation", enableCustomization: "enableCustomization", defaultOptions: "defaultOptions" }, outputs: { opened: "opened", closed: "closed", expandedChange: "expandedChange", afterExpand: "afterExpand", afterCollapse: "afterCollapse", destroyed: "destroyed", widgetEvent: "widgetEvent" }, viewQueries: [{ propertyName: "accordionRef", first: true, predicate: ["accordion"], descendants: true }, { propertyName: "panels", predicate: ["panel"], descendants: true, read: MatExpansionPanel }], usesOnChanges: true, ngImport: i0, template: `
563
1222
  <div
564
1223
  class="praxis-expansion-root"
565
1224
  [class.density-compact]="config?.appearance?.density === 'compact'"
@@ -567,16 +1226,59 @@ class PraxisExpansion {
567
1226
  [class.density-spacious]="config?.appearance?.density === 'spacious'"
568
1227
  [ngClass]="config?.appearance?.themeClass || ''"
569
1228
  [attr.data-expansion-id]="expansionId || 'default'"
570
- >
571
- <style *ngIf="config?.appearance?.customCss" [innerHTML]="config?.appearance?.customCss"></style>
572
- <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
573
-
1229
+ >
1230
+ @if (config?.appearance?.customCss) {
1231
+ <style [innerHTML]="config?.appearance?.customCss"></style>
1232
+ }
1233
+ @if (styleCss(); as s) {
1234
+ <style [innerHTML]="s"></style>
1235
+ }
1236
+
574
1237
  @if (enableCustomization) {
575
1238
  <div class="expansion-ai-assistant">
576
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
1239
+ <button
1240
+ mat-mini-fab
1241
+ color="primary"
1242
+ type="button"
1243
+ class="expansion-ai-assistant-trigger"
1244
+ aria-label="Abrir copiloto semantico Praxis dos paineis"
1245
+ data-testid="praxis-expansion-ai-assistant-trigger"
1246
+ (click)="openAiAssistant()"
1247
+ >
1248
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
1249
+ </button>
577
1250
  </div>
578
1251
  }
579
-
1252
+
1253
+ @if (aiAssistantOpen && aiAssistantViewState) {
1254
+ <praxis-ai-assistant-shell
1255
+ [labels]="aiAssistantLabels"
1256
+ [mode]="aiAssistantViewState.mode"
1257
+ [state]="aiAssistantViewState.state"
1258
+ [contextItems]="aiAssistantViewState.contextItems"
1259
+ [attachments]="aiAssistantViewState.attachments"
1260
+ [messages]="aiAssistantViewState.messages"
1261
+ [quickReplies]="aiAssistantViewState.quickReplies"
1262
+ [prompt]="aiAssistantPrompt"
1263
+ [statusText]="aiAssistantViewState.statusText"
1264
+ [errorText]="aiAssistantViewState.errorText"
1265
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
1266
+ [canApply]="aiAssistantViewState.canApply"
1267
+ [layout]="aiAssistantLayout"
1268
+ testIdPrefix="praxis-expansion-ai-assistant"
1269
+ (promptChange)="onAiAssistantPromptChange($event)"
1270
+ (submitPrompt)="onAiAssistantSubmit($event)"
1271
+ (apply)="onAiAssistantApply()"
1272
+ (retryTurn)="onAiAssistantRetry()"
1273
+ (cancelTurn)="onAiAssistantCancel()"
1274
+ (quickReply)="onAiAssistantQuickReply($event)"
1275
+ (editMessage)="onAiAssistantEditMessage($event)"
1276
+ (resendMessage)="onAiAssistantResendMessage($event)"
1277
+ (layoutChange)="onAiAssistantLayoutChange($event)"
1278
+ (close)="closeAiAssistant()"
1279
+ ></praxis-ai-assistant-shell>
1280
+ }
1281
+
580
1282
  @if (hasMultiple()) {
581
1283
  <mat-accordion
582
1284
  #accordion
@@ -585,7 +1287,7 @@ class PraxisExpansion {
585
1287
  [displayMode]="config?.accordion?.displayMode || 'default'"
586
1288
  [togglePosition]="config?.accordion?.togglePosition || 'after'"
587
1289
  [hideToggle]="config?.accordion?.hideToggle || false"
588
- >
1290
+ >
589
1291
  @for (p of (config?.panels || []); let i = $index; track p.id ?? i) {
590
1292
  @let id = panelDomId(p, i);
591
1293
  <mat-expansion-panel
@@ -600,15 +1302,20 @@ class PraxisExpansion {
600
1302
  (afterExpand)="emitAfterExpand(p, i)"
601
1303
  (afterCollapse)="emitAfterCollapse(p, i)"
602
1304
  (destroyed)="emitDestroyed(p, i)"
603
- >
1305
+ >
604
1306
  <mat-expansion-panel-header
605
1307
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
606
1308
  [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
607
- >
1309
+ >
1310
+ @if (p.icon) {
1311
+ <mat-icon [praxisIcon]="p.icon"></mat-icon>
1312
+ }
608
1313
  <mat-panel-title>{{ p.title }}</mat-panel-title>
609
- <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
1314
+ @if (p.description) {
1315
+ <mat-panel-description>{{ p.description }}</mat-panel-description>
1316
+ }
610
1317
  </mat-expansion-panel-header>
611
-
1318
+
612
1319
  <ng-template matExpansionPanelContent>
613
1320
  @if (p.content?.length) {
614
1321
  <ng-container
@@ -617,7 +1324,7 @@ class PraxisExpansion {
617
1324
  [formGroup]="panelFormFor(p, i)"
618
1325
  ></ng-container>
619
1326
  }
620
-
1327
+
621
1328
  @if (p.widgets?.length) {
622
1329
  @for (w of p.widgets; let wi = $index; track wi) {
623
1330
  <ng-container
@@ -628,12 +1335,14 @@ class PraxisExpansion {
628
1335
  ></ng-container>
629
1336
  }
630
1337
  }
631
-
1338
+
632
1339
  @if (p.actionButtons?.length) {
633
1340
  <mat-action-row>
634
1341
  @for (a of p.actionButtons; track a.label) {
635
1342
  <button mat-stroked-button (click)="emitAction(p, i, a.action)">
636
- <mat-icon *ngIf="a.icon" [praxisIcon]="a.icon"></mat-icon>
1343
+ @if (a.icon) {
1344
+ <mat-icon [praxisIcon]="a.icon"></mat-icon>
1345
+ }
637
1346
  {{ a.label }}
638
1347
  </button>
639
1348
  }
@@ -657,15 +1366,20 @@ class PraxisExpansion {
657
1366
  (afterExpand)="emitAfterExpand(p, 0)"
658
1367
  (afterCollapse)="emitAfterCollapse(p, 0)"
659
1368
  (destroyed)="emitDestroyed(p, 0)"
660
- >
1369
+ >
661
1370
  <mat-expansion-panel-header
662
1371
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
663
1372
  [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
664
- >
1373
+ >
1374
+ @if (p.icon) {
1375
+ <mat-icon [praxisIcon]="p.icon"></mat-icon>
1376
+ }
665
1377
  <mat-panel-title>{{ p.title }}</mat-panel-title>
666
- <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
1378
+ @if (p.description) {
1379
+ <mat-panel-description>{{ p.description }}</mat-panel-description>
1380
+ }
667
1381
  </mat-expansion-panel-header>
668
-
1382
+
669
1383
  <ng-template matExpansionPanelContent>
670
1384
  @if (p.content?.length) {
671
1385
  <ng-container
@@ -674,7 +1388,7 @@ class PraxisExpansion {
674
1388
  [formGroup]="panelFormFor(p, 0)"
675
1389
  ></ng-container>
676
1390
  }
677
-
1391
+
678
1392
  @if (p.widgets?.length) {
679
1393
  @for (w of p.widgets; let wi = $index; track wi) {
680
1394
  <ng-container
@@ -685,12 +1399,14 @@ class PraxisExpansion {
685
1399
  ></ng-container>
686
1400
  }
687
1401
  }
688
-
1402
+
689
1403
  @if (p.actionButtons?.length) {
690
1404
  <mat-action-row>
691
1405
  @for (a of p.actionButtons; track a.label) {
692
1406
  <button mat-stroked-button (click)="emitAction(p, 0, a.action)">
693
- <mat-icon *ngIf="a.icon" [praxisIcon]="a.icon"></mat-icon>
1407
+ @if (a.icon) {
1408
+ <mat-icon [praxisIcon]="a.icon"></mat-icon>
1409
+ }
694
1410
  {{ a.label }}
695
1411
  </button>
696
1412
  }
@@ -701,26 +1417,29 @@ class PraxisExpansion {
701
1417
  } @else {
702
1418
  <div class="praxis-expansion-empty">
703
1419
  <span>Nenhum painel configurado</span>
704
- <button mat-stroked-button color="primary" *ngIf="enableCustomization" (click)="openEditor()">
705
- <mat-icon [praxisIcon]="'tune'"></mat-icon> Configurar
706
- </button>
1420
+ @if (enableCustomization) {
1421
+ <button mat-stroked-button color="primary" (click)="openEditor()">
1422
+ <mat-icon [praxisIcon]="'tune'"></mat-icon> Configurar
1423
+ </button>
1424
+ }
707
1425
  </div>
708
1426
  }
709
1427
  }
710
1428
  </div>
711
1429
  <!-- Edit button -->
712
- <button
713
- *ngIf="enableCustomization"
714
- mat-fab
715
- class="edit-fab"
716
- aria-label="Editar painéis"
717
- (click)="openEditor()"
718
- >
719
- <mat-icon fontIcon="edit"></mat-icon>
720
- </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 });
1430
+ @if (enableCustomization) {
1431
+ <button
1432
+ mat-fab
1433
+ class="edit-fab"
1434
+ aria-label="Editar painéis"
1435
+ (click)="openEditor()"
1436
+ >
1437
+ <mat-icon fontIcon="edit"></mat-icon>
1438
+ </button>
1439
+ }
1440
+ `, 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: "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", "canvasFocus"] }, { 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
1441
  }
723
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansion, decorators: [{
1442
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisExpansion, decorators: [{
724
1443
  type: Component,
725
1444
  args: [{ selector: 'praxis-expansion', standalone: true, imports: [
726
1445
  CommonModule,
@@ -730,7 +1449,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
730
1449
  DynamicFieldLoaderDirective,
731
1450
  DynamicWidgetLoaderDirective,
732
1451
  PraxisIconDirective,
733
- PraxisAiAssistantComponent,
1452
+ PraxisAiAssistantShellComponent,
734
1453
  ], template: `
735
1454
  <div
736
1455
  class="praxis-expansion-root"
@@ -739,16 +1458,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
739
1458
  [class.density-spacious]="config?.appearance?.density === 'spacious'"
740
1459
  [ngClass]="config?.appearance?.themeClass || ''"
741
1460
  [attr.data-expansion-id]="expansionId || 'default'"
742
- >
743
- <style *ngIf="config?.appearance?.customCss" [innerHTML]="config?.appearance?.customCss"></style>
744
- <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
745
-
1461
+ >
1462
+ @if (config?.appearance?.customCss) {
1463
+ <style [innerHTML]="config?.appearance?.customCss"></style>
1464
+ }
1465
+ @if (styleCss(); as s) {
1466
+ <style [innerHTML]="s"></style>
1467
+ }
1468
+
746
1469
  @if (enableCustomization) {
747
1470
  <div class="expansion-ai-assistant">
748
- <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
1471
+ <button
1472
+ mat-mini-fab
1473
+ color="primary"
1474
+ type="button"
1475
+ class="expansion-ai-assistant-trigger"
1476
+ aria-label="Abrir copiloto semantico Praxis dos paineis"
1477
+ data-testid="praxis-expansion-ai-assistant-trigger"
1478
+ (click)="openAiAssistant()"
1479
+ >
1480
+ <mat-icon [praxisIcon]="'auto_awesome'"></mat-icon>
1481
+ </button>
749
1482
  </div>
750
1483
  }
751
-
1484
+
1485
+ @if (aiAssistantOpen && aiAssistantViewState) {
1486
+ <praxis-ai-assistant-shell
1487
+ [labels]="aiAssistantLabels"
1488
+ [mode]="aiAssistantViewState.mode"
1489
+ [state]="aiAssistantViewState.state"
1490
+ [contextItems]="aiAssistantViewState.contextItems"
1491
+ [attachments]="aiAssistantViewState.attachments"
1492
+ [messages]="aiAssistantViewState.messages"
1493
+ [quickReplies]="aiAssistantViewState.quickReplies"
1494
+ [prompt]="aiAssistantPrompt"
1495
+ [statusText]="aiAssistantViewState.statusText"
1496
+ [errorText]="aiAssistantViewState.errorText"
1497
+ [busy]="aiAssistantViewState.state === 'processing' || aiAssistantViewState.state === 'applying'"
1498
+ [canApply]="aiAssistantViewState.canApply"
1499
+ [layout]="aiAssistantLayout"
1500
+ testIdPrefix="praxis-expansion-ai-assistant"
1501
+ (promptChange)="onAiAssistantPromptChange($event)"
1502
+ (submitPrompt)="onAiAssistantSubmit($event)"
1503
+ (apply)="onAiAssistantApply()"
1504
+ (retryTurn)="onAiAssistantRetry()"
1505
+ (cancelTurn)="onAiAssistantCancel()"
1506
+ (quickReply)="onAiAssistantQuickReply($event)"
1507
+ (editMessage)="onAiAssistantEditMessage($event)"
1508
+ (resendMessage)="onAiAssistantResendMessage($event)"
1509
+ (layoutChange)="onAiAssistantLayoutChange($event)"
1510
+ (close)="closeAiAssistant()"
1511
+ ></praxis-ai-assistant-shell>
1512
+ }
1513
+
752
1514
  @if (hasMultiple()) {
753
1515
  <mat-accordion
754
1516
  #accordion
@@ -757,7 +1519,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
757
1519
  [displayMode]="config?.accordion?.displayMode || 'default'"
758
1520
  [togglePosition]="config?.accordion?.togglePosition || 'after'"
759
1521
  [hideToggle]="config?.accordion?.hideToggle || false"
760
- >
1522
+ >
761
1523
  @for (p of (config?.panels || []); let i = $index; track p.id ?? i) {
762
1524
  @let id = panelDomId(p, i);
763
1525
  <mat-expansion-panel
@@ -772,15 +1534,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
772
1534
  (afterExpand)="emitAfterExpand(p, i)"
773
1535
  (afterCollapse)="emitAfterCollapse(p, i)"
774
1536
  (destroyed)="emitDestroyed(p, i)"
775
- >
1537
+ >
776
1538
  <mat-expansion-panel-header
777
1539
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
778
1540
  [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
779
- >
1541
+ >
1542
+ @if (p.icon) {
1543
+ <mat-icon [praxisIcon]="p.icon"></mat-icon>
1544
+ }
780
1545
  <mat-panel-title>{{ p.title }}</mat-panel-title>
781
- <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
1546
+ @if (p.description) {
1547
+ <mat-panel-description>{{ p.description }}</mat-panel-description>
1548
+ }
782
1549
  </mat-expansion-panel-header>
783
-
1550
+
784
1551
  <ng-template matExpansionPanelContent>
785
1552
  @if (p.content?.length) {
786
1553
  <ng-container
@@ -789,7 +1556,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
789
1556
  [formGroup]="panelFormFor(p, i)"
790
1557
  ></ng-container>
791
1558
  }
792
-
1559
+
793
1560
  @if (p.widgets?.length) {
794
1561
  @for (w of p.widgets; let wi = $index; track wi) {
795
1562
  <ng-container
@@ -800,12 +1567,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
800
1567
  ></ng-container>
801
1568
  }
802
1569
  }
803
-
1570
+
804
1571
  @if (p.actionButtons?.length) {
805
1572
  <mat-action-row>
806
1573
  @for (a of p.actionButtons; track a.label) {
807
1574
  <button mat-stroked-button (click)="emitAction(p, i, a.action)">
808
- <mat-icon *ngIf="a.icon" [praxisIcon]="a.icon"></mat-icon>
1575
+ @if (a.icon) {
1576
+ <mat-icon [praxisIcon]="a.icon"></mat-icon>
1577
+ }
809
1578
  {{ a.label }}
810
1579
  </button>
811
1580
  }
@@ -829,15 +1598,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
829
1598
  (afterExpand)="emitAfterExpand(p, 0)"
830
1599
  (afterCollapse)="emitAfterCollapse(p, 0)"
831
1600
  (destroyed)="emitDestroyed(p, 0)"
832
- >
1601
+ >
833
1602
  <mat-expansion-panel-header
834
1603
  [collapsedHeight]="p.collapsedHeight || (defaultOptions?.collapsedHeight || injectedDefaults?.collapsedHeight) || ''"
835
1604
  [expandedHeight]="p.expandedHeight || (defaultOptions?.expandedHeight || injectedDefaults?.expandedHeight) || ''"
836
- >
1605
+ >
1606
+ @if (p.icon) {
1607
+ <mat-icon [praxisIcon]="p.icon"></mat-icon>
1608
+ }
837
1609
  <mat-panel-title>{{ p.title }}</mat-panel-title>
838
- <mat-panel-description *ngIf="p.description">{{ p.description }}</mat-panel-description>
1610
+ @if (p.description) {
1611
+ <mat-panel-description>{{ p.description }}</mat-panel-description>
1612
+ }
839
1613
  </mat-expansion-panel-header>
840
-
1614
+
841
1615
  <ng-template matExpansionPanelContent>
842
1616
  @if (p.content?.length) {
843
1617
  <ng-container
@@ -846,7 +1620,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
846
1620
  [formGroup]="panelFormFor(p, 0)"
847
1621
  ></ng-container>
848
1622
  }
849
-
1623
+
850
1624
  @if (p.widgets?.length) {
851
1625
  @for (w of p.widgets; let wi = $index; track wi) {
852
1626
  <ng-container
@@ -857,12 +1631,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
857
1631
  ></ng-container>
858
1632
  }
859
1633
  }
860
-
1634
+
861
1635
  @if (p.actionButtons?.length) {
862
1636
  <mat-action-row>
863
1637
  @for (a of p.actionButtons; track a.label) {
864
1638
  <button mat-stroked-button (click)="emitAction(p, 0, a.action)">
865
- <mat-icon *ngIf="a.icon" [praxisIcon]="a.icon"></mat-icon>
1639
+ @if (a.icon) {
1640
+ <mat-icon [praxisIcon]="a.icon"></mat-icon>
1641
+ }
866
1642
  {{ a.label }}
867
1643
  </button>
868
1644
  }
@@ -873,24 +1649,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
873
1649
  } @else {
874
1650
  <div class="praxis-expansion-empty">
875
1651
  <span>Nenhum painel configurado</span>
876
- <button mat-stroked-button color="primary" *ngIf="enableCustomization" (click)="openEditor()">
877
- <mat-icon [praxisIcon]="'tune'"></mat-icon> Configurar
878
- </button>
1652
+ @if (enableCustomization) {
1653
+ <button mat-stroked-button color="primary" (click)="openEditor()">
1654
+ <mat-icon [praxisIcon]="'tune'"></mat-icon> Configurar
1655
+ </button>
1656
+ }
879
1657
  </div>
880
1658
  }
881
1659
  }
882
1660
  </div>
883
1661
  <!-- Edit button -->
884
- <button
885
- *ngIf="enableCustomization"
886
- mat-fab
887
- class="edit-fab"
888
- aria-label="Editar painéis"
889
- (click)="openEditor()"
890
- >
891
- <mat-icon fontIcon="edit"></mat-icon>
892
- </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"] }]
1662
+ @if (enableCustomization) {
1663
+ <button
1664
+ mat-fab
1665
+ class="edit-fab"
1666
+ aria-label="Editar painéis"
1667
+ (click)="openEditor()"
1668
+ >
1669
+ <mat-icon fontIcon="edit"></mat-icon>
1670
+ </button>
1671
+ }
1672
+ `, 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
1673
  }], propDecorators: { config: [{
895
1674
  type: Input
896
1675
  }], expansionId: [{
@@ -1097,9 +1876,9 @@ class PraxisExpansionConfigEditor {
1097
1876
  const current = JSON.stringify(this.config || {});
1098
1877
  this.isDirty$.next(current !== this.initialJson);
1099
1878
  }
1100
- 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
- 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">
1879
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisExpansionConfigEditor, deps: [{ token: SETTINGS_PANEL_DATA }], target: i0.ɵɵFactoryTarget.Component });
1880
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisExpansionConfigEditor, isStandalone: true, selector: "praxis-expansion-config-editor", inputs: { config: "config", expansionId: "expansionId" }, usesOnChanges: true, ngImport: i0, template: `
1881
+ <div class="pdx-expansion-editor" data-testid="expansion-config-editor">
1103
1882
  <section class="sec">
1104
1883
  <h4>Aparência</h4>
1105
1884
  <div class="grid two">
@@ -1117,7 +1896,7 @@ class PraxisExpansionConfigEditor {
1117
1896
  </select>
1118
1897
  </mat-form-field>
1119
1898
  </div>
1120
-
1899
+
1121
1900
  <h5>Container</h5>
1122
1901
  <div class="grid two">
1123
1902
  <mat-form-field appearance="outline">
@@ -1159,7 +1938,7 @@ class PraxisExpansionConfigEditor {
1159
1938
  <input matInput [value]="config?.appearance?.container?.panelGap || ''" (input)="setContainer('panelGap', $any($event.target).value)" placeholder="12px" />
1160
1939
  </mat-form-field>
1161
1940
  </div>
1162
-
1941
+
1163
1942
  <h5>Cabeçalho</h5>
1164
1943
  <div class="grid two">
1165
1944
  <mat-form-field appearance="outline">
@@ -1219,7 +1998,7 @@ class PraxisExpansionConfigEditor {
1219
1998
  <input matInput [value]="config?.appearance?.header?.toggleIconSize || ''" (input)="setHeader('toggleIconSize', $any($event.target).value)" placeholder="12px" />
1220
1999
  </mat-form-field>
1221
2000
  </div>
1222
-
2001
+
1223
2002
  <h5>Estados</h5>
1224
2003
  <div class="grid two">
1225
2004
  <mat-form-field appearance="outline">
@@ -1265,13 +2044,15 @@ class PraxisExpansionConfigEditor {
1265
2044
  type="button"
1266
2045
  [matTooltip]="'Use tokens M3, por exemplo: var(--md-sys-color-primary)'"
1267
2046
  matTooltipPosition="above"
1268
- >
2047
+ >
1269
2048
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
1270
2049
  </button>
1271
2050
  </mat-form-field>
1272
- <div class="error" *ngIf="tokensError">{{ tokensError }}</div>
2051
+ @if (tokensError) {
2052
+ <div class="error">{{ tokensError }}</div>
2053
+ }
1273
2054
  </section>
1274
-
2055
+
1275
2056
  <section class="sec">
1276
2057
  <h4>Configuração do acordeão</h4>
1277
2058
  <div class="grid two">
@@ -1302,42 +2083,47 @@ class PraxisExpansionConfigEditor {
1302
2083
  <button mat-stroked-button color="primary" (click)="addPanel()"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar painel</button>
1303
2084
  </div>
1304
2085
  </section>
1305
-
1306
- <section class="sec" *ngFor="let p of (config?.panels || []); let i = index">
1307
- <div class="panel-row">
1308
- <strong>{{ p.title || ('Painel ' + (i + 1)) }}</strong>
1309
- <span class="grow"></span>
1310
- <button mat-button (click)="move(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon>Subir</button>
1311
- <button mat-button (click)="move(i, 1)" [disabled]="i>=(config!.panels!.length-1)"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon>Descer</button>
1312
- <button mat-button color="warn" (click)="removePanel(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon>Remover</button>
1313
- </div>
1314
- <div class="grid two">
1315
- <mat-form-field appearance="outline"><mat-label>ID</mat-label>
2086
+
2087
+ @for (p of (config?.panels || []); track p; let i = $index) {
2088
+ <section class="sec">
2089
+ <div class="panel-row">
2090
+ <strong>{{ p.title || ('Painel ' + (i + 1)) }}</strong>
2091
+ <span class="grow"></span>
2092
+ <button mat-button (click)="move(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon>Subir</button>
2093
+ <button mat-button (click)="move(i, 1)" [disabled]="i>=(config!.panels!.length-1)"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon>Descer</button>
2094
+ <button mat-button color="warn" (click)="removePanel(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon>Remover</button>
2095
+ </div>
2096
+ <div class="grid two">
2097
+ <mat-form-field appearance="outline"><mat-label>ID</mat-label>
1316
2098
  <input matInput [value]="p.id || ''" (input)="setPanel(i, 'id', $any($event.target).value)" />
1317
2099
  </mat-form-field>
1318
2100
  <mat-form-field appearance="outline"><mat-label>Título</mat-label>
1319
- <input matInput [value]="p.title || ''" (input)="setPanel(i, 'title', $any($event.target).value)" />
1320
- </mat-form-field>
1321
- <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
1322
- <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
1323
- </mat-form-field>
1324
- <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
1325
- <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
1326
- </mat-form-field>
1327
- <mat-form-field appearance="outline"><mat-label>Altura expandida</mat-label>
1328
- <input matInput placeholder="64px" [value]="p.expandedHeight || ''" (input)="setPanel(i, 'expandedHeight', $any($event.target).value)" />
1329
- </mat-form-field>
1330
- <div class="row wrap">
1331
- <mat-slide-toggle [checked]="p.disabled || false" (change)="setPanel(i, 'disabled', $event.checked)">Desativado</mat-slide-toggle>
1332
- <mat-slide-toggle [checked]="p.expanded || false" (change)="setPanel(i, 'expanded', $event.checked)">Expandido</mat-slide-toggle>
1333
- <mat-slide-toggle [checked]="p.hideToggle || false" (change)="setPanel(i, 'hideToggle', $event.checked)">Ocultar toggle</mat-slide-toggle>
1334
- </div>
1335
- </div>
1336
- </section>
2101
+ <input matInput [value]="p.title || ''" (input)="setPanel(i, 'title', $any($event.target).value)" />
2102
+ </mat-form-field>
2103
+ <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
2104
+ <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
2105
+ </mat-form-field>
2106
+ <mat-form-field appearance="outline"><mat-label>Ícone</mat-label>
2107
+ <input matInput [value]="p.icon || ''" (input)="setPanel(i, 'icon', $any($event.target).value)" placeholder="info" />
2108
+ </mat-form-field>
2109
+ <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
2110
+ <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
2111
+ </mat-form-field>
2112
+ <mat-form-field appearance="outline"><mat-label>Altura expandida</mat-label>
2113
+ <input matInput placeholder="64px" [value]="p.expandedHeight || ''" (input)="setPanel(i, 'expandedHeight', $any($event.target).value)" />
2114
+ </mat-form-field>
2115
+ <div class="row wrap">
2116
+ <mat-slide-toggle [checked]="p.disabled || false" (change)="setPanel(i, 'disabled', $event.checked)">Desativado</mat-slide-toggle>
2117
+ <mat-slide-toggle [checked]="p.expanded || false" (change)="setPanel(i, 'expanded', $event.checked)">Expandido</mat-slide-toggle>
2118
+ <mat-slide-toggle [checked]="p.hideToggle || false" (change)="setPanel(i, 'hideToggle', $event.checked)">Ocultar toggle</mat-slide-toggle>
2119
+ </div>
1337
2120
  </div>
1338
- `, isInline: true, styles: [":host{display:block;color:var(--md-sys-color-on-surface)}.pdx-expansion-editor{display:grid;gap:16px}.sec{border:1px solid var(--md-sys-color-outline-variant);padding:12px;border-radius:10px;background:var(--md-sys-color-surface-container-lowest);display:grid;gap:12px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.row.wrap{flex-wrap:wrap}.panel-row{display:flex;gap:8px;align-items:center;width:100%}.grow{flex:1 1 auto}.grid{display:grid;gap:10px;align-items:center}.grid.two{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.full{width:100%}.error{color:var(--md-sys-color-error);font-size:12px}.pdx-expansion-editor .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2121
+ </section>
2122
+ }
2123
+ </div>
2124
+ `, isInline: true, styles: [":host{display:block;color:var(--md-sys-color-on-surface)}.pdx-expansion-editor{display:grid;gap:16px}.sec{border:1px solid var(--md-sys-color-outline-variant);padding:12px;border-radius:10px;background:var(--md-sys-color-surface-container-lowest);display:grid;gap:12px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.row.wrap{flex-wrap:wrap}.panel-row{display:flex;gap:8px;align-items:center;width:100%}.grow{flex:1 1 auto}.grid{display:grid;gap:10px;align-items:center}.grid.two{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.full{width:100%}.error{color:var(--md-sys-color-error);font-size:12px}.pdx-expansion-editor .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1339
2125
  }
1340
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansionConfigEditor, decorators: [{
2126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisExpansionConfigEditor, decorators: [{
1341
2127
  type: Component,
1342
2128
  args: [{ selector: 'praxis-expansion-config-editor', standalone: true, imports: [
1343
2129
  CommonModule,
@@ -1349,7 +2135,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1349
2135
  MatTooltipModule,
1350
2136
  PraxisIconDirective,
1351
2137
  ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1352
- <div class="pdx-expansion-editor">
2138
+ <div class="pdx-expansion-editor" data-testid="expansion-config-editor">
1353
2139
  <section class="sec">
1354
2140
  <h4>Aparência</h4>
1355
2141
  <div class="grid two">
@@ -1367,7 +2153,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1367
2153
  </select>
1368
2154
  </mat-form-field>
1369
2155
  </div>
1370
-
2156
+
1371
2157
  <h5>Container</h5>
1372
2158
  <div class="grid two">
1373
2159
  <mat-form-field appearance="outline">
@@ -1409,7 +2195,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1409
2195
  <input matInput [value]="config?.appearance?.container?.panelGap || ''" (input)="setContainer('panelGap', $any($event.target).value)" placeholder="12px" />
1410
2196
  </mat-form-field>
1411
2197
  </div>
1412
-
2198
+
1413
2199
  <h5>Cabeçalho</h5>
1414
2200
  <div class="grid two">
1415
2201
  <mat-form-field appearance="outline">
@@ -1469,7 +2255,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1469
2255
  <input matInput [value]="config?.appearance?.header?.toggleIconSize || ''" (input)="setHeader('toggleIconSize', $any($event.target).value)" placeholder="12px" />
1470
2256
  </mat-form-field>
1471
2257
  </div>
1472
-
2258
+
1473
2259
  <h5>Estados</h5>
1474
2260
  <div class="grid two">
1475
2261
  <mat-form-field appearance="outline">
@@ -1515,13 +2301,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1515
2301
  type="button"
1516
2302
  [matTooltip]="'Use tokens M3, por exemplo: var(--md-sys-color-primary)'"
1517
2303
  matTooltipPosition="above"
1518
- >
2304
+ >
1519
2305
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
1520
2306
  </button>
1521
2307
  </mat-form-field>
1522
- <div class="error" *ngIf="tokensError">{{ tokensError }}</div>
2308
+ @if (tokensError) {
2309
+ <div class="error">{{ tokensError }}</div>
2310
+ }
1523
2311
  </section>
1524
-
2312
+
1525
2313
  <section class="sec">
1526
2314
  <h4>Configuração do acordeão</h4>
1527
2315
  <div class="grid two">
@@ -1552,40 +2340,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1552
2340
  <button mat-stroked-button color="primary" (click)="addPanel()"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar painel</button>
1553
2341
  </div>
1554
2342
  </section>
1555
-
1556
- <section class="sec" *ngFor="let p of (config?.panels || []); let i = index">
1557
- <div class="panel-row">
1558
- <strong>{{ p.title || ('Painel ' + (i + 1)) }}</strong>
1559
- <span class="grow"></span>
1560
- <button mat-button (click)="move(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon>Subir</button>
1561
- <button mat-button (click)="move(i, 1)" [disabled]="i>=(config!.panels!.length-1)"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon>Descer</button>
1562
- <button mat-button color="warn" (click)="removePanel(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon>Remover</button>
1563
- </div>
1564
- <div class="grid two">
1565
- <mat-form-field appearance="outline"><mat-label>ID</mat-label>
2343
+
2344
+ @for (p of (config?.panels || []); track p; let i = $index) {
2345
+ <section class="sec">
2346
+ <div class="panel-row">
2347
+ <strong>{{ p.title || ('Painel ' + (i + 1)) }}</strong>
2348
+ <span class="grow"></span>
2349
+ <button mat-button (click)="move(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon>Subir</button>
2350
+ <button mat-button (click)="move(i, 1)" [disabled]="i>=(config!.panels!.length-1)"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon>Descer</button>
2351
+ <button mat-button color="warn" (click)="removePanel(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon>Remover</button>
2352
+ </div>
2353
+ <div class="grid two">
2354
+ <mat-form-field appearance="outline"><mat-label>ID</mat-label>
1566
2355
  <input matInput [value]="p.id || ''" (input)="setPanel(i, 'id', $any($event.target).value)" />
1567
2356
  </mat-form-field>
1568
2357
  <mat-form-field appearance="outline"><mat-label>Título</mat-label>
1569
- <input matInput [value]="p.title || ''" (input)="setPanel(i, 'title', $any($event.target).value)" />
1570
- </mat-form-field>
1571
- <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
1572
- <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
1573
- </mat-form-field>
1574
- <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
1575
- <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
1576
- </mat-form-field>
1577
- <mat-form-field appearance="outline"><mat-label>Altura expandida</mat-label>
1578
- <input matInput placeholder="64px" [value]="p.expandedHeight || ''" (input)="setPanel(i, 'expandedHeight', $any($event.target).value)" />
1579
- </mat-form-field>
1580
- <div class="row wrap">
1581
- <mat-slide-toggle [checked]="p.disabled || false" (change)="setPanel(i, 'disabled', $event.checked)">Desativado</mat-slide-toggle>
1582
- <mat-slide-toggle [checked]="p.expanded || false" (change)="setPanel(i, 'expanded', $event.checked)">Expandido</mat-slide-toggle>
1583
- <mat-slide-toggle [checked]="p.hideToggle || false" (change)="setPanel(i, 'hideToggle', $event.checked)">Ocultar toggle</mat-slide-toggle>
1584
- </div>
1585
- </div>
1586
- </section>
2358
+ <input matInput [value]="p.title || ''" (input)="setPanel(i, 'title', $any($event.target).value)" />
2359
+ </mat-form-field>
2360
+ <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
2361
+ <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
2362
+ </mat-form-field>
2363
+ <mat-form-field appearance="outline"><mat-label>Ícone</mat-label>
2364
+ <input matInput [value]="p.icon || ''" (input)="setPanel(i, 'icon', $any($event.target).value)" placeholder="info" />
2365
+ </mat-form-field>
2366
+ <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
2367
+ <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
2368
+ </mat-form-field>
2369
+ <mat-form-field appearance="outline"><mat-label>Altura expandida</mat-label>
2370
+ <input matInput placeholder="64px" [value]="p.expandedHeight || ''" (input)="setPanel(i, 'expandedHeight', $any($event.target).value)" />
2371
+ </mat-form-field>
2372
+ <div class="row wrap">
2373
+ <mat-slide-toggle [checked]="p.disabled || false" (change)="setPanel(i, 'disabled', $event.checked)">Desativado</mat-slide-toggle>
2374
+ <mat-slide-toggle [checked]="p.expanded || false" (change)="setPanel(i, 'expanded', $event.checked)">Expandido</mat-slide-toggle>
2375
+ <mat-slide-toggle [checked]="p.hideToggle || false" (change)="setPanel(i, 'hideToggle', $event.checked)">Ocultar toggle</mat-slide-toggle>
2376
+ </div>
2377
+ </div>
2378
+ </section>
2379
+ }
1587
2380
  </div>
1588
- `, styles: [":host{display:block;color:var(--md-sys-color-on-surface)}.pdx-expansion-editor{display:grid;gap:16px}.sec{border:1px solid var(--md-sys-color-outline-variant);padding:12px;border-radius:10px;background:var(--md-sys-color-surface-container-lowest);display:grid;gap:12px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.row.wrap{flex-wrap:wrap}.panel-row{display:flex;gap:8px;align-items:center;width:100%}.grow{flex:1 1 auto}.grid{display:grid;gap:10px;align-items:center}.grid.two{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.full{width:100%}.error{color:var(--md-sys-color-error);font-size:12px}.pdx-expansion-editor .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"] }]
2381
+ `, styles: [":host{display:block;color:var(--md-sys-color-on-surface)}.pdx-expansion-editor{display:grid;gap:16px}.sec{border:1px solid var(--md-sys-color-outline-variant);padding:12px;border-radius:10px;background:var(--md-sys-color-surface-container-lowest);display:grid;gap:12px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.row.wrap{flex-wrap:wrap}.panel-row{display:flex;gap:8px;align-items:center;width:100%}.grow{flex:1 1 auto}.grid{display:grid;gap:10px;align-items:center}.grid.two{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.full{width:100%}.error{color:var(--md-sys-color-error);font-size:12px}.pdx-expansion-editor .mat-mdc-form-field{width:100%;--mdc-outlined-text-field-container-height: 48px;--mdc-outlined-text-field-outline-color: var(--md-sys-color-outline-variant);--mdc-outlined-text-field-hover-outline-color: var(--md-sys-color-outline);--mdc-outlined-text-field-focus-outline-color: var(--md-sys-color-primary);--mdc-outlined-text-field-error-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-focus-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-error-hover-outline-color: var(--md-sys-color-error);--mdc-outlined-text-field-label-text-color: var(--md-sys-color-on-surface-variant);--mdc-outlined-text-field-input-text-color: var(--md-sys-color-on-surface);--mdc-outlined-text-field-supporting-text-color: var(--md-sys-color-on-surface-variant)}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"] }]
1589
2382
  }], ctorParameters: () => [{ type: undefined, decorators: [{
1590
2383
  type: Inject,
1591
2384
  args: [SETTINGS_PANEL_DATA]
@@ -1595,6 +2388,116 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1595
2388
  type: Input
1596
2389
  }] } });
1597
2390
 
2391
+ class PraxisExpansionWidgetConfigEditor {
2392
+ inputs = null;
2393
+ widgetKey;
2394
+ expansionEditor;
2395
+ isDirty$ = new BehaviorSubject(false);
2396
+ isValid$ = new BehaviorSubject(true);
2397
+ isBusy$ = new BehaviorSubject(false);
2398
+ subscription = new Subscription();
2399
+ emptyConfig = {};
2400
+ get config() {
2401
+ return this.inputs?.config ?? this.emptyConfig;
2402
+ }
2403
+ get expansionId() {
2404
+ return this.inputs?.expansionId ?? this.widgetKey;
2405
+ }
2406
+ ngAfterViewInit() {
2407
+ if (!this.expansionEditor) {
2408
+ return;
2409
+ }
2410
+ this.subscription.add(this.expansionEditor.isDirty$.subscribe((value) => this.isDirty$.next(value)));
2411
+ this.subscription.add(this.expansionEditor.isValid$.subscribe((value) => this.isValid$.next(value)));
2412
+ this.subscription.add(this.expansionEditor.isBusy$.subscribe((value) => this.isBusy$.next(value)));
2413
+ }
2414
+ ngOnDestroy() {
2415
+ this.subscription.unsubscribe();
2416
+ }
2417
+ getSettingsValue() {
2418
+ return this.buildValue(this.expansionEditor?.getSettingsValue());
2419
+ }
2420
+ onSave() {
2421
+ return this.buildValue(this.expansionEditor?.getSettingsValue());
2422
+ }
2423
+ reset() {
2424
+ this.expansionEditor?.reset();
2425
+ }
2426
+ buildValue(config) {
2427
+ return {
2428
+ inputs: {
2429
+ ...(this.inputs ?? {}),
2430
+ config: config ?? this.config,
2431
+ ...(this.expansionId ? { expansionId: this.expansionId } : {}),
2432
+ },
2433
+ };
2434
+ }
2435
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisExpansionWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
2436
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.14", 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: `
2437
+ <section data-testid="expansion-widget-config-editor">
2438
+ <praxis-expansion-config-editor
2439
+ #expansionEditor
2440
+ [config]="config"
2441
+ [expansionId]="expansionId"
2442
+ />
2443
+ </section>
2444
+ `, isInline: true, dependencies: [{ kind: "component", type: PraxisExpansionConfigEditor, selector: "praxis-expansion-config-editor", inputs: ["config", "expansionId"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2445
+ }
2446
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisExpansionWidgetConfigEditor, decorators: [{
2447
+ type: Component,
2448
+ args: [{
2449
+ selector: 'praxis-expansion-widget-config-editor',
2450
+ standalone: true,
2451
+ imports: [PraxisExpansionConfigEditor],
2452
+ template: `
2453
+ <section data-testid="expansion-widget-config-editor">
2454
+ <praxis-expansion-config-editor
2455
+ #expansionEditor
2456
+ [config]="config"
2457
+ [expansionId]="expansionId"
2458
+ />
2459
+ </section>
2460
+ `,
2461
+ changeDetection: ChangeDetectionStrategy.OnPush,
2462
+ }]
2463
+ }], propDecorators: { inputs: [{
2464
+ type: Input
2465
+ }], widgetKey: [{
2466
+ type: Input
2467
+ }], expansionEditor: [{
2468
+ type: ViewChild,
2469
+ args: ['expansionEditor']
2470
+ }] } });
2471
+
2472
+ const PRAXIS_EXPANSION_PORTS = [
2473
+ {
2474
+ id: 'config',
2475
+ label: 'Configuracao',
2476
+ direction: 'input',
2477
+ semanticKind: 'config-fragment',
2478
+ schema: {
2479
+ id: 'ExpansionMetadata',
2480
+ kind: 'ts-type',
2481
+ ref: 'ExpansionMetadata',
2482
+ },
2483
+ description: 'Fragmento canonico de configuracao dos paineis e dos widgets internos.',
2484
+ exposure: { public: true, group: 'config' },
2485
+ },
2486
+ {
2487
+ id: 'widgetEvent',
2488
+ label: 'Evento interno de widget',
2489
+ direction: 'output',
2490
+ semanticKind: 'event',
2491
+ schema: {
2492
+ id: 'WidgetEventEnvelope',
2493
+ kind: 'ts-type',
2494
+ ref: 'WidgetEventEnvelope',
2495
+ },
2496
+ cardinality: 'stream',
2497
+ description: 'Bridge composta para transporte de eventos internos. As portas canonicas dos filhos sao resolvidas por component-port + nestedPath.',
2498
+ exposure: { public: true, advanced: true, group: 'composite' },
2499
+ },
2500
+ ];
1598
2501
  const PRAXIS_EXPANSION_COMPONENT_METADATA = {
1599
2502
  id: 'praxis-expansion',
1600
2503
  selector: 'praxis-expansion',
@@ -1602,6 +2505,14 @@ const PRAXIS_EXPANSION_COMPONENT_METADATA = {
1602
2505
  friendlyName: 'Praxis Expansion Panel',
1603
2506
  description: 'Acordeão/Painéis de expansão configuráveis por metadata, com aparência e tokens M3.',
1604
2507
  icon: 'unfold_more',
2508
+ authoringManifestRef: {
2509
+ componentId: 'praxis-expansion',
2510
+ source: 'PRAXIS_EXPANSION_AUTHORING_MANIFEST',
2511
+ },
2512
+ configEditor: {
2513
+ component: PraxisExpansionWidgetConfigEditor,
2514
+ title: 'Configure expansion',
2515
+ },
1605
2516
  inputs: [
1606
2517
  { name: 'config', type: 'ExpansionMetadata', label: 'Configuração', description: 'Configuração JSON (acordeão, aparência e painéis)' },
1607
2518
  { name: 'expansionId', type: 'string', label: 'ID', description: 'Identificador para persistência (obrigatório)' },
@@ -1676,6 +2587,7 @@ const PRAXIS_EXPANSION_COMPONENT_METADATA = {
1676
2587
  ],
1677
2588
  tags: ['widget', 'expansion', 'accordion', 'configurable'],
1678
2589
  lib: '@praxisui/expansion',
2590
+ ports: PRAXIS_EXPANSION_PORTS,
1679
2591
  };
1680
2592
  function providePraxisExpansionMetadata() {
1681
2593
  return {
@@ -1691,6 +2603,315 @@ function providePraxisExpansionDefaults(opts) {
1691
2603
  return { provide: MAT_EXPANSION_PANEL_DEFAULT_OPTIONS, useValue: opts };
1692
2604
  }
1693
2605
 
2606
+ const panelItemSchema = {
2607
+ type: 'object',
2608
+ required: ['id', 'title'],
2609
+ properties: {
2610
+ id: { type: 'string' },
2611
+ title: { type: 'string' },
2612
+ description: { type: 'string' },
2613
+ icon: { type: 'string' },
2614
+ disabled: { type: 'boolean' },
2615
+ expanded: { type: 'boolean' },
2616
+ hideToggle: { type: 'boolean' },
2617
+ collapsedHeight: { type: 'string' },
2618
+ expandedHeight: { type: 'string' },
2619
+ content: { type: 'array', items: { type: 'object' } },
2620
+ widgets: { type: 'array', items: { type: 'object' } },
2621
+ actionButtons: { type: 'array', items: { type: 'object' } },
2622
+ },
2623
+ };
2624
+ const panelPatchSchema = {
2625
+ type: 'object',
2626
+ minProperties: 1,
2627
+ properties: {
2628
+ id: { type: 'string' },
2629
+ title: { type: 'string' },
2630
+ description: { type: 'string' },
2631
+ icon: { type: 'string' },
2632
+ disabled: { type: 'boolean' },
2633
+ expanded: { type: 'boolean' },
2634
+ hideToggle: { type: 'boolean' },
2635
+ collapsedHeight: { type: 'string' },
2636
+ expandedHeight: { type: 'string' },
2637
+ content: { type: 'array', items: { type: 'object' } },
2638
+ widgets: { type: 'array', items: { type: 'object' } },
2639
+ actionButtons: { type: 'array', items: { type: 'object' } },
2640
+ },
2641
+ };
2642
+ const PRAXIS_EXPANSION_AUTHORING_MANIFEST = {
2643
+ schemaVersion: '1.0.0',
2644
+ componentId: 'praxis-expansion',
2645
+ ownerPackage: '@praxisui/expansion',
2646
+ configSchemaId: 'ExpansionMetadata',
2647
+ manifestVersion: '1.0.0',
2648
+ runtimeInputs: [
2649
+ { name: 'config', type: 'ExpansionMetadata', description: 'Canonical accordion and panel configuration.' },
2650
+ { name: 'expansionId', type: 'string', description: 'Stable id used to derive expansion config persistence scope.' },
2651
+ { name: 'componentInstanceId', type: 'string', description: 'Optional instance discriminator for persistence scope.' },
2652
+ { name: 'context', type: 'Record<string, any>', description: 'Context passed to nested widgets.' },
2653
+ { name: 'strictValidation', type: 'boolean', description: 'Controls nested widget validation strictness.' },
2654
+ { name: 'defaultOptions', type: 'MatExpansionPanelDefaultOptions', description: 'Instance-level Material expansion defaults.' },
2655
+ { name: 'enableCustomization', type: 'boolean', description: 'Enables Settings Panel authoring surfaces.' },
2656
+ ],
2657
+ editableTargets: [
2658
+ { kind: 'panel', resolver: 'panel-by-id-or-title', description: 'A panel in config.panels[].' },
2659
+ { kind: 'panelHeader', resolver: 'panel-by-id-or-title', description: 'Header title, description, icon and heights for a panel.' },
2660
+ { kind: 'panelContent', resolver: 'panel-content-by-id', description: 'Lazy panel content hosted through fields, widgets or action buttons.' },
2661
+ { kind: 'expandedState', resolver: 'panel-by-id-or-title', description: 'Panel expanded state and default expanded selection.' },
2662
+ { kind: 'disabledState', resolver: 'panel-by-id-or-title', description: 'Panel disabled state.' },
2663
+ { kind: 'layout', resolver: 'expansion-layout-config', description: 'Accordion display mode, toggle position, density and visual layout.' },
2664
+ { kind: 'behavior', resolver: 'expansion-behavior-config', description: 'Accordion multi-expand and hide-toggle behavior.' },
2665
+ ],
2666
+ operations: [
2667
+ {
2668
+ operationId: 'panel.add',
2669
+ title: 'Add panel',
2670
+ scope: 'global',
2671
+ targetKind: 'panel',
2672
+ target: { kind: 'panel', resolver: 'panels-array', ambiguityPolicy: 'fail', required: false },
2673
+ inputSchema: panelItemSchema,
2674
+ effects: [{ kind: 'append-unique', path: 'panels[]', key: 'id' }],
2675
+ destructive: false,
2676
+ requiresConfirmation: false,
2677
+ validators: ['panel-id-unique', 'panel-order-deterministic', 'panel-content-valid'],
2678
+ affectedPaths: ['panels[]'],
2679
+ submissionImpact: 'config-only',
2680
+ preconditions: ['config-initialized'],
2681
+ },
2682
+ {
2683
+ operationId: 'panel.remove',
2684
+ title: 'Remove panel',
2685
+ scope: 'layout',
2686
+ targetKind: 'panel',
2687
+ target: { kind: 'panel', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2688
+ inputSchema: {
2689
+ type: 'object',
2690
+ properties: {
2691
+ replacementExpandedPanelId: { type: 'string' },
2692
+ },
2693
+ },
2694
+ effects: [{
2695
+ kind: 'compile-domain-patch',
2696
+ handler: 'expansion-panel-remove',
2697
+ handlerContract: {
2698
+ reads: ['accordion.multi', 'panels[]', 'panels[].id', 'panels[].expanded'],
2699
+ writes: ['panels[]', 'panels[].expanded'],
2700
+ identityKeys: ['panels[].id'],
2701
+ inputSchema: {
2702
+ type: 'object',
2703
+ properties: {
2704
+ replacementExpandedPanelId: { type: 'string' },
2705
+ },
2706
+ },
2707
+ failureModes: ['panel-not-found', 'replacement-panel-not-found', 'content-removal-not-confirmed', 'single-expand-conflict'],
2708
+ description: 'Removes the target panel by stable id and applies replacement expanded state when the removed panel was expanded.',
2709
+ },
2710
+ }],
2711
+ destructive: true,
2712
+ requiresConfirmation: true,
2713
+ validators: ['panel-exists', 'default-expanded-removal-safe', 'panel-content-removal-confirmed'],
2714
+ affectedPaths: ['panels[]', 'panels[].expanded'],
2715
+ submissionImpact: 'config-only',
2716
+ preconditions: ['config-initialized', 'target-panel-exists', 'confirmation-collected'],
2717
+ },
2718
+ {
2719
+ operationId: 'panel.title.set',
2720
+ title: 'Set panel title',
2721
+ scope: 'layout',
2722
+ targetKind: 'panelHeader',
2723
+ target: { kind: 'panelHeader', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2724
+ inputSchema: { type: 'object', required: ['title'], properties: { title: { type: 'string' } } },
2725
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2726
+ destructive: false,
2727
+ requiresConfirmation: false,
2728
+ validators: ['panel-exists', 'panel-title-valid'],
2729
+ affectedPaths: ['panels[].title'],
2730
+ submissionImpact: 'config-only',
2731
+ preconditions: ['config-initialized', 'target-panel-exists'],
2732
+ },
2733
+ {
2734
+ operationId: 'panel.description.set',
2735
+ title: 'Set panel description',
2736
+ scope: 'layout',
2737
+ targetKind: 'panelHeader',
2738
+ target: { kind: 'panelHeader', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2739
+ inputSchema: { type: 'object', required: ['description'], properties: { description: { type: 'string' } } },
2740
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2741
+ destructive: false,
2742
+ requiresConfirmation: false,
2743
+ validators: ['panel-exists', 'panel-description-valid'],
2744
+ affectedPaths: ['panels[].description'],
2745
+ submissionImpact: 'config-only',
2746
+ preconditions: ['config-initialized', 'target-panel-exists'],
2747
+ },
2748
+ {
2749
+ operationId: 'panel.icon.set',
2750
+ title: 'Set panel icon',
2751
+ scope: 'layout',
2752
+ targetKind: 'panelHeader',
2753
+ target: { kind: 'panelHeader', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2754
+ inputSchema: { type: 'object', required: ['icon'], properties: { icon: { type: 'string' } } },
2755
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2756
+ destructive: false,
2757
+ requiresConfirmation: false,
2758
+ validators: ['panel-exists', 'panel-icon-valid', 'editor-runtime-round-trip'],
2759
+ affectedPaths: ['panels[].icon'],
2760
+ submissionImpact: 'config-only',
2761
+ preconditions: ['config-initialized', 'target-panel-exists'],
2762
+ },
2763
+ {
2764
+ operationId: 'panel.order.set',
2765
+ title: 'Reorder panels',
2766
+ scope: 'layout',
2767
+ targetKind: 'panel',
2768
+ target: { kind: 'panel', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2769
+ inputSchema: { type: 'object', required: ['beforePanelId'], properties: { beforePanelId: { type: 'string' } } },
2770
+ effects: [{ kind: 'reorder-by-key', path: 'panels[]', key: 'id' }],
2771
+ destructive: false,
2772
+ requiresConfirmation: false,
2773
+ validators: ['panel-exists', 'panel-order-deterministic'],
2774
+ affectedPaths: ['panels[]'],
2775
+ submissionImpact: 'config-only',
2776
+ preconditions: ['config-initialized', 'target-panel-exists'],
2777
+ },
2778
+ {
2779
+ operationId: 'panel.disabled.set',
2780
+ title: 'Set disabled state',
2781
+ scope: 'interaction',
2782
+ targetKind: 'disabledState',
2783
+ target: { kind: 'disabledState', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2784
+ inputSchema: { type: 'object', required: ['disabled'], properties: { disabled: { type: 'boolean' } } },
2785
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2786
+ destructive: false,
2787
+ requiresConfirmation: false,
2788
+ validators: ['panel-exists', 'disabled-expanded-compatible'],
2789
+ affectedPaths: ['panels[].disabled', 'panels[].expanded'],
2790
+ submissionImpact: 'config-only',
2791
+ preconditions: ['config-initialized', 'target-panel-exists'],
2792
+ },
2793
+ {
2794
+ operationId: 'behavior.multiExpand.set',
2795
+ title: 'Set multi-expand behavior',
2796
+ scope: 'interaction',
2797
+ targetKind: 'behavior',
2798
+ target: { kind: 'behavior', resolver: 'expansion-behavior-config', ambiguityPolicy: 'fail', required: true },
2799
+ inputSchema: { type: 'object', required: ['multi'], properties: { multi: { type: 'boolean' } } },
2800
+ effects: [{
2801
+ kind: 'compile-domain-patch',
2802
+ handler: 'expansion-multi-expand-set',
2803
+ handlerContract: {
2804
+ reads: ['accordion.multi', 'panels[]', 'panels[].id', 'panels[].expanded'],
2805
+ writes: ['accordion.multi', 'panels[].expanded'],
2806
+ identityKeys: ['panels[].id'],
2807
+ inputSchema: { type: 'object', required: ['multi'], properties: { multi: { type: 'boolean' } } },
2808
+ failureModes: ['multiple-expanded-panels-conflict', 'panel-id-missing'],
2809
+ description: 'Sets accordion.multi and collapses competing expanded panels when switching to single-expand behavior.',
2810
+ },
2811
+ }],
2812
+ destructive: false,
2813
+ requiresConfirmation: false,
2814
+ validators: ['multi-expand-default-compatible', 'accordion-values-valid', 'editor-runtime-round-trip'],
2815
+ affectedPaths: ['accordion.multi', 'panels[].expanded'],
2816
+ submissionImpact: 'config-only',
2817
+ preconditions: ['config-initialized'],
2818
+ },
2819
+ {
2820
+ operationId: 'behavior.defaultExpanded.set',
2821
+ title: 'Set default expanded panel',
2822
+ scope: 'interaction',
2823
+ targetKind: 'expandedState',
2824
+ target: { kind: 'expandedState', resolver: 'panel-by-id-or-title', ambiguityPolicy: 'fail', required: true },
2825
+ inputSchema: {
2826
+ type: 'object',
2827
+ required: ['expanded'],
2828
+ properties: {
2829
+ expanded: { type: 'boolean' },
2830
+ collapseOthers: { type: 'boolean' },
2831
+ },
2832
+ },
2833
+ effects: [{
2834
+ kind: 'compile-domain-patch',
2835
+ handler: 'expansion-default-expanded-upsert',
2836
+ handlerContract: {
2837
+ reads: ['accordion.multi', 'panels[]', 'panels[].id', 'panels[].expanded', 'panels[].disabled'],
2838
+ writes: ['panels[].expanded'],
2839
+ identityKeys: ['panels[].id'],
2840
+ inputSchema: {
2841
+ type: 'object',
2842
+ required: ['panelId', 'expanded'],
2843
+ properties: {
2844
+ panelId: { type: 'string' },
2845
+ expanded: { type: 'boolean' },
2846
+ collapseOthers: { type: 'boolean' },
2847
+ },
2848
+ },
2849
+ failureModes: ['panel-not-found', 'panel-disabled', 'single-expand-conflict'],
2850
+ description: 'Sets a panel expanded state and collapses competing panels when accordion.multi is false.',
2851
+ },
2852
+ }],
2853
+ validators: ['panel-exists', 'default-expanded-panel-exists', 'multi-expand-default-compatible', 'disabled-expanded-compatible'],
2854
+ destructive: false,
2855
+ requiresConfirmation: false,
2856
+ affectedPaths: ['panels[].expanded', 'accordion.multi'],
2857
+ submissionImpact: 'config-only',
2858
+ preconditions: ['config-initialized', 'target-panel-exists'],
2859
+ },
2860
+ {
2861
+ operationId: 'panel.content.set',
2862
+ title: 'Set panel content',
2863
+ scope: 'layout',
2864
+ targetKind: 'panelContent',
2865
+ target: { kind: 'panelContent', resolver: 'panel-content-by-id', ambiguityPolicy: 'fail', required: true },
2866
+ inputSchema: panelPatchSchema,
2867
+ effects: [{ kind: 'merge-by-key', path: 'panels[]', key: 'id' }],
2868
+ destructive: false,
2869
+ requiresConfirmation: false,
2870
+ validators: ['panel-exists', 'panel-content-valid', 'lazy-content-compatible', 'nested-widget-contract-delegated'],
2871
+ affectedPaths: ['panels[].content', 'panels[].widgets', 'panels[].actionButtons'],
2872
+ submissionImpact: 'affects-schema-backed-data',
2873
+ preconditions: ['config-initialized', 'target-panel-exists'],
2874
+ },
2875
+ ],
2876
+ validators: [
2877
+ { validatorId: 'panel-id-unique', level: 'error', code: 'PEXP001', description: 'Panel ids must be unique and stable within config.panels[].' },
2878
+ { validatorId: 'panel-exists', level: 'error', code: 'PEXP002', description: 'Target panel must exist before applying the operation.' },
2879
+ { validatorId: 'panel-order-deterministic', level: 'error', code: 'PEXP003', description: 'Panel ordering must use stable ids, not transient array index as identity.' },
2880
+ { validatorId: 'panel-title-valid', level: 'error', code: 'PEXP004', description: 'Panel title must be a non-empty text value after localization/domain projection.' },
2881
+ { validatorId: 'panel-description-valid', level: 'warning', code: 'PEXP005', description: 'Panel description should remain plain header-support text and not replace panel content.' },
2882
+ { validatorId: 'panel-icon-valid', level: 'warning', code: 'PEXP006', description: 'Panel icon metadata must remain compatible with PraxisIconDirective and editor round-trip.' },
2883
+ { validatorId: 'panel-content-valid', level: 'error', code: 'PEXP007', description: 'Panel content must remain valid FieldMetadata[], WidgetDefinition[] or action button metadata.' },
2884
+ { validatorId: 'panel-content-removal-confirmed', level: 'error', code: 'PEXP008', description: 'Removing a panel with fields, widgets or action buttons is destructive and requires confirmation.' },
2885
+ { validatorId: 'default-expanded-panel-exists', level: 'error', code: 'PEXP009', description: 'Default expanded state must reference an existing panel id.' },
2886
+ { validatorId: 'default-expanded-removal-safe', level: 'error', code: 'PEXP010', description: 'Removing an expanded panel requires deterministic replacement state or explicit confirmation.' },
2887
+ { 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.' },
2888
+ { validatorId: 'disabled-expanded-compatible', level: 'warning', code: 'PEXP012', description: 'A disabled panel should not be the only expanded/default focus target without explicit intent.' },
2889
+ { validatorId: 'accordion-values-valid', level: 'error', code: 'PEXP013', description: 'Accordion behavior values must match ExpansionMetadata and Angular Material expansion bindings.' },
2890
+ { validatorId: 'lazy-content-compatible', level: 'info', code: 'PEXP015', description: 'Panel content remains lazy through matExpansionPanelContent and should not require eager child runtime state.' },
2891
+ { validatorId: 'nested-widget-contract-delegated', level: 'info', code: 'PEXP016', description: 'Nested widget content remains governed by child component contracts and component-port nestedPath semantics.' },
2892
+ { 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.' },
2893
+ ],
2894
+ roundTripRequirements: [
2895
+ 'Operations must preserve stable panel ids; array index may be used only as a resolver fallback, never as canonical identity.',
2896
+ 'Settings Panel editor, runtime persistence and registry projection must round-trip ExpansionMetadata without losing panel ids, order, icons or expanded state.',
2897
+ 'When accordion.multi is false, authoring must collapse competing panels or fail validation instead of producing multiple default-expanded panels.',
2898
+ 'Lazy panel content remains represented by panels[].content, panels[].widgets and panels[].actionButtons; authoring cannot require eager child component instances.',
2899
+ 'Nested widgets remain delegated through WidgetDefinition and component-port nestedPath semantics instead of being redefined by the expansion contract.',
2900
+ ],
2901
+ examples: [
2902
+ { id: 'add-summary-panel', request: 'Add a summary panel before the audit panel.', operationId: 'panel.add', params: { id: 'summary', title: 'Summary' }, isPositive: true },
2903
+ { id: 'rename-details-panel', request: 'Rename details to Account details.', operationId: 'panel.title.set', target: 'details', params: { title: 'Account details' }, isPositive: true },
2904
+ { id: 'describe-audit-panel', request: 'Set audit panel description to Recent changes.', operationId: 'panel.description.set', target: 'audit', params: { description: 'Recent changes' }, isPositive: true },
2905
+ { id: 'set-panel-icon', request: 'Use the info icon on the summary panel.', operationId: 'panel.icon.set', target: 'summary', params: { icon: 'info' }, isPositive: true },
2906
+ { 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 },
2907
+ { id: 'enable-multi-expand', request: 'Allow multiple panels to stay open.', operationId: 'behavior.multiExpand.set', params: { multi: true }, isPositive: true },
2908
+ { id: 'disable-archive-panel', request: 'Disable the archive panel.', operationId: 'panel.disabled.set', target: 'archive', params: { disabled: true }, isPositive: true },
2909
+ { id: 'reject-missing-default-expanded', request: 'Open the missing panel by default.', operationId: 'behavior.defaultExpanded.set', target: 'missing', params: { expanded: true }, isPositive: false },
2910
+ { id: 'reject-duplicate-panel-id', request: 'Add another panel with id summary.', operationId: 'panel.add', params: { id: 'summary', title: 'Duplicate summary' }, isPositive: false },
2911
+ { id: 'confirm-remove-content-panel', request: 'Remove the details panel that contains widgets.', operationId: 'panel.remove', target: 'details', params: { replacementExpandedPanelId: 'summary' }, isPositive: true },
2912
+ ],
2913
+ };
2914
+
1694
2915
  /*
1695
2916
  * Public API Surface of praxis-expansion
1696
2917
  */
@@ -1699,4 +2920,4 @@ function providePraxisExpansionDefaults(opts) {
1699
2920
  * Generated bundle index. Do not edit.
1700
2921
  */
1701
2922
 
1702
- export { EXPANSION_AI_CAPABILITIES, PRAXIS_EXPANSION_COMPONENT_METADATA, PraxisExpansion, PraxisExpansionConfigEditor, providePraxisExpansionDefaults, providePraxisExpansionMetadata };
2923
+ export { EXPANSION_AI_CAPABILITIES, PRAXIS_EXPANSION_AUTHORING_MANIFEST, PRAXIS_EXPANSION_COMPONENT_METADATA, PraxisExpansion, PraxisExpansionConfigEditor, PraxisExpansionWidgetConfigEditor, providePraxisExpansionDefaults, providePraxisExpansionMetadata };