@praxisui/expansion 1.0.0-beta.7 → 2.0.0-beta.0

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,23 +1,196 @@
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, DestroyRef, ViewChildren, ViewChild, Output, Input, ChangeDetectionStrategy, Component, ENVIRONMENT_INITIALIZER } from '@angular/core';
4
+ import { EventEmitter, inject, ChangeDetectorRef, DestroyRef, ViewChildren, ViewChild, Output, Input, ChangeDetectionStrategy, Component, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
5
+ import { ActivatedRoute } from '@angular/router';
5
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
7
  import * as i2 from '@angular/material/expansion';
7
8
  import { MAT_EXPANSION_PANEL_DEFAULT_OPTIONS, MatExpansionModule, MatExpansionPanel } from '@angular/material/expansion';
9
+ import * as i5 from '@angular/material/form-field';
10
+ import { MatFormFieldModule } from '@angular/material/form-field';
8
11
  import * as i3 from '@angular/material/icon';
9
12
  import { MatIconModule } from '@angular/material/icon';
10
13
  import * as i4 from '@angular/material/button';
11
14
  import { MatButtonModule } from '@angular/material/button';
12
- import { CONFIG_STORAGE, DynamicWidgetLoaderDirective, PraxisIconDirective, ComponentMetadataRegistry } from '@praxisui/core';
13
- import { SettingsPanelService } from '@praxisui/settings-panel';
15
+ import * as i6 from '@angular/material/input';
16
+ import { MatInputModule } from '@angular/material/input';
17
+ import * as i7 from '@angular/material/slide-toggle';
18
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
19
+ import * as i8 from '@angular/material/tooltip';
20
+ import { MatTooltipModule } from '@angular/material/tooltip';
21
+ import { DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';
22
+ import { deepMerge, ASYNC_CONFIG_STORAGE, DynamicFormService, ComponentKeyService, DynamicWidgetLoaderDirective, PraxisIconDirective, ComponentMetadataRegistry } from '@praxisui/core';
23
+ import { SettingsPanelService, SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
24
+ import { BehaviorSubject } from 'rxjs';
25
+ import { take } from 'rxjs/operators';
26
+ import { BaseAiAdapter, PraxisAiAssistantComponent } from '@praxisui/ai';
27
+
28
+ /**
29
+ * Capabilities catalog for Praxis Expansion (ExpansionMetadata).
30
+ * Paths follow ExpansionMetadata shape (patch merged at config root).
31
+ */
32
+ const ENUMS = {
33
+ density: ['compact', 'comfortable', 'spacious'],
34
+ displayMode: ['default', 'flat'],
35
+ togglePosition: ['before', 'after'],
36
+ widgetId: ['praxis-dynamic-form', 'praxis-list', 'pdx-material-searchable-select', 'praxis-files-upload'],
37
+ expansionTokenKey: ['header-background-color'],
38
+ };
39
+ const TOKEN_CAPS = ENUMS.expansionTokenKey.map((t) => ({
40
+ path: `appearance.tokens.${t}`,
41
+ category: 'appearance',
42
+ valueKind: 'string',
43
+ description: `Token ${t} (CSS color or var).`,
44
+ safetyNotes: 'Aceita apenas hex color ou var(--token).',
45
+ }));
46
+ const CAPS = [
47
+ { path: 'appearance', category: 'appearance', valueKind: 'object', description: 'Appearance settings.' },
48
+ { path: 'appearance.density', category: 'appearance', valueKind: 'enum', allowedValues: ENUMS.density, description: 'Density preset.' },
49
+ { path: 'appearance.themeClass', category: 'appearance', valueKind: 'string', description: 'Theme CSS class for root.' },
50
+ { path: 'appearance.customCss', category: 'appearance', valueKind: 'string', description: 'Custom CSS injected into component.', safetyNotes: 'CSS is injected as raw string.' },
51
+ { path: 'appearance.tokens', category: 'appearance', valueKind: 'object', description: 'Token map for theme overrides.' },
52
+ { path: 'appearance.tokens.[token]', category: 'appearance', valueKind: 'string', allowedValues: ENUMS.expansionTokenKey, description: 'Token value (CSS or var).', safetyNotes: 'Only keys supported by styleCss().' },
53
+ ...TOKEN_CAPS,
54
+ { path: 'accordion', category: 'accordion', valueKind: 'object', description: 'Accordion behavior settings.' },
55
+ { path: 'accordion.multi', category: 'accordion', valueKind: 'boolean', description: 'Allow multiple panels open.' },
56
+ { path: 'accordion.displayMode', category: 'accordion', valueKind: 'enum', allowedValues: ENUMS.displayMode, description: 'Accordion display mode.' },
57
+ { path: 'accordion.togglePosition', category: 'accordion', valueKind: 'enum', allowedValues: ENUMS.togglePosition, description: 'Toggle icon position.' },
58
+ { path: 'accordion.hideToggle', category: 'accordion', valueKind: 'boolean', description: 'Hide toggle icon.' },
59
+ { path: 'accordion.id', category: 'accordion', valueKind: 'string', description: 'Accordion id.' },
60
+ { path: 'panels', category: 'panels', valueKind: 'array', description: 'Panel list.' },
61
+ { path: 'panels[]', category: 'panels', valueKind: 'object', description: 'Panel definition.' },
62
+ { path: 'panels[].id', category: 'panels', valueKind: 'string', description: 'Panel id.' },
63
+ { path: 'panels[].title', category: 'panels', valueKind: 'string', description: 'Panel title.' },
64
+ { path: 'panels[].description', category: 'panels', valueKind: 'string', description: 'Panel description.' },
65
+ { path: 'panels[].disabled', category: 'panels', valueKind: 'boolean', description: 'Disable panel.' },
66
+ { path: 'panels[].expanded', category: 'panels', valueKind: 'boolean', description: 'Expanded state.' },
67
+ { path: 'panels[].hideToggle', category: 'panels', valueKind: 'boolean', description: 'Hide toggle icon for panel.' },
68
+ { path: 'panels[].collapsedHeight', category: 'panels', valueKind: 'string', description: 'Collapsed header height (e.g., 48px).' },
69
+ { path: 'panels[].expandedHeight', category: 'panels', valueKind: 'string', description: 'Expanded header height (e.g., 64px).' },
70
+ { path: 'panels[].content', category: 'panels', valueKind: 'array', description: 'Dynamic field metadata list (legacy).' },
71
+ { path: 'panels[].content[]', category: 'panels', valueKind: 'object', description: 'Dynamic field metadata item.', safetyNotes: 'Use FieldMetadata base when no controlType catalog exists.' },
72
+ { path: 'panels[].widgets', category: 'panels', valueKind: 'array', description: 'WidgetDefinition list for content.' },
73
+ { path: 'panels[].widgets[]', category: 'panels', valueKind: 'object', description: 'WidgetDefinition item.' },
74
+ { path: 'panels[].widgets[].id', category: 'panels', valueKind: 'enum', allowedValues: ENUMS.widgetId, description: 'Component registry id for widget.' },
75
+ { path: 'panels[].widgets[].inputs', category: 'panels', valueKind: 'object', description: 'Inputs bound into the widget instance.' },
76
+ { path: 'panels[].widgets[].outputs', category: 'panels', valueKind: 'object', description: 'Outputs mapped to actions.' },
77
+ { path: 'panels[].widgets[].outputs.[outputName]', category: 'panels', valueKind: 'object', description: 'Output action mapping (ActionDefinition or "emit").' },
78
+ { path: 'panels[].widgets[].bindingOrder', category: 'panels', valueKind: 'array', description: 'Explicit input binding order.' },
79
+ { path: 'panels[].widgets[].bindingOrder[]', category: 'panels', valueKind: 'string', description: 'Input name applied first.' },
80
+ { path: 'panels[].actionButtons', category: 'actions', valueKind: 'array', description: 'Action buttons at panel footer.' },
81
+ { path: 'panels[].actionButtons[]', category: 'actions', valueKind: 'object', description: 'Action button definition.' },
82
+ { path: 'panels[].actionButtons[].icon', category: 'actions', valueKind: 'string', description: 'Action icon.' },
83
+ { path: 'panels[].actionButtons[].label', category: 'actions', valueKind: 'string', description: 'Action label.' },
84
+ { path: 'panels[].actionButtons[].action', category: 'actions', valueKind: 'string', description: 'Action identifier payload.' },
85
+ ];
86
+ const EXPANSION_AI_CAPABILITIES = {
87
+ version: 'v1.1',
88
+ enums: ENUMS,
89
+ targets: [
90
+ 'praxis-expansion',
91
+ 'mat-accordion',
92
+ 'expansion-panel',
93
+ 'praxis-expansion-config-editor',
94
+ ],
95
+ notes: [
96
+ 'panels[] should be merged by id or title to avoid replacing all panels.',
97
+ 'appearance.customCss is injected into a <style> tag; avoid unsafe CSS from untrusted sources.',
98
+ 'appearance.tokens supports limited keys used by styleCss().',
99
+ 'panels[].widgets[].id must be a ComponentMetadataRegistry id.',
100
+ 'If a field controlType lacks a dedicated catalog, treat it as FieldMetadata base.',
101
+ ],
102
+ capabilities: CAPS,
103
+ };
104
+
105
+ class ExpansionAiAdapter extends BaseAiAdapter {
106
+ expansion;
107
+ componentName = 'Praxis Expansion';
108
+ constructor(expansion) {
109
+ super();
110
+ this.expansion = expansion;
111
+ }
112
+ getCurrentConfig() {
113
+ const current = this.expansion.config || {};
114
+ return this.cloneConfig(current);
115
+ }
116
+ getCapabilities() {
117
+ return EXPANSION_AI_CAPABILITIES.capabilities;
118
+ }
119
+ getRuntimeState() {
120
+ const panels = this.expansion.config?.panels || [];
121
+ return {
122
+ panelsCount: panels.length,
123
+ expandedCount: panels.filter((p) => p.expanded).length,
124
+ multi: this.expansion.config?.accordion?.multi ?? false,
125
+ };
126
+ }
127
+ createSnapshot() {
128
+ return this.getCurrentConfig();
129
+ }
130
+ async restoreSnapshot(snapshot) {
131
+ if (!snapshot)
132
+ return;
133
+ this.applyConfig(this.cloneConfig(snapshot));
134
+ }
135
+ async applyPatch(patch) {
136
+ try {
137
+ const current = this.getCurrentConfig();
138
+ const next = this.smartMergeExpansionConfig(current, patch);
139
+ this.applyConfig(next);
140
+ return { success: true };
141
+ }
142
+ catch (error) {
143
+ const message = error instanceof Error ? error.message : 'Unknown error applying patch';
144
+ return { success: false, error: message };
145
+ }
146
+ }
147
+ applyConfig(config) {
148
+ const apply = this.expansion.applyConfigFromAdapter;
149
+ if (typeof apply === 'function') {
150
+ apply.call(this.expansion, config);
151
+ return;
152
+ }
153
+ this.expansion.config = config;
154
+ }
155
+ smartMergeExpansionConfig(base, patch) {
156
+ const result = deepMerge(base, patch);
157
+ if (patch.panels && Array.isArray(patch.panels)) {
158
+ const merged = this.mergeByKey(base.panels || [], patch.panels, (p) => p.id || p.title || '');
159
+ result.panels = merged;
160
+ }
161
+ return result;
162
+ }
163
+ mergeByKey(baseArr, patchArr, keyFn) {
164
+ const merged = baseArr.map((orig) => {
165
+ const key = keyFn(orig);
166
+ const match = key ? patchArr.find((p) => keyFn(p) === key) : undefined;
167
+ return match ? deepMerge(orig, match) : orig;
168
+ });
169
+ patchArr.forEach((item) => {
170
+ const key = keyFn(item);
171
+ if (!key || !baseArr.find((o) => keyFn(o) === key)) {
172
+ merged.push(item);
173
+ }
174
+ });
175
+ return merged;
176
+ }
177
+ cloneConfig(config) {
178
+ try {
179
+ return structuredClone(config);
180
+ }
181
+ catch {
182
+ return JSON.parse(JSON.stringify(config));
183
+ }
184
+ }
185
+ }
14
186
 
15
187
  class PraxisExpansion {
16
188
  config;
17
189
  expansionId;
190
+ componentInstanceId;
18
191
  context = {};
19
192
  strictValidation = true;
20
- editModeEnabled = false;
193
+ enableCustomization = false;
21
194
  defaultOptions;
22
195
  opened = new EventEmitter();
23
196
  closed = new EventEmitter();
@@ -27,26 +200,202 @@ class PraxisExpansion {
27
200
  destroyed = new EventEmitter();
28
201
  widgetEvent = new EventEmitter();
29
202
  settings = inject(SettingsPanelService);
30
- storage = inject(CONFIG_STORAGE);
203
+ storage = inject(ASYNC_CONFIG_STORAGE);
204
+ cdr = inject(ChangeDetectorRef);
31
205
  destroyRef = inject(DestroyRef);
206
+ dynamicForm = inject(DynamicFormService);
207
+ componentKeys = inject(ComponentKeyService);
208
+ route = (() => { try {
209
+ return inject(ActivatedRoute);
210
+ }
211
+ catch {
212
+ return undefined;
213
+ } })();
214
+ warnedMissingId = false;
215
+ panelForms = new Map();
216
+ aiAdapter = new ExpansionAiAdapter(this);
32
217
  accordionRef;
33
218
  panels;
34
219
  injectedDefaults = inject(MAT_EXPANSION_PANEL_DEFAULT_OPTIONS, { optional: true });
35
220
  hasMultiple = () => (this.config?.panels?.length || 0) > 1;
36
221
  single = () => (this.config?.panels && this.config.panels[0]) || null;
222
+ ngOnInit() {
223
+ const key = this.storageKey();
224
+ if (!key)
225
+ return;
226
+ this.storage.loadConfig(key).pipe(take(1)).subscribe((stored) => {
227
+ if (!stored)
228
+ return;
229
+ this.config = stored;
230
+ this.buildPanelForms();
231
+ this.cdr.markForCheck();
232
+ });
233
+ }
234
+ ngOnChanges(changes) {
235
+ if ('config' in changes) {
236
+ this.buildPanelForms();
237
+ this.persistConfig(this.config);
238
+ }
239
+ }
37
240
  styleCss() {
38
241
  const t = this.config?.appearance?.tokens;
39
- if (!t)
242
+ const appearance = this.config?.appearance;
243
+ if (!t && !appearance?.container && !appearance?.header && !appearance?.states)
40
244
  return null;
41
245
  const scope = `.praxis-expansion-root[data-expansion-id="${(this.expansionId || 'default').replace(/"/g, '')}"]`;
42
246
  const rules = [];
43
- const push = (selector, decls) => { rules.push(`${scope} ${selector}{${decls.join('')}}`); };
44
- // Example: header background color
45
- const bg = t['header-background-color'];
46
- if (bg) {
47
- const safe = /^#[0-9a-fA-F]{3,8}$/.test(bg) || /^var\(--[\w-]+\)$/.test(bg);
48
- if (safe) {
49
- push(' .mat-expansion-panel-header', [`background-color:${bg}!important;`]);
247
+ const push = (selector, decls) => {
248
+ rules.push(`${scope} ${selector}{${decls.join('')}}`);
249
+ };
250
+ const isSafeColor = (value) => /^#[0-9a-fA-F]{3,8}$/.test(value)
251
+ || /^var\(--[\w-]+\)$/.test(value)
252
+ || /^(rgb|rgba|hsl|hsla)\(/.test(value)
253
+ || /^color-mix\(/.test(value);
254
+ if (t) {
255
+ const panelBg = t['panel-background-color'];
256
+ if (panelBg && isSafeColor(panelBg)) {
257
+ push(' .mat-expansion-panel', [`background:${panelBg}!important;`]);
258
+ }
259
+ const panelBorder = t['panel-border-color'];
260
+ if (panelBorder && isSafeColor(panelBorder)) {
261
+ push(' .mat-expansion-panel', [`border-color:${panelBorder}!important;`]);
262
+ }
263
+ const headerBg = t['header-background-color'];
264
+ if (headerBg && isSafeColor(headerBg)) {
265
+ push(' .mat-expansion-panel-header', [`background-color:${headerBg}!important;`]);
266
+ }
267
+ const headerText = t['header-text-color'];
268
+ if (headerText && isSafeColor(headerText)) {
269
+ push(' .mat-expansion-panel-header', [`color:${headerText}!important;`]);
270
+ }
271
+ const descText = t['description-text-color'];
272
+ if (descText && isSafeColor(descText)) {
273
+ push(' .mat-expansion-panel-header-description', [`color:${descText}!important;`]);
274
+ }
275
+ const bodyText = t['body-text-color'];
276
+ if (bodyText && isSafeColor(bodyText)) {
277
+ push(' .mat-expansion-panel-body', [`color:${bodyText}!important;`]);
278
+ }
279
+ const actionBorder = t['action-row-border-color'];
280
+ if (actionBorder && isSafeColor(actionBorder)) {
281
+ push(' .mat-action-row', [`border-top-color:${actionBorder}!important;`]);
282
+ }
283
+ }
284
+ if (appearance?.container) {
285
+ const container = appearance.container;
286
+ const decls = [];
287
+ if (container.background)
288
+ decls.push(`background:${container.background};`);
289
+ if (container.borderColor)
290
+ decls.push(`border-color:${container.borderColor};`);
291
+ if (container.borderWidth)
292
+ decls.push(`border-width:${container.borderWidth};`);
293
+ if (container.borderStyle)
294
+ decls.push(`border-style:${container.borderStyle};`);
295
+ if (container.borderRadius)
296
+ decls.push(`border-radius:${container.borderRadius};`);
297
+ if (container.shadow)
298
+ decls.push(`box-shadow:${container.shadow};`);
299
+ if (container.padding)
300
+ decls.push(`padding:${container.padding};`);
301
+ if (decls.length) {
302
+ push('', decls);
303
+ }
304
+ if (container.panelGap) {
305
+ push(' .mat-expansion-panel:not(:last-child)', [`margin-bottom:${container.panelGap};`]);
306
+ }
307
+ }
308
+ if (appearance?.header) {
309
+ const header = appearance.header;
310
+ const headerDecls = [];
311
+ if (header.background)
312
+ headerDecls.push(`background:${header.background}!important;`);
313
+ if (header.textColor)
314
+ headerDecls.push(`color:${header.textColor}!important;`);
315
+ if (header.height)
316
+ headerDecls.push(`min-height:${header.height};`);
317
+ if (header.padding)
318
+ headerDecls.push(`padding:${header.padding};`);
319
+ if (header.gap)
320
+ headerDecls.push(`column-gap:${header.gap};`);
321
+ if (headerDecls.length)
322
+ push(' .mat-expansion-panel-header', headerDecls);
323
+ if (header.fontFamily) {
324
+ push(' .mat-expansion-panel-header-title', [`font-family:${header.fontFamily};`]);
325
+ push(' .mat-expansion-panel-header-description', [`font-family:${header.fontFamily};`]);
326
+ }
327
+ const titleDecls = [];
328
+ if (header.titleColor)
329
+ titleDecls.push(`color:${header.titleColor}!important;`);
330
+ if (header.titleSize)
331
+ titleDecls.push(`font-size:${header.titleSize};`);
332
+ if (header.titleWeight)
333
+ titleDecls.push(`font-weight:${header.titleWeight};`);
334
+ if (titleDecls.length)
335
+ push(' .mat-expansion-panel-header-title', titleDecls);
336
+ const descDecls = [];
337
+ if (header.descriptionColor)
338
+ descDecls.push(`color:${header.descriptionColor}!important;`);
339
+ if (header.descriptionSize)
340
+ descDecls.push(`font-size:${header.descriptionSize};`);
341
+ if (header.descriptionWeight)
342
+ descDecls.push(`font-weight:${header.descriptionWeight};`);
343
+ if (descDecls.length)
344
+ push(' .mat-expansion-panel-header-description', descDecls);
345
+ const iconDecls = [];
346
+ if (header.toggleIconColor)
347
+ iconDecls.push(`border-color:${header.toggleIconColor}!important;`);
348
+ if (iconDecls.length)
349
+ push(' .mat-expansion-indicator::after', iconDecls);
350
+ if (header.toggleIconSize) {
351
+ push(' .mat-expansion-indicator::after', [
352
+ `width:${header.toggleIconSize};`,
353
+ `height:${header.toggleIconSize};`,
354
+ ]);
355
+ }
356
+ }
357
+ if (appearance?.states) {
358
+ const states = appearance.states;
359
+ if (states.hover) {
360
+ const decls = [];
361
+ if (states.hover.headerBackground)
362
+ decls.push(`background:${states.hover.headerBackground}!important;`);
363
+ if (states.hover.textColor)
364
+ decls.push(`color:${states.hover.textColor}!important;`);
365
+ if (decls.length)
366
+ push(' .mat-expansion-panel-header:hover', decls);
367
+ }
368
+ if (states.expanded) {
369
+ const decls = [];
370
+ if (states.expanded.headerBackground)
371
+ decls.push(`background:${states.expanded.headerBackground}!important;`);
372
+ if (states.expanded.textColor)
373
+ decls.push(`color:${states.expanded.textColor}!important;`);
374
+ if (decls.length)
375
+ push(' .mat-expansion-panel.mat-expanded > .mat-expansion-panel-header', decls);
376
+ if (states.expanded.accentBarColor) {
377
+ push(' .mat-expansion-panel.mat-expanded > .mat-expansion-panel-header', [
378
+ 'position:relative;',
379
+ ]);
380
+ push(' .mat-expansion-panel.mat-expanded > .mat-expansion-panel-header::before', [
381
+ 'content:"";',
382
+ 'position:absolute;',
383
+ 'left:0;',
384
+ 'top:0;',
385
+ 'bottom:0;',
386
+ 'width:4px;',
387
+ `background:${states.expanded.accentBarColor};`,
388
+ ]);
389
+ }
390
+ }
391
+ if (states.disabled) {
392
+ const decls = [];
393
+ if (states.disabled.headerBackground)
394
+ decls.push(`background:${states.disabled.headerBackground}!important;`);
395
+ if (states.disabled.textColor)
396
+ decls.push(`color:${states.disabled.textColor}!important;`);
397
+ if (decls.length)
398
+ push(' .mat-expansion-panel.mat-expansion-panel-disabled .mat-expansion-panel-header', decls);
50
399
  }
51
400
  }
52
401
  if (rules.length === 0)
@@ -101,9 +450,29 @@ class PraxisExpansion {
101
450
  const base = p?.id || `${this.expansionId || 'exp'}-panel-${index + 1}`;
102
451
  return base.replace(/[^a-zA-Z0-9_-]/g, '-');
103
452
  }
453
+ panelFormFor(panel, index) {
454
+ const key = this.panelKey(panel, index);
455
+ const existing = this.panelForms.get(key);
456
+ if (existing)
457
+ return existing;
458
+ const fields = panel.content ?? [];
459
+ const form = this.dynamicForm.createFormGroupFromMetadata(fields);
460
+ this.panelForms.set(key, form);
461
+ return form;
462
+ }
463
+ applyConfigFromAdapter(next) {
464
+ this.config = next;
465
+ this.buildPanelForms();
466
+ const key = this.storageKey();
467
+ if (key) {
468
+ this.storage.saveConfig(key, this.config).pipe(take(1)).subscribe({ error: () => { } });
469
+ }
470
+ this.cdr.markForCheck();
471
+ }
104
472
  openEditor() {
473
+ const key = this.storageKey() || this.expansionId || 'default';
105
474
  const ref = this.settings.open({
106
- id: `praxis-expansion-editor:${this.expansionId || 'default'}`,
475
+ id: `praxis-expansion-editor:${key}`,
107
476
  title: 'Configurar Painéis',
108
477
  content: { component: PraxisExpansionConfigEditor, inputs: { config: this.config, expansionId: this.expansionId } },
109
478
  });
@@ -111,21 +480,71 @@ class PraxisExpansion {
111
480
  const nextCfg = value?.config || value;
112
481
  if (nextCfg) {
113
482
  this.config = { ...nextCfg };
114
- if (this.expansionId)
115
- this.storage.saveConfig(this.storageKey(), this.config);
483
+ this.buildPanelForms();
484
+ const storageKey = this.storageKey();
485
+ if (storageKey) {
486
+ this.storage.saveConfig(storageKey, this.config).pipe(take(1)).subscribe({ error: () => { } });
487
+ }
488
+ this.cdr.markForCheck();
116
489
  }
117
490
  };
118
491
  ref.applied$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(apply);
119
492
  ref.saved$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(apply);
120
493
  }
121
494
  storageKey() {
122
- return `expansion:${this.expansionId}`;
495
+ const id = this.componentKeyId();
496
+ return id ? `expansion:${id}` : null;
497
+ }
498
+ persistConfig(config) {
499
+ if (!config)
500
+ return;
501
+ const key = this.storageKey();
502
+ if (!key)
503
+ return;
504
+ this.storage.saveConfig(key, config).pipe(take(1)).subscribe({ error: () => { } });
123
505
  }
124
506
  emitAction(p, index, action) {
125
507
  this.widgetEvent.emit({ panelId: p.id, panelIndex: index, sourceId: 'praxis-expansion', output: 'action', payload: { action } });
126
508
  }
127
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisExpansion, deps: [], target: i0.ɵɵFactoryTarget.Component });
128
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: PraxisExpansion, isStandalone: true, selector: "praxis-expansion", inputs: { config: "config", expansionId: "expansionId", context: "context", strictValidation: "strictValidation", editModeEnabled: "editModeEnabled", 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 }], ngImport: i0, template: `
509
+ panelKey(panel, index) {
510
+ return panel.id ?? `panel-${index}`;
511
+ }
512
+ buildPanelForms() {
513
+ const previous = new Map(this.panelForms);
514
+ this.panelForms.clear();
515
+ const panels = this.config?.panels ?? [];
516
+ panels.forEach((panel, index) => {
517
+ const fields = panel.content ?? [];
518
+ if (!fields.length)
519
+ return;
520
+ const key = this.panelKey(panel, index);
521
+ const form = this.dynamicForm.createFormGroupFromMetadata(fields);
522
+ const previousValues = previous.get(key)?.getRawValue?.() ?? {};
523
+ form.patchValue(previousValues);
524
+ this.panelForms.set(key, form);
525
+ });
526
+ }
527
+ componentKeyId() {
528
+ const key = this.componentKeys.buildComponentId({
529
+ componentType: 'praxis-expansion',
530
+ componentId: this.expansionId,
531
+ instanceKey: this.componentInstanceId,
532
+ componentRef: this,
533
+ route: this.route,
534
+ requireComponentId: true,
535
+ });
536
+ if (!key)
537
+ this.warnMissingId();
538
+ return key;
539
+ }
540
+ warnMissingId() {
541
+ if (this.warnedMissingId)
542
+ return;
543
+ this.warnedMissingId = true;
544
+ console.warn('[PraxisExpansion] expansionId is required for config persistence.');
545
+ }
546
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansion, deps: [], target: i0.ɵɵFactoryTarget.Component });
547
+ 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: `
129
548
  <div
130
549
  class="praxis-expansion-root"
131
550
  [class.density-compact]="config?.appearance?.density === 'compact'"
@@ -137,6 +556,12 @@ class PraxisExpansion {
137
556
  <style *ngIf="config?.appearance?.customCss" [innerHTML]="config?.appearance?.customCss"></style>
138
557
  <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
139
558
 
559
+ @if (enableCustomization) {
560
+ <div class="expansion-ai-assistant">
561
+ <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
562
+ </div>
563
+ }
564
+
140
565
  @if (hasMultiple()) {
141
566
  <mat-accordion
142
567
  #accordion
@@ -170,6 +595,14 @@ class PraxisExpansion {
170
595
  </mat-expansion-panel-header>
171
596
 
172
597
  <ng-template matExpansionPanelContent>
598
+ @if (p.content?.length) {
599
+ <ng-container
600
+ dynamicFieldLoader
601
+ [fields]="p.content ?? []"
602
+ [formGroup]="panelFormFor(p, i)"
603
+ ></ng-container>
604
+ }
605
+
173
606
  @if (p.widgets?.length) {
174
607
  @for (w of p.widgets; let wi = $index; track wi) {
175
608
  <ng-container
@@ -219,6 +652,14 @@ class PraxisExpansion {
219
652
  </mat-expansion-panel-header>
220
653
 
221
654
  <ng-template matExpansionPanelContent>
655
+ @if (p.content?.length) {
656
+ <ng-container
657
+ dynamicFieldLoader
658
+ [fields]="p.content ?? []"
659
+ [formGroup]="panelFormFor(p, 0)"
660
+ ></ng-container>
661
+ }
662
+
222
663
  @if (p.widgets?.length) {
223
664
  @for (w of p.widgets; let wi = $index; track wi) {
224
665
  <ng-container
@@ -245,7 +686,7 @@ class PraxisExpansion {
245
686
  } @else {
246
687
  <div class="praxis-expansion-empty">
247
688
  <span>Nenhum painel configurado</span>
248
- <button mat-stroked-button color="primary" *ngIf="editModeEnabled" (click)="openEditor()">
689
+ <button mat-stroked-button color="primary" *ngIf="enableCustomization" (click)="openEditor()">
249
690
  <mat-icon [praxisIcon]="'tune'"></mat-icon> Configurar
250
691
  </button>
251
692
  </div>
@@ -254,7 +695,7 @@ class PraxisExpansion {
254
695
  </div>
255
696
  <!-- Edit button -->
256
697
  <button
257
- *ngIf="editModeEnabled"
698
+ *ngIf="enableCustomization"
258
699
  mat-fab
259
700
  class="edit-fab"
260
701
  aria-label="Editar painéis"
@@ -262,17 +703,19 @@ class PraxisExpansion {
262
703
  >
263
704
  <mat-icon fontIcon="edit"></mat-icon>
264
705
  </button>
265
- `, isInline: true, styles: [":host{display:block;position:relative}.praxis-expansion-root{display:block}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px;opacity:.8}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}\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: DynamicWidgetLoaderDirective, selector: "[dynamicWidgetLoader]", inputs: ["dynamicWidgetLoader", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
706
+ `, 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", "context", "strictValidation", "autoWireOutputs"], outputs: ["widgetEvent"], 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 });
266
707
  }
267
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisExpansion, decorators: [{
708
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansion, decorators: [{
268
709
  type: Component,
269
710
  args: [{ selector: 'praxis-expansion', standalone: true, imports: [
270
711
  CommonModule,
271
712
  MatExpansionModule,
272
713
  MatIconModule,
273
714
  MatButtonModule,
715
+ DynamicFieldLoaderDirective,
274
716
  DynamicWidgetLoaderDirective,
275
717
  PraxisIconDirective,
718
+ PraxisAiAssistantComponent,
276
719
  ], template: `
277
720
  <div
278
721
  class="praxis-expansion-root"
@@ -285,6 +728,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
285
728
  <style *ngIf="config?.appearance?.customCss" [innerHTML]="config?.appearance?.customCss"></style>
286
729
  <style *ngIf="styleCss() as s" [innerHTML]="s"></style>
287
730
 
731
+ @if (enableCustomization) {
732
+ <div class="expansion-ai-assistant">
733
+ <praxis-ai-assistant [adapter]="aiAdapter"></praxis-ai-assistant>
734
+ </div>
735
+ }
736
+
288
737
  @if (hasMultiple()) {
289
738
  <mat-accordion
290
739
  #accordion
@@ -318,6 +767,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
318
767
  </mat-expansion-panel-header>
319
768
 
320
769
  <ng-template matExpansionPanelContent>
770
+ @if (p.content?.length) {
771
+ <ng-container
772
+ dynamicFieldLoader
773
+ [fields]="p.content ?? []"
774
+ [formGroup]="panelFormFor(p, i)"
775
+ ></ng-container>
776
+ }
777
+
321
778
  @if (p.widgets?.length) {
322
779
  @for (w of p.widgets; let wi = $index; track wi) {
323
780
  <ng-container
@@ -367,6 +824,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
367
824
  </mat-expansion-panel-header>
368
825
 
369
826
  <ng-template matExpansionPanelContent>
827
+ @if (p.content?.length) {
828
+ <ng-container
829
+ dynamicFieldLoader
830
+ [fields]="p.content ?? []"
831
+ [formGroup]="panelFormFor(p, 0)"
832
+ ></ng-container>
833
+ }
834
+
370
835
  @if (p.widgets?.length) {
371
836
  @for (w of p.widgets; let wi = $index; track wi) {
372
837
  <ng-container
@@ -393,7 +858,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
393
858
  } @else {
394
859
  <div class="praxis-expansion-empty">
395
860
  <span>Nenhum painel configurado</span>
396
- <button mat-stroked-button color="primary" *ngIf="editModeEnabled" (click)="openEditor()">
861
+ <button mat-stroked-button color="primary" *ngIf="enableCustomization" (click)="openEditor()">
397
862
  <mat-icon [praxisIcon]="'tune'"></mat-icon> Configurar
398
863
  </button>
399
864
  </div>
@@ -402,7 +867,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
402
867
  </div>
403
868
  <!-- Edit button -->
404
869
  <button
405
- *ngIf="editModeEnabled"
870
+ *ngIf="enableCustomization"
406
871
  mat-fab
407
872
  class="edit-fab"
408
873
  aria-label="Editar painéis"
@@ -410,16 +875,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
410
875
  >
411
876
  <mat-icon fontIcon="edit"></mat-icon>
412
877
  </button>
413
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;position:relative}.praxis-expansion-root{display:block}.praxis-expansion-empty{display:flex;gap:8px;align-items:center;padding:8px;opacity:.8}.edit-fab{position:absolute;right:12px;bottom:12px;z-index:2}\n"] }]
878
+ `, 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"] }]
414
879
  }], propDecorators: { config: [{
415
880
  type: Input
416
881
  }], expansionId: [{
882
+ type: Input,
883
+ args: [{ required: true }]
884
+ }], componentInstanceId: [{
417
885
  type: Input
418
886
  }], context: [{
419
887
  type: Input
420
888
  }], strictValidation: [{
421
889
  type: Input
422
- }], editModeEnabled: [{
890
+ }], enableCustomization: [{
423
891
  type: Input
424
892
  }], defaultOptions: [{
425
893
  type: Input
@@ -448,15 +916,130 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
448
916
  class PraxisExpansionConfigEditor {
449
917
  config;
450
918
  expansionId;
919
+ tokensError = '';
920
+ isDirty$ = new BehaviorSubject(false);
921
+ isValid$ = new BehaviorSubject(true);
922
+ isBusy$ = new BehaviorSubject(false);
923
+ initialJson = '';
924
+ hasInitializedSnapshot = false;
925
+ constructor(injected) {
926
+ const injectedConfig = injected?.config;
927
+ if (injectedConfig) {
928
+ this.config = injectedConfig;
929
+ }
930
+ }
931
+ ngOnChanges(changes) {
932
+ if (changes['config'] && this.config && !this.hasInitializedSnapshot) {
933
+ this.captureInitialSnapshot(this.config);
934
+ }
935
+ }
936
+ getSettingsValue() {
937
+ return this.config;
938
+ }
939
+ reset() {
940
+ if (!this.initialJson)
941
+ return;
942
+ try {
943
+ this.config = JSON.parse(this.initialJson);
944
+ this.tokensError = '';
945
+ this.isValid$.next(true);
946
+ this.isDirty$.next(false);
947
+ }
948
+ catch { }
949
+ }
950
+ stringifyTokens(tokens) {
951
+ if (!tokens || Object.keys(tokens).length === 0)
952
+ return '';
953
+ try {
954
+ return JSON.stringify(tokens, null, 2);
955
+ }
956
+ catch {
957
+ return '';
958
+ }
959
+ }
960
+ setAppearance(key, value) {
961
+ const ap = { ...(this.config?.appearance || {}) };
962
+ if (value === '' || value === null || typeof value === 'undefined') {
963
+ delete ap[key];
964
+ }
965
+ else {
966
+ ap[key] = value;
967
+ }
968
+ this.setConfig({ ...(this.config || {}), appearance: ap });
969
+ }
970
+ setTokens(text) {
971
+ const raw = (text || '').trim();
972
+ if (!raw) {
973
+ this.tokensError = '';
974
+ this.setAppearance('tokens', undefined);
975
+ this.isValid$.next(true);
976
+ return;
977
+ }
978
+ try {
979
+ const parsed = JSON.parse(raw);
980
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
981
+ this.tokensError = 'Formato inválido: use um objeto JSON.';
982
+ this.isValid$.next(false);
983
+ return;
984
+ }
985
+ this.tokensError = '';
986
+ this.isValid$.next(true);
987
+ this.setAppearance('tokens', parsed);
988
+ }
989
+ catch (err) {
990
+ this.tokensError = err?.message || 'JSON inválido.';
991
+ this.isValid$.next(false);
992
+ }
993
+ }
994
+ setContainer(key, value) {
995
+ const container = { ...(this.config?.appearance?.container || {}) };
996
+ if (value === '' || value === null || typeof value === 'undefined') {
997
+ delete container[key];
998
+ }
999
+ else {
1000
+ container[key] = value;
1001
+ }
1002
+ const next = Object.keys(container).length ? container : undefined;
1003
+ this.setAppearance('container', next);
1004
+ }
1005
+ setHeader(key, value) {
1006
+ const header = { ...(this.config?.appearance?.header || {}) };
1007
+ if (value === '' || value === null || typeof value === 'undefined') {
1008
+ delete header[key];
1009
+ }
1010
+ else {
1011
+ header[key] = value;
1012
+ }
1013
+ const next = Object.keys(header).length ? header : undefined;
1014
+ this.setAppearance('header', next);
1015
+ }
1016
+ setState(state, key, value) {
1017
+ const current = { ...(this.config?.appearance?.states || {}) };
1018
+ const stateObj = { ...(current[state] || {}) };
1019
+ if (value === '' || value === null || typeof value === 'undefined') {
1020
+ delete stateObj[key];
1021
+ }
1022
+ else {
1023
+ stateObj[key] = value;
1024
+ }
1025
+ if (Object.keys(stateObj).length) {
1026
+ current[state] = stateObj;
1027
+ }
1028
+ else {
1029
+ delete current[state];
1030
+ }
1031
+ const next = Object.keys(current).length ? current : undefined;
1032
+ this.setAppearance('states', next);
1033
+ }
451
1034
  addPanel() {
452
1035
  const next = { ...(this.config || {}), panels: [...(this.config?.panels || []), { title: 'Novo painel' }] };
453
- this.config = next;
1036
+ this.setConfig(next);
454
1037
  }
455
1038
  removePanel(index) {
456
1039
  if (!this.config?.panels)
457
1040
  return;
458
1041
  const next = { ...this.config, panels: this.config.panels.filter((_, i) => i !== index) };
459
- this.config = next;
1042
+ this.setConfig(next);
460
1043
  }
461
1044
  move(index, delta) {
462
1045
  if (!this.config?.panels)
@@ -467,48 +1050,241 @@ class PraxisExpansionConfigEditor {
467
1050
  return;
468
1051
  const [m] = arr.splice(index, 1);
469
1052
  arr.splice(to, 0, m);
470
- this.config = { ...(this.config || {}), panels: arr };
1053
+ this.setConfig({ ...(this.config || {}), panels: arr });
471
1054
  }
472
1055
  setAccordion(key, value) {
473
1056
  const acc = { ...(this.config?.accordion || {}) };
474
1057
  acc[key] = value;
475
- this.config = { ...(this.config || {}), accordion: acc };
1058
+ this.setConfig({ ...(this.config || {}), accordion: acc });
476
1059
  }
477
1060
  setPanel(index, key, value) {
478
1061
  if (!this.config?.panels)
479
1062
  return;
480
1063
  const arr = this.config.panels.map((p, i) => i === index ? { ...p, [key]: value } : p);
481
- this.config = { ...(this.config || {}), panels: arr };
1064
+ this.setConfig({ ...(this.config || {}), panels: arr });
1065
+ }
1066
+ setConfig(next) {
1067
+ this.config = next;
1068
+ if (!this.hasInitializedSnapshot) {
1069
+ this.captureInitialSnapshot(next);
1070
+ return;
1071
+ }
1072
+ this.updateDirtyState();
482
1073
  }
483
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisExpansionConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
484
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: PraxisExpansionConfigEditor, isStandalone: true, selector: "praxis-expansion-config-editor", inputs: { config: "config", expansionId: "expansionId" }, ngImport: i0, template: `
1074
+ captureInitialSnapshot(cfg) {
1075
+ this.initialJson = JSON.stringify(cfg || {});
1076
+ this.hasInitializedSnapshot = true;
1077
+ this.isDirty$.next(false);
1078
+ }
1079
+ updateDirtyState() {
1080
+ if (!this.hasInitializedSnapshot)
1081
+ return;
1082
+ const current = JSON.stringify(this.config || {});
1083
+ this.isDirty$.next(current !== this.initialJson);
1084
+ }
1085
+ 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 });
1086
+ 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: `
485
1087
  <div class="pdx-expansion-editor">
486
1088
  <section class="sec">
487
- <h4>Configuração do Acordeão</h4>
488
- <div class="grid">
489
- <label>
490
- <span>ID</span>
491
- <input type="text" [value]="config?.accordion?.id || ''" (input)="setAccordion('id', $any($event.target).value)" />
492
- </label>
493
- <label>
494
- <span>Display Mode</span>
495
- <select [value]="config?.accordion?.displayMode || 'default'" (change)="setAccordion('displayMode', $any($event.target).value)">
496
- <option value="default">default</option>
497
- <option value="flat">flat</option>
1089
+ <h4>Aparência</h4>
1090
+ <div class="grid two">
1091
+ <mat-form-field appearance="outline">
1092
+ <mat-label>Classe de tema</mat-label>
1093
+ <input matInput [value]="config?.appearance?.themeClass || ''" (input)="setAppearance('themeClass', $any($event.target).value)" placeholder="ex.: expansion-accent" />
1094
+ </mat-form-field>
1095
+ <mat-form-field appearance="outline">
1096
+ <mat-label>Densidade</mat-label>
1097
+ <select matNativeControl [value]="config?.appearance?.density || ''" (change)="setAppearance('density', $any($event.target).value || undefined)">
1098
+ <option value="">Padrão</option>
1099
+ <option value="compact">Compacta</option>
1100
+ <option value="comfortable">Confortável</option>
1101
+ <option value="spacious">Espaçosa</option>
498
1102
  </select>
499
- </label>
500
- <label>
501
- <span>Toggle Position</span>
502
- <select [value]="config?.accordion?.togglePosition || 'after'" (change)="setAccordion('togglePosition', $any($event.target).value)">
503
- <option value="after">after</option>
504
- <option value="before">before</option>
1103
+ </mat-form-field>
1104
+ </div>
1105
+
1106
+ <h5>Container</h5>
1107
+ <div class="grid two">
1108
+ <mat-form-field appearance="outline">
1109
+ <mat-label>Background</mat-label>
1110
+ <input matInput [value]="config?.appearance?.container?.background || ''" (input)="setContainer('background', $any($event.target).value)" placeholder="var(--md-sys-color-surface)" />
1111
+ </mat-form-field>
1112
+ <mat-form-field appearance="outline">
1113
+ <mat-label>Padding</mat-label>
1114
+ <input matInput [value]="config?.appearance?.container?.padding || ''" (input)="setContainer('padding', $any($event.target).value)" placeholder="0" />
1115
+ </mat-form-field>
1116
+ <mat-form-field appearance="outline">
1117
+ <mat-label>Cor da borda</mat-label>
1118
+ <input matInput [value]="config?.appearance?.container?.borderColor || ''" (input)="setContainer('borderColor', $any($event.target).value)" placeholder="var(--md-sys-color-outline-variant)" />
1119
+ </mat-form-field>
1120
+ <mat-form-field appearance="outline">
1121
+ <mat-label>Espessura da borda</mat-label>
1122
+ <input matInput [value]="config?.appearance?.container?.borderWidth || ''" (input)="setContainer('borderWidth', $any($event.target).value)" placeholder="1px" />
1123
+ </mat-form-field>
1124
+ <mat-form-field appearance="outline">
1125
+ <mat-label>Estilo da borda</mat-label>
1126
+ <select matNativeControl [value]="config?.appearance?.container?.borderStyle || ''" (change)="setContainer('borderStyle', $any($event.target).value || undefined)">
1127
+ <option value="">Padrão</option>
1128
+ <option value="solid">Solid</option>
1129
+ <option value="dashed">Dashed</option>
1130
+ <option value="dotted">Dotted</option>
1131
+ <option value="none">None</option>
505
1132
  </select>
506
- </label>
507
- <label class="chk"><input type="checkbox" [checked]="config?.accordion?.multi || false" (change)="setAccordion('multi', $any($event.target).checked)"/> Multi</label>
508
- <label class="chk"><input type="checkbox" [checked]="config?.accordion?.hideToggle || false" (change)="setAccordion('hideToggle', $any($event.target).checked)"/> Hide Toggle</label>
1133
+ </mat-form-field>
1134
+ <mat-form-field appearance="outline">
1135
+ <mat-label>Raio da borda</mat-label>
1136
+ <input matInput [value]="config?.appearance?.container?.borderRadius || ''" (input)="setContainer('borderRadius', $any($event.target).value)" placeholder="12px" />
1137
+ </mat-form-field>
1138
+ <mat-form-field appearance="outline">
1139
+ <mat-label>Sombra</mat-label>
1140
+ <input matInput [value]="config?.appearance?.container?.shadow || ''" (input)="setContainer('shadow', $any($event.target).value)" placeholder="var(--md-sys-color-shadow)" />
1141
+ </mat-form-field>
1142
+ <mat-form-field appearance="outline">
1143
+ <mat-label>Espaço entre painéis</mat-label>
1144
+ <input matInput [value]="config?.appearance?.container?.panelGap || ''" (input)="setContainer('panelGap', $any($event.target).value)" placeholder="12px" />
1145
+ </mat-form-field>
1146
+ </div>
1147
+
1148
+ <h5>Cabeçalho</h5>
1149
+ <div class="grid two">
1150
+ <mat-form-field appearance="outline">
1151
+ <mat-label>Background</mat-label>
1152
+ <input matInput [value]="config?.appearance?.header?.background || ''" (input)="setHeader('background', $any($event.target).value)" placeholder="var(--md-sys-color-surface-container)" />
1153
+ </mat-form-field>
1154
+ <mat-form-field appearance="outline">
1155
+ <mat-label>Cor do texto</mat-label>
1156
+ <input matInput [value]="config?.appearance?.header?.textColor || ''" (input)="setHeader('textColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface)" />
1157
+ </mat-form-field>
1158
+ <mat-form-field appearance="outline">
1159
+ <mat-label>Cor do título</mat-label>
1160
+ <input matInput [value]="config?.appearance?.header?.titleColor || ''" (input)="setHeader('titleColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface)" />
1161
+ </mat-form-field>
1162
+ <mat-form-field appearance="outline">
1163
+ <mat-label>Cor da descrição</mat-label>
1164
+ <input matInput [value]="config?.appearance?.header?.descriptionColor || ''" (input)="setHeader('descriptionColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface-variant)" />
1165
+ </mat-form-field>
1166
+ <mat-form-field appearance="outline">
1167
+ <mat-label>Altura</mat-label>
1168
+ <input matInput [value]="config?.appearance?.header?.height || ''" (input)="setHeader('height', $any($event.target).value)" placeholder="48px" />
1169
+ </mat-form-field>
1170
+ <mat-form-field appearance="outline">
1171
+ <mat-label>Padding</mat-label>
1172
+ <input matInput [value]="config?.appearance?.header?.padding || ''" (input)="setHeader('padding', $any($event.target).value)" placeholder="0 16px" />
1173
+ </mat-form-field>
1174
+ <mat-form-field appearance="outline">
1175
+ <mat-label>Gap</mat-label>
1176
+ <input matInput [value]="config?.appearance?.header?.gap || ''" (input)="setHeader('gap', $any($event.target).value)" placeholder="12px" />
1177
+ </mat-form-field>
1178
+ <mat-form-field appearance="outline">
1179
+ <mat-label>Font family</mat-label>
1180
+ <input matInput [value]="config?.appearance?.header?.fontFamily || ''" (input)="setHeader('fontFamily', $any($event.target).value)" placeholder="inherit" />
1181
+ </mat-form-field>
1182
+ <mat-form-field appearance="outline">
1183
+ <mat-label>Tamanho do título</mat-label>
1184
+ <input matInput [value]="config?.appearance?.header?.titleSize || ''" (input)="setHeader('titleSize', $any($event.target).value)" placeholder="14px" />
1185
+ </mat-form-field>
1186
+ <mat-form-field appearance="outline">
1187
+ <mat-label>Peso do título</mat-label>
1188
+ <input matInput [value]="config?.appearance?.header?.titleWeight || ''" (input)="setHeader('titleWeight', $any($event.target).value)" placeholder="600" />
1189
+ </mat-form-field>
1190
+ <mat-form-field appearance="outline">
1191
+ <mat-label>Tamanho da descrição</mat-label>
1192
+ <input matInput [value]="config?.appearance?.header?.descriptionSize || ''" (input)="setHeader('descriptionSize', $any($event.target).value)" placeholder="12px" />
1193
+ </mat-form-field>
1194
+ <mat-form-field appearance="outline">
1195
+ <mat-label>Peso da descrição</mat-label>
1196
+ <input matInput [value]="config?.appearance?.header?.descriptionWeight || ''" (input)="setHeader('descriptionWeight', $any($event.target).value)" placeholder="400" />
1197
+ </mat-form-field>
1198
+ <mat-form-field appearance="outline">
1199
+ <mat-label>Cor do ícone toggle</mat-label>
1200
+ <input matInput [value]="config?.appearance?.header?.toggleIconColor || ''" (input)="setHeader('toggleIconColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface-variant)" />
1201
+ </mat-form-field>
1202
+ <mat-form-field appearance="outline">
1203
+ <mat-label>Tamanho do ícone toggle</mat-label>
1204
+ <input matInput [value]="config?.appearance?.header?.toggleIconSize || ''" (input)="setHeader('toggleIconSize', $any($event.target).value)" placeholder="12px" />
1205
+ </mat-form-field>
1206
+ </div>
1207
+
1208
+ <h5>Estados</h5>
1209
+ <div class="grid two">
1210
+ <mat-form-field appearance="outline">
1211
+ <mat-label>Hover - background</mat-label>
1212
+ <input matInput [value]="config?.appearance?.states?.hover?.headerBackground || ''" (input)="setState('hover', 'headerBackground', $any($event.target).value)" placeholder="var(--md-sys-color-surface-container-high)" />
1213
+ </mat-form-field>
1214
+ <mat-form-field appearance="outline">
1215
+ <mat-label>Hover - texto</mat-label>
1216
+ <input matInput [value]="config?.appearance?.states?.hover?.textColor || ''" (input)="setState('hover', 'textColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface)" />
1217
+ </mat-form-field>
1218
+ <mat-form-field appearance="outline">
1219
+ <mat-label>Expandido - background</mat-label>
1220
+ <input matInput [value]="config?.appearance?.states?.expanded?.headerBackground || ''" (input)="setState('expanded', 'headerBackground', $any($event.target).value)" placeholder="var(--md-sys-color-surface-container)" />
1221
+ </mat-form-field>
1222
+ <mat-form-field appearance="outline">
1223
+ <mat-label>Expandido - texto</mat-label>
1224
+ <input matInput [value]="config?.appearance?.states?.expanded?.textColor || ''" (input)="setState('expanded', 'textColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface)" />
1225
+ </mat-form-field>
1226
+ <mat-form-field appearance="outline">
1227
+ <mat-label>Expandido - accent bar</mat-label>
1228
+ <input matInput [value]="config?.appearance?.states?.expanded?.accentBarColor || ''" (input)="setState('expanded', 'accentBarColor', $any($event.target).value)" placeholder="var(--md-sys-color-primary)" />
1229
+ </mat-form-field>
1230
+ <mat-form-field appearance="outline">
1231
+ <mat-label>Desabilitado - background</mat-label>
1232
+ <input matInput [value]="config?.appearance?.states?.disabled?.headerBackground || ''" (input)="setState('disabled', 'headerBackground', $any($event.target).value)" placeholder="var(--md-sys-color-surface-container)" />
1233
+ </mat-form-field>
1234
+ <mat-form-field appearance="outline">
1235
+ <mat-label>Desabilitado - texto</mat-label>
1236
+ <input matInput [value]="config?.appearance?.states?.disabled?.textColor || ''" (input)="setState('disabled', 'textColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface-variant)" />
1237
+ </mat-form-field>
1238
+ </div>
1239
+ <mat-form-field appearance="outline" class="full">
1240
+ <mat-label>CSS personalizado</mat-label>
1241
+ <textarea matInput rows="4" [value]="config?.appearance?.customCss || ''" (input)="setAppearance('customCss', $any($event.target).value)" placeholder=".praxis-expansion-root .mat-expansion-panel { }"></textarea>
1242
+ </mat-form-field>
1243
+ <mat-form-field appearance="outline" class="full">
1244
+ <mat-label>Tokens (JSON)</mat-label>
1245
+ <textarea matInput rows="4" [value]="stringifyTokens(config?.appearance?.tokens)" (input)="setTokens($any($event.target).value)" placeholder='{"header-background-color":"var(--md-sys-color-surface-container)"}'></textarea>
1246
+ <button
1247
+ mat-icon-button
1248
+ matSuffix
1249
+ class="help-icon-button"
1250
+ type="button"
1251
+ [matTooltip]="'Use tokens M3, por exemplo: var(--md-sys-color-primary)'"
1252
+ matTooltipPosition="above"
1253
+ >
1254
+ <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
1255
+ </button>
1256
+ </mat-form-field>
1257
+ <div class="error" *ngIf="tokensError">{{ tokensError }}</div>
1258
+ </section>
1259
+
1260
+ <section class="sec">
1261
+ <h4>Configuração do acordeão</h4>
1262
+ <div class="grid two">
1263
+ <mat-form-field appearance="outline">
1264
+ <mat-label>ID</mat-label>
1265
+ <input matInput [value]="config?.accordion?.id || ''" (input)="setAccordion('id', $any($event.target).value)" />
1266
+ </mat-form-field>
1267
+ <mat-form-field appearance="outline">
1268
+ <mat-label>Modo de exibição</mat-label>
1269
+ <select matNativeControl [value]="config?.accordion?.displayMode || 'default'" (change)="setAccordion('displayMode', $any($event.target).value)">
1270
+ <option value="default">Padrão</option>
1271
+ <option value="flat">Flat</option>
1272
+ </select>
1273
+ </mat-form-field>
1274
+ <mat-form-field appearance="outline">
1275
+ <mat-label>Posição do toggle</mat-label>
1276
+ <select matNativeControl [value]="config?.accordion?.togglePosition || 'after'" (change)="setAccordion('togglePosition', $any($event.target).value)">
1277
+ <option value="after">Depois</option>
1278
+ <option value="before">Antes</option>
1279
+ </select>
1280
+ </mat-form-field>
1281
+ <div class="row wrap">
1282
+ <mat-slide-toggle [checked]="config?.accordion?.multi || false" (change)="setAccordion('multi', $event.checked)">Múltiplos abertos</mat-slide-toggle>
1283
+ <mat-slide-toggle [checked]="config?.accordion?.hideToggle || false" (change)="setAccordion('hideToggle', $event.checked)">Ocultar toggle</mat-slide-toggle>
1284
+ </div>
509
1285
  </div>
510
1286
  <div class="row">
511
- <button mat-stroked-button color="primary" (click)="addPanel()"><mat-icon [praxisIcon]="'add'"></mat-icon> Adicionar painel</button>
1287
+ <button mat-stroked-button color="primary" (click)="addPanel()"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar painel</button>
512
1288
  </div>
513
1289
  </section>
514
1290
 
@@ -516,54 +1292,249 @@ class PraxisExpansionConfigEditor {
516
1292
  <div class="panel-row">
517
1293
  <strong>{{ p.title || ('Painel ' + (i + 1)) }}</strong>
518
1294
  <span class="grow"></span>
519
- <button mat-button (click)="move(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon> Mover</button>
520
- <button mat-button (click)="move(i, 1)" [disabled]="i>=(config!.panels!.length-1)"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon> Mover</button>
521
- <button mat-button color="warn" (click)="removePanel(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon> Remover</button>
1295
+ <button mat-button (click)="move(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon>Subir</button>
1296
+ <button mat-button (click)="move(i, 1)" [disabled]="i>=(config!.panels!.length-1)"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon>Descer</button>
1297
+ <button mat-button color="warn" (click)="removePanel(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon>Remover</button>
522
1298
  </div>
523
- <div class="grid">
524
- <label><span>Id</span><input type="text" [value]="p.id || ''" (input)="setPanel(i, 'id', $any($event.target).value)" /></label>
525
- <label><span>Título</span><input type="text" [value]="p.title || ''" (input)="setPanel(i, 'title', $any($event.target).value)" /></label>
526
- <label><span>Descrição</span><input type="text" [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" /></label>
527
- <label><span>Collapsed Height</span><input type="text" placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" /></label>
528
- <label><span>Expanded Height</span><input type="text" placeholder="64px" [value]="p.expandedHeight || ''" (input)="setPanel(i, 'expandedHeight', $any($event.target).value)" /></label>
529
- <label class="chk"><input type="checkbox" [checked]="p.disabled || false" (change)="setPanel(i, 'disabled', $any($event.target).checked)"/> Disabled</label>
530
- <label class="chk"><input type="checkbox" [checked]="p.expanded || false" (change)="setPanel(i, 'expanded', $any($event.target).checked)"/> Expanded</label>
531
- <label class="chk"><input type="checkbox" [checked]="p.hideToggle || false" (change)="setPanel(i, 'hideToggle', $any($event.target).checked)"/> Hide Toggle</label>
1299
+ <div class="grid two">
1300
+ <mat-form-field appearance="outline"><mat-label>ID</mat-label>
1301
+ <input matInput [value]="p.id || ''" (input)="setPanel(i, 'id', $any($event.target).value)" />
1302
+ </mat-form-field>
1303
+ <mat-form-field appearance="outline"><mat-label>Título</mat-label>
1304
+ <input matInput [value]="p.title || ''" (input)="setPanel(i, 'title', $any($event.target).value)" />
1305
+ </mat-form-field>
1306
+ <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
1307
+ <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
1308
+ </mat-form-field>
1309
+ <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
1310
+ <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
1311
+ </mat-form-field>
1312
+ <mat-form-field appearance="outline"><mat-label>Altura expandida</mat-label>
1313
+ <input matInput placeholder="64px" [value]="p.expandedHeight || ''" (input)="setPanel(i, 'expandedHeight', $any($event.target).value)" />
1314
+ </mat-form-field>
1315
+ <div class="row wrap">
1316
+ <mat-slide-toggle [checked]="p.disabled || false" (change)="setPanel(i, 'disabled', $event.checked)">Desativado</mat-slide-toggle>
1317
+ <mat-slide-toggle [checked]="p.expanded || false" (change)="setPanel(i, 'expanded', $event.checked)">Expandido</mat-slide-toggle>
1318
+ <mat-slide-toggle [checked]="p.hideToggle || false" (change)="setPanel(i, 'hideToggle', $event.checked)">Ocultar toggle</mat-slide-toggle>
1319
+ </div>
532
1320
  </div>
533
1321
  </section>
534
1322
  </div>
535
- `, isInline: true, styles: [".pdx-expansion-editor{display:grid;gap:16px}.sec{border:1px dashed rgba(0,0,0,.12);padding:12px;border-radius:8px}.row{display:flex;gap:8px;align-items:center}.panel-row{display:flex;gap:8px;align-items:center;width:100%}.grow{flex:1 1 auto}.grid{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:8px;align-items:center}label{display:flex;flex-direction:column;gap:4px;font-size:12px}label.chk{flex-direction:row;align-items:center;gap:6px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { 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: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1323
+ `, 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 });
536
1324
  }
537
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisExpansionConfigEditor, decorators: [{
1325
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisExpansionConfigEditor, decorators: [{
538
1326
  type: Component,
539
- args: [{ selector: 'praxis-expansion-config-editor', standalone: true, imports: [CommonModule, MatButtonModule, MatIconModule, PraxisIconDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1327
+ args: [{ selector: 'praxis-expansion-config-editor', standalone: true, imports: [
1328
+ CommonModule,
1329
+ MatButtonModule,
1330
+ MatIconModule,
1331
+ MatFormFieldModule,
1332
+ MatInputModule,
1333
+ MatSlideToggleModule,
1334
+ MatTooltipModule,
1335
+ PraxisIconDirective,
1336
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
540
1337
  <div class="pdx-expansion-editor">
541
1338
  <section class="sec">
542
- <h4>Configuração do Acordeão</h4>
543
- <div class="grid">
544
- <label>
545
- <span>ID</span>
546
- <input type="text" [value]="config?.accordion?.id || ''" (input)="setAccordion('id', $any($event.target).value)" />
547
- </label>
548
- <label>
549
- <span>Display Mode</span>
550
- <select [value]="config?.accordion?.displayMode || 'default'" (change)="setAccordion('displayMode', $any($event.target).value)">
551
- <option value="default">default</option>
552
- <option value="flat">flat</option>
1339
+ <h4>Aparência</h4>
1340
+ <div class="grid two">
1341
+ <mat-form-field appearance="outline">
1342
+ <mat-label>Classe de tema</mat-label>
1343
+ <input matInput [value]="config?.appearance?.themeClass || ''" (input)="setAppearance('themeClass', $any($event.target).value)" placeholder="ex.: expansion-accent" />
1344
+ </mat-form-field>
1345
+ <mat-form-field appearance="outline">
1346
+ <mat-label>Densidade</mat-label>
1347
+ <select matNativeControl [value]="config?.appearance?.density || ''" (change)="setAppearance('density', $any($event.target).value || undefined)">
1348
+ <option value="">Padrão</option>
1349
+ <option value="compact">Compacta</option>
1350
+ <option value="comfortable">Confortável</option>
1351
+ <option value="spacious">Espaçosa</option>
553
1352
  </select>
554
- </label>
555
- <label>
556
- <span>Toggle Position</span>
557
- <select [value]="config?.accordion?.togglePosition || 'after'" (change)="setAccordion('togglePosition', $any($event.target).value)">
558
- <option value="after">after</option>
559
- <option value="before">before</option>
1353
+ </mat-form-field>
1354
+ </div>
1355
+
1356
+ <h5>Container</h5>
1357
+ <div class="grid two">
1358
+ <mat-form-field appearance="outline">
1359
+ <mat-label>Background</mat-label>
1360
+ <input matInput [value]="config?.appearance?.container?.background || ''" (input)="setContainer('background', $any($event.target).value)" placeholder="var(--md-sys-color-surface)" />
1361
+ </mat-form-field>
1362
+ <mat-form-field appearance="outline">
1363
+ <mat-label>Padding</mat-label>
1364
+ <input matInput [value]="config?.appearance?.container?.padding || ''" (input)="setContainer('padding', $any($event.target).value)" placeholder="0" />
1365
+ </mat-form-field>
1366
+ <mat-form-field appearance="outline">
1367
+ <mat-label>Cor da borda</mat-label>
1368
+ <input matInput [value]="config?.appearance?.container?.borderColor || ''" (input)="setContainer('borderColor', $any($event.target).value)" placeholder="var(--md-sys-color-outline-variant)" />
1369
+ </mat-form-field>
1370
+ <mat-form-field appearance="outline">
1371
+ <mat-label>Espessura da borda</mat-label>
1372
+ <input matInput [value]="config?.appearance?.container?.borderWidth || ''" (input)="setContainer('borderWidth', $any($event.target).value)" placeholder="1px" />
1373
+ </mat-form-field>
1374
+ <mat-form-field appearance="outline">
1375
+ <mat-label>Estilo da borda</mat-label>
1376
+ <select matNativeControl [value]="config?.appearance?.container?.borderStyle || ''" (change)="setContainer('borderStyle', $any($event.target).value || undefined)">
1377
+ <option value="">Padrão</option>
1378
+ <option value="solid">Solid</option>
1379
+ <option value="dashed">Dashed</option>
1380
+ <option value="dotted">Dotted</option>
1381
+ <option value="none">None</option>
560
1382
  </select>
561
- </label>
562
- <label class="chk"><input type="checkbox" [checked]="config?.accordion?.multi || false" (change)="setAccordion('multi', $any($event.target).checked)"/> Multi</label>
563
- <label class="chk"><input type="checkbox" [checked]="config?.accordion?.hideToggle || false" (change)="setAccordion('hideToggle', $any($event.target).checked)"/> Hide Toggle</label>
1383
+ </mat-form-field>
1384
+ <mat-form-field appearance="outline">
1385
+ <mat-label>Raio da borda</mat-label>
1386
+ <input matInput [value]="config?.appearance?.container?.borderRadius || ''" (input)="setContainer('borderRadius', $any($event.target).value)" placeholder="12px" />
1387
+ </mat-form-field>
1388
+ <mat-form-field appearance="outline">
1389
+ <mat-label>Sombra</mat-label>
1390
+ <input matInput [value]="config?.appearance?.container?.shadow || ''" (input)="setContainer('shadow', $any($event.target).value)" placeholder="var(--md-sys-color-shadow)" />
1391
+ </mat-form-field>
1392
+ <mat-form-field appearance="outline">
1393
+ <mat-label>Espaço entre painéis</mat-label>
1394
+ <input matInput [value]="config?.appearance?.container?.panelGap || ''" (input)="setContainer('panelGap', $any($event.target).value)" placeholder="12px" />
1395
+ </mat-form-field>
1396
+ </div>
1397
+
1398
+ <h5>Cabeçalho</h5>
1399
+ <div class="grid two">
1400
+ <mat-form-field appearance="outline">
1401
+ <mat-label>Background</mat-label>
1402
+ <input matInput [value]="config?.appearance?.header?.background || ''" (input)="setHeader('background', $any($event.target).value)" placeholder="var(--md-sys-color-surface-container)" />
1403
+ </mat-form-field>
1404
+ <mat-form-field appearance="outline">
1405
+ <mat-label>Cor do texto</mat-label>
1406
+ <input matInput [value]="config?.appearance?.header?.textColor || ''" (input)="setHeader('textColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface)" />
1407
+ </mat-form-field>
1408
+ <mat-form-field appearance="outline">
1409
+ <mat-label>Cor do título</mat-label>
1410
+ <input matInput [value]="config?.appearance?.header?.titleColor || ''" (input)="setHeader('titleColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface)" />
1411
+ </mat-form-field>
1412
+ <mat-form-field appearance="outline">
1413
+ <mat-label>Cor da descrição</mat-label>
1414
+ <input matInput [value]="config?.appearance?.header?.descriptionColor || ''" (input)="setHeader('descriptionColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface-variant)" />
1415
+ </mat-form-field>
1416
+ <mat-form-field appearance="outline">
1417
+ <mat-label>Altura</mat-label>
1418
+ <input matInput [value]="config?.appearance?.header?.height || ''" (input)="setHeader('height', $any($event.target).value)" placeholder="48px" />
1419
+ </mat-form-field>
1420
+ <mat-form-field appearance="outline">
1421
+ <mat-label>Padding</mat-label>
1422
+ <input matInput [value]="config?.appearance?.header?.padding || ''" (input)="setHeader('padding', $any($event.target).value)" placeholder="0 16px" />
1423
+ </mat-form-field>
1424
+ <mat-form-field appearance="outline">
1425
+ <mat-label>Gap</mat-label>
1426
+ <input matInput [value]="config?.appearance?.header?.gap || ''" (input)="setHeader('gap', $any($event.target).value)" placeholder="12px" />
1427
+ </mat-form-field>
1428
+ <mat-form-field appearance="outline">
1429
+ <mat-label>Font family</mat-label>
1430
+ <input matInput [value]="config?.appearance?.header?.fontFamily || ''" (input)="setHeader('fontFamily', $any($event.target).value)" placeholder="inherit" />
1431
+ </mat-form-field>
1432
+ <mat-form-field appearance="outline">
1433
+ <mat-label>Tamanho do título</mat-label>
1434
+ <input matInput [value]="config?.appearance?.header?.titleSize || ''" (input)="setHeader('titleSize', $any($event.target).value)" placeholder="14px" />
1435
+ </mat-form-field>
1436
+ <mat-form-field appearance="outline">
1437
+ <mat-label>Peso do título</mat-label>
1438
+ <input matInput [value]="config?.appearance?.header?.titleWeight || ''" (input)="setHeader('titleWeight', $any($event.target).value)" placeholder="600" />
1439
+ </mat-form-field>
1440
+ <mat-form-field appearance="outline">
1441
+ <mat-label>Tamanho da descrição</mat-label>
1442
+ <input matInput [value]="config?.appearance?.header?.descriptionSize || ''" (input)="setHeader('descriptionSize', $any($event.target).value)" placeholder="12px" />
1443
+ </mat-form-field>
1444
+ <mat-form-field appearance="outline">
1445
+ <mat-label>Peso da descrição</mat-label>
1446
+ <input matInput [value]="config?.appearance?.header?.descriptionWeight || ''" (input)="setHeader('descriptionWeight', $any($event.target).value)" placeholder="400" />
1447
+ </mat-form-field>
1448
+ <mat-form-field appearance="outline">
1449
+ <mat-label>Cor do ícone toggle</mat-label>
1450
+ <input matInput [value]="config?.appearance?.header?.toggleIconColor || ''" (input)="setHeader('toggleIconColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface-variant)" />
1451
+ </mat-form-field>
1452
+ <mat-form-field appearance="outline">
1453
+ <mat-label>Tamanho do ícone toggle</mat-label>
1454
+ <input matInput [value]="config?.appearance?.header?.toggleIconSize || ''" (input)="setHeader('toggleIconSize', $any($event.target).value)" placeholder="12px" />
1455
+ </mat-form-field>
1456
+ </div>
1457
+
1458
+ <h5>Estados</h5>
1459
+ <div class="grid two">
1460
+ <mat-form-field appearance="outline">
1461
+ <mat-label>Hover - background</mat-label>
1462
+ <input matInput [value]="config?.appearance?.states?.hover?.headerBackground || ''" (input)="setState('hover', 'headerBackground', $any($event.target).value)" placeholder="var(--md-sys-color-surface-container-high)" />
1463
+ </mat-form-field>
1464
+ <mat-form-field appearance="outline">
1465
+ <mat-label>Hover - texto</mat-label>
1466
+ <input matInput [value]="config?.appearance?.states?.hover?.textColor || ''" (input)="setState('hover', 'textColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface)" />
1467
+ </mat-form-field>
1468
+ <mat-form-field appearance="outline">
1469
+ <mat-label>Expandido - background</mat-label>
1470
+ <input matInput [value]="config?.appearance?.states?.expanded?.headerBackground || ''" (input)="setState('expanded', 'headerBackground', $any($event.target).value)" placeholder="var(--md-sys-color-surface-container)" />
1471
+ </mat-form-field>
1472
+ <mat-form-field appearance="outline">
1473
+ <mat-label>Expandido - texto</mat-label>
1474
+ <input matInput [value]="config?.appearance?.states?.expanded?.textColor || ''" (input)="setState('expanded', 'textColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface)" />
1475
+ </mat-form-field>
1476
+ <mat-form-field appearance="outline">
1477
+ <mat-label>Expandido - accent bar</mat-label>
1478
+ <input matInput [value]="config?.appearance?.states?.expanded?.accentBarColor || ''" (input)="setState('expanded', 'accentBarColor', $any($event.target).value)" placeholder="var(--md-sys-color-primary)" />
1479
+ </mat-form-field>
1480
+ <mat-form-field appearance="outline">
1481
+ <mat-label>Desabilitado - background</mat-label>
1482
+ <input matInput [value]="config?.appearance?.states?.disabled?.headerBackground || ''" (input)="setState('disabled', 'headerBackground', $any($event.target).value)" placeholder="var(--md-sys-color-surface-container)" />
1483
+ </mat-form-field>
1484
+ <mat-form-field appearance="outline">
1485
+ <mat-label>Desabilitado - texto</mat-label>
1486
+ <input matInput [value]="config?.appearance?.states?.disabled?.textColor || ''" (input)="setState('disabled', 'textColor', $any($event.target).value)" placeholder="var(--md-sys-color-on-surface-variant)" />
1487
+ </mat-form-field>
1488
+ </div>
1489
+ <mat-form-field appearance="outline" class="full">
1490
+ <mat-label>CSS personalizado</mat-label>
1491
+ <textarea matInput rows="4" [value]="config?.appearance?.customCss || ''" (input)="setAppearance('customCss', $any($event.target).value)" placeholder=".praxis-expansion-root .mat-expansion-panel { }"></textarea>
1492
+ </mat-form-field>
1493
+ <mat-form-field appearance="outline" class="full">
1494
+ <mat-label>Tokens (JSON)</mat-label>
1495
+ <textarea matInput rows="4" [value]="stringifyTokens(config?.appearance?.tokens)" (input)="setTokens($any($event.target).value)" placeholder='{"header-background-color":"var(--md-sys-color-surface-container)"}'></textarea>
1496
+ <button
1497
+ mat-icon-button
1498
+ matSuffix
1499
+ class="help-icon-button"
1500
+ type="button"
1501
+ [matTooltip]="'Use tokens M3, por exemplo: var(--md-sys-color-primary)'"
1502
+ matTooltipPosition="above"
1503
+ >
1504
+ <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
1505
+ </button>
1506
+ </mat-form-field>
1507
+ <div class="error" *ngIf="tokensError">{{ tokensError }}</div>
1508
+ </section>
1509
+
1510
+ <section class="sec">
1511
+ <h4>Configuração do acordeão</h4>
1512
+ <div class="grid two">
1513
+ <mat-form-field appearance="outline">
1514
+ <mat-label>ID</mat-label>
1515
+ <input matInput [value]="config?.accordion?.id || ''" (input)="setAccordion('id', $any($event.target).value)" />
1516
+ </mat-form-field>
1517
+ <mat-form-field appearance="outline">
1518
+ <mat-label>Modo de exibição</mat-label>
1519
+ <select matNativeControl [value]="config?.accordion?.displayMode || 'default'" (change)="setAccordion('displayMode', $any($event.target).value)">
1520
+ <option value="default">Padrão</option>
1521
+ <option value="flat">Flat</option>
1522
+ </select>
1523
+ </mat-form-field>
1524
+ <mat-form-field appearance="outline">
1525
+ <mat-label>Posição do toggle</mat-label>
1526
+ <select matNativeControl [value]="config?.accordion?.togglePosition || 'after'" (change)="setAccordion('togglePosition', $any($event.target).value)">
1527
+ <option value="after">Depois</option>
1528
+ <option value="before">Antes</option>
1529
+ </select>
1530
+ </mat-form-field>
1531
+ <div class="row wrap">
1532
+ <mat-slide-toggle [checked]="config?.accordion?.multi || false" (change)="setAccordion('multi', $event.checked)">Múltiplos abertos</mat-slide-toggle>
1533
+ <mat-slide-toggle [checked]="config?.accordion?.hideToggle || false" (change)="setAccordion('hideToggle', $event.checked)">Ocultar toggle</mat-slide-toggle>
1534
+ </div>
564
1535
  </div>
565
1536
  <div class="row">
566
- <button mat-stroked-button color="primary" (click)="addPanel()"><mat-icon [praxisIcon]="'add'"></mat-icon> Adicionar painel</button>
1537
+ <button mat-stroked-button color="primary" (click)="addPanel()"><mat-icon [praxisIcon]="'add'"></mat-icon>Adicionar painel</button>
567
1538
  </div>
568
1539
  </section>
569
1540
 
@@ -571,24 +1542,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImpor
571
1542
  <div class="panel-row">
572
1543
  <strong>{{ p.title || ('Painel ' + (i + 1)) }}</strong>
573
1544
  <span class="grow"></span>
574
- <button mat-button (click)="move(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon> Mover</button>
575
- <button mat-button (click)="move(i, 1)" [disabled]="i>=(config!.panels!.length-1)"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon> Mover</button>
576
- <button mat-button color="warn" (click)="removePanel(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon> Remover</button>
1545
+ <button mat-button (click)="move(i, -1)" [disabled]="i===0"><mat-icon [praxisIcon]="'arrow_upward'"></mat-icon>Subir</button>
1546
+ <button mat-button (click)="move(i, 1)" [disabled]="i>=(config!.panels!.length-1)"><mat-icon [praxisIcon]="'arrow_downward'"></mat-icon>Descer</button>
1547
+ <button mat-button color="warn" (click)="removePanel(i)"><mat-icon [praxisIcon]="'delete'"></mat-icon>Remover</button>
577
1548
  </div>
578
- <div class="grid">
579
- <label><span>Id</span><input type="text" [value]="p.id || ''" (input)="setPanel(i, 'id', $any($event.target).value)" /></label>
580
- <label><span>Título</span><input type="text" [value]="p.title || ''" (input)="setPanel(i, 'title', $any($event.target).value)" /></label>
581
- <label><span>Descrição</span><input type="text" [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" /></label>
582
- <label><span>Collapsed Height</span><input type="text" placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" /></label>
583
- <label><span>Expanded Height</span><input type="text" placeholder="64px" [value]="p.expandedHeight || ''" (input)="setPanel(i, 'expandedHeight', $any($event.target).value)" /></label>
584
- <label class="chk"><input type="checkbox" [checked]="p.disabled || false" (change)="setPanel(i, 'disabled', $any($event.target).checked)"/> Disabled</label>
585
- <label class="chk"><input type="checkbox" [checked]="p.expanded || false" (change)="setPanel(i, 'expanded', $any($event.target).checked)"/> Expanded</label>
586
- <label class="chk"><input type="checkbox" [checked]="p.hideToggle || false" (change)="setPanel(i, 'hideToggle', $any($event.target).checked)"/> Hide Toggle</label>
1549
+ <div class="grid two">
1550
+ <mat-form-field appearance="outline"><mat-label>ID</mat-label>
1551
+ <input matInput [value]="p.id || ''" (input)="setPanel(i, 'id', $any($event.target).value)" />
1552
+ </mat-form-field>
1553
+ <mat-form-field appearance="outline"><mat-label>Título</mat-label>
1554
+ <input matInput [value]="p.title || ''" (input)="setPanel(i, 'title', $any($event.target).value)" />
1555
+ </mat-form-field>
1556
+ <mat-form-field appearance="outline"><mat-label>Descrição</mat-label>
1557
+ <input matInput [value]="p.description || ''" (input)="setPanel(i, 'description', $any($event.target).value)" />
1558
+ </mat-form-field>
1559
+ <mat-form-field appearance="outline"><mat-label>Altura recolhida</mat-label>
1560
+ <input matInput placeholder="48px" [value]="p.collapsedHeight || ''" (input)="setPanel(i, 'collapsedHeight', $any($event.target).value)" />
1561
+ </mat-form-field>
1562
+ <mat-form-field appearance="outline"><mat-label>Altura expandida</mat-label>
1563
+ <input matInput placeholder="64px" [value]="p.expandedHeight || ''" (input)="setPanel(i, 'expandedHeight', $any($event.target).value)" />
1564
+ </mat-form-field>
1565
+ <div class="row wrap">
1566
+ <mat-slide-toggle [checked]="p.disabled || false" (change)="setPanel(i, 'disabled', $event.checked)">Desativado</mat-slide-toggle>
1567
+ <mat-slide-toggle [checked]="p.expanded || false" (change)="setPanel(i, 'expanded', $event.checked)">Expandido</mat-slide-toggle>
1568
+ <mat-slide-toggle [checked]="p.hideToggle || false" (change)="setPanel(i, 'hideToggle', $event.checked)">Ocultar toggle</mat-slide-toggle>
1569
+ </div>
587
1570
  </div>
588
1571
  </section>
589
1572
  </div>
590
- `, styles: [".pdx-expansion-editor{display:grid;gap:16px}.sec{border:1px dashed rgba(0,0,0,.12);padding:12px;border-radius:8px}.row{display:flex;gap:8px;align-items:center}.panel-row{display:flex;gap:8px;align-items:center;width:100%}.grow{flex:1 1 auto}.grid{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:8px;align-items:center}label{display:flex;flex-direction:column;gap:4px;font-size:12px}label.chk{flex-direction:row;align-items:center;gap:6px}\n"] }]
591
- }], propDecorators: { config: [{
1573
+ `, 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"] }]
1574
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
1575
+ type: Inject,
1576
+ args: [SETTINGS_PANEL_DATA]
1577
+ }] }], propDecorators: { config: [{
592
1578
  type: Input
593
1579
  }], expansionId: [{
594
1580
  type: Input
@@ -599,12 +1585,13 @@ const PRAXIS_EXPANSION_COMPONENT_METADATA = {
599
1585
  selector: 'praxis-expansion',
600
1586
  component: PraxisExpansion,
601
1587
  friendlyName: 'Praxis Expansion Panel',
602
- description: 'Acordeão/Painéis de expansão configuráveis por metadata, baseados em Angular Material.',
1588
+ description: 'Acordeão/Painéis de expansão configuráveis por metadata, com aparência e tokens M3.',
603
1589
  icon: 'unfold_more',
604
1590
  inputs: [
605
- { name: 'config', type: 'ExpansionMetadata', label: 'Configuração', description: 'Configuração JSON (acordeão e painéis)' },
606
- { name: 'expansionId', type: 'string', label: 'ID', description: 'Identificador para persistência local' },
607
- { name: 'editModeEnabled', type: 'boolean', default: false, label: 'Modo de edição' },
1591
+ { name: 'config', type: 'ExpansionMetadata', label: 'Configuração', description: 'Configuração JSON (acordeão, aparência e painéis)' },
1592
+ { name: 'expansionId', type: 'string', label: 'ID', description: 'Identificador para persistência (obrigatório)' },
1593
+ { name: 'componentInstanceId', type: 'string', label: 'ID da instância', description: 'Identificador opcional para múltiplas instâncias na mesma rota' },
1594
+ { name: 'enableCustomization', type: 'boolean', default: false, label: 'Modo de customização' },
608
1595
  { name: 'context', type: 'Record<string, any>', label: 'Contexto' },
609
1596
  { name: 'strictValidation', type: 'boolean', default: true },
610
1597
  { name: 'defaultOptions', type: 'MatExpansionPanelDefaultOptions', label: 'Opções padrão do painel' },
@@ -618,6 +1605,60 @@ const PRAXIS_EXPANSION_COMPONENT_METADATA = {
618
1605
  { name: 'destroyed', type: '{ panelId?: string; panelIndex: number }', label: 'Destruído' },
619
1606
  { name: 'widgetEvent', type: '{ panelId?: string; panelIndex?: number; sourceId: string; output?: string; payload?: any }', label: 'Evento interno' },
620
1607
  ],
1608
+ actions: [
1609
+ {
1610
+ id: 'panel-opened',
1611
+ label: 'Painel aberto',
1612
+ icon: 'unfold_more',
1613
+ description: 'Emite evento ao abrir um painel',
1614
+ emit: 'opened',
1615
+ payloadSchema: {
1616
+ type: 'object',
1617
+ properties: {
1618
+ panelId: { type: 'string', description: 'ID do painel' },
1619
+ panelIndex: { type: 'number', description: 'Índice do painel' },
1620
+ },
1621
+ required: ['panelIndex'],
1622
+ example: { panelIndex: 0 },
1623
+ },
1624
+ scope: 'context',
1625
+ },
1626
+ {
1627
+ id: 'panel-closed',
1628
+ label: 'Painel fechado',
1629
+ icon: 'unfold_less',
1630
+ description: 'Emite evento ao fechar um painel',
1631
+ emit: 'closed',
1632
+ payloadSchema: {
1633
+ type: 'object',
1634
+ properties: {
1635
+ panelId: { type: 'string', description: 'ID do painel' },
1636
+ panelIndex: { type: 'number', description: 'Índice do painel' },
1637
+ },
1638
+ required: ['panelIndex'],
1639
+ example: { panelIndex: 0 },
1640
+ },
1641
+ scope: 'context',
1642
+ },
1643
+ {
1644
+ id: 'panel-toggle',
1645
+ label: 'Alternar painel',
1646
+ icon: 'open_in_full',
1647
+ description: 'Emite evento ao alternar expansão do painel',
1648
+ emit: 'expandedChange',
1649
+ payloadSchema: {
1650
+ type: 'object',
1651
+ properties: {
1652
+ panelId: { type: 'string', description: 'ID do painel' },
1653
+ panelIndex: { type: 'number', description: 'Índice do painel' },
1654
+ expanded: { type: 'boolean', description: 'Estado expandido' },
1655
+ },
1656
+ required: ['panelIndex', 'expanded'],
1657
+ example: { panelIndex: 0, expanded: true },
1658
+ },
1659
+ scope: 'context',
1660
+ },
1661
+ ],
621
1662
  tags: ['widget', 'expansion', 'accordion', 'configurable'],
622
1663
  lib: '@praxisui/expansion',
623
1664
  };
@@ -638,11 +1679,10 @@ function providePraxisExpansionDefaults(opts) {
638
1679
  /*
639
1680
  * Public API Surface of praxis-expansion
640
1681
  */
641
- // Editor é exportado pelo mesmo arquivo de componente (praxis-expansion.ts)
642
1682
 
643
1683
  /**
644
1684
  * Generated bundle index. Do not edit.
645
1685
  */
646
1686
 
647
- export { PRAXIS_EXPANSION_COMPONENT_METADATA, PraxisExpansion, PraxisExpansionConfigEditor, providePraxisExpansionDefaults, providePraxisExpansionMetadata };
1687
+ export { EXPANSION_AI_CAPABILITIES, PRAXIS_EXPANSION_COMPONENT_METADATA, PraxisExpansion, PraxisExpansionConfigEditor, providePraxisExpansionDefaults, providePraxisExpansionMetadata };
648
1688
  //# sourceMappingURL=praxisui-expansion.mjs.map