@praxisui/crud 9.0.0-beta.5 → 9.0.0-beta.50

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, InjectionToken, inject, input, signal, computed, effect, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, ChangeDetectorRef, Injector, ViewChild, Output, Input, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, InjectionToken, inject, input, signal, computed, effect, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, ChangeDetectorRef, Injector, ViewChild, Output, Input, Inject, ViewEncapsulation, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import { HttpClient } from '@angular/common/http';
5
5
  import { Router, ActivatedRoute, RouterLink } from '@angular/router';
@@ -10,6 +10,7 @@ import { ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionServi
10
10
  import { SettingsPanelService } from '@praxisui/settings-panel';
11
11
  import { PraxisTableInlineAuthoringEditorComponent, PraxisTable } from '@praxisui/table';
12
12
  import { ConfirmDialogComponent } from '@praxisui/dynamic-fields';
13
+ import { take, filter } from 'rxjs/operators';
13
14
  import * as i1 from '@angular/material/dialog';
14
15
  import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
15
16
  export { MAT_DIALOG_DATA as DIALOG_DATA } from '@angular/material/dialog';
@@ -34,7 +35,6 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
34
35
  import * as i10 from '@angular/material/tabs';
35
36
  import { MatTabsModule } from '@angular/material/tabs';
36
37
  import { PraxisDynamicForm } from '@praxisui/dynamic-form';
37
- import { filter, take } from 'rxjs/operators';
38
38
 
39
39
  class DialogService {
40
40
  matDialog;
@@ -68,6 +68,27 @@ function getCrudDrawerAdapterToken() {
68
68
  }
69
69
  const CRUD_DRAWER_ADAPTER = getCrudDrawerAdapterToken();
70
70
 
71
+ function isCrudDebugEnabled$1() {
72
+ try {
73
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
74
+ return true;
75
+ }
76
+ if (typeof localStorage !== 'undefined') {
77
+ return localStorage.getItem('praxis.debug.crud') === 'true';
78
+ }
79
+ }
80
+ catch { }
81
+ return false;
82
+ }
83
+ function debugCrudLauncher(message, ...data) {
84
+ if (!isCrudDebugEnabled$1()) {
85
+ return;
86
+ }
87
+ try {
88
+ console.debug(message, ...data);
89
+ }
90
+ catch { }
91
+ }
71
92
  class CrudLauncherService {
72
93
  router = inject(Router);
73
94
  dialog = inject(DialogService);
@@ -85,8 +106,9 @@ class CrudLauncherService {
85
106
  async launch(action, row, metadata, componentKeyId, drawerCallbacks, runtime) {
86
107
  // Carregar overrides de CRUD (se houver) e mesclar em uma cópia local
87
108
  const merged = await this.mergeCrudOverrides(metadata, action, componentKeyId || undefined);
109
+ merged.action = this.normalizeActionForLaunch(merged.action);
88
110
  const mode = this.resolveOpenMode(merged.action, merged.metadata);
89
- console.debug('[CRUD:Launcher] mode=', mode, 'action=', action);
111
+ debugCrudLauncher('[CRUD:Launcher] mode=', mode, 'action=', action);
90
112
  if (mode === 'route') {
91
113
  if (!merged.action.route) {
92
114
  throw new Error(`Route not provided for action ${merged.action.action}`);
@@ -95,22 +117,6 @@ class CrudLauncherService {
95
117
  await this.router.navigateByUrl(url);
96
118
  return { mode };
97
119
  }
98
- if (mode === 'drawer' && this.drawerAdapter) {
99
- const actionForLaunch = this.resolveActionForLaunch(merged.action, merged.metadata);
100
- const inputs = this.mapInputs(actionForLaunch, row, merged.metadata, runtime);
101
- const idField = merged.metadata.resource?.idField ?? 'id';
102
- if (row && inputs[idField] === undefined && row[idField] !== undefined) {
103
- inputs[idField] = row[idField];
104
- }
105
- await Promise.resolve(this.drawerAdapter.open({
106
- action: actionForLaunch,
107
- metadata: merged.metadata,
108
- inputs,
109
- onClose: drawerCallbacks?.onClose,
110
- onResult: drawerCallbacks?.onResult,
111
- }));
112
- return { mode };
113
- }
114
120
  const actionForLaunch = this.resolveActionForLaunch(merged.action, merged.metadata);
115
121
  if (!actionForLaunch.formId) {
116
122
  throw new Error(`formId not provided for action ${actionForLaunch.action}`);
@@ -123,18 +129,26 @@ class CrudLauncherService {
123
129
  row[idField] !== undefined) {
124
130
  inputs[idField] = row[idField];
125
131
  }
132
+ if (mode === 'drawer' && this.drawerAdapter) {
133
+ await Promise.resolve(this.drawerAdapter.open({
134
+ action: actionForLaunch,
135
+ metadata: merged.metadata,
136
+ inputs,
137
+ onClose: drawerCallbacks?.onClose,
138
+ onResult: drawerCallbacks?.onResult,
139
+ }));
140
+ return { mode };
141
+ }
126
142
  const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
127
- console.debug('[CRUD:Launcher] opening dialog with:', {
143
+ debugCrudLauncher('[CRUD:Launcher] opening dialog with:', {
128
144
  action: merged.action.action,
129
145
  formId: actionForLaunch.formId,
130
146
  inputs,
131
147
  modalCfg,
132
148
  resourcePath: merged.metadata.resource?.path ?? merged.metadata.table?.resourcePath,
133
149
  });
134
- const panelClasses = ['pfx-dialog-pane', 'pfx-dialog-frosted'];
135
- if (modalCfg.panelClass) {
136
- panelClasses.push(modalCfg.panelClass);
137
- }
150
+ const drawerMode = mode === 'drawer';
151
+ const panelClasses = mergeClassList(['pfx-dialog-pane', drawerMode ? 'pfx-drawer-pane' : 'pfx-dialog-frosted'], modalCfg.panelClass);
138
152
  // Backdrop style presets
139
153
  const backdropClasses = [];
140
154
  const style = modalCfg.backdropStyle;
@@ -147,20 +161,44 @@ class CrudLauncherService {
147
161
  else if (style === 'transparent') {
148
162
  backdropClasses.push('pfx-transparent-backdrop');
149
163
  }
150
- if (modalCfg.backdropClass) {
151
- backdropClasses.push(modalCfg.backdropClass);
152
- }
164
+ const mergedBackdropClasses = mergeClassList(backdropClasses, modalCfg.backdropClass);
153
165
  const ref = await this.dialog.openAsync(() => Promise.resolve().then(function () { return dynamicFormDialogHost_component; }).then((m) => m.DynamicFormDialogHostComponent), {
154
166
  ...modalCfg,
155
167
  panelClass: panelClasses,
156
- backdropClass: backdropClasses,
168
+ backdropClass: mergedBackdropClasses,
157
169
  autoFocus: modalCfg.autoFocus ?? true,
158
170
  restoreFocus: modalCfg.restoreFocus ?? true,
159
- minWidth: '360px',
160
- maxWidth: '95vw',
171
+ minWidth: drawerMode ? (modalCfg.minWidth ?? '0') : '360px',
172
+ width: drawerMode ? (modalCfg.width ?? 'min(560px, 100vw)') : modalCfg.width,
173
+ height: drawerMode ? (modalCfg.height ?? '100dvh') : modalCfg.height,
174
+ maxWidth: drawerMode ? (modalCfg.maxWidth ?? '100vw') : '95vw',
175
+ maxHeight: drawerMode ? (modalCfg.maxHeight ?? '100dvh') : modalCfg.maxHeight,
176
+ position: drawerMode ? (modalCfg.position ?? { right: '0', top: '0' }) : modalCfg.position,
161
177
  ariaLabelledBy: 'crudDialogTitle',
162
- data: { action: actionForLaunch, row, metadata: merged.metadata, inputs },
178
+ data: {
179
+ action: actionForLaunch,
180
+ row,
181
+ metadata: merged.metadata,
182
+ inputs,
183
+ presentation: drawerMode ? 'drawer' : 'modal',
184
+ },
163
185
  });
186
+ if (drawerMode) {
187
+ ref
188
+ .afterClosed()
189
+ .pipe(take(1))
190
+ .subscribe((closedValue) => {
191
+ const result = toCrudDrawerResult(closedValue);
192
+ drawerCallbacks?.onClose?.();
193
+ if (result?.type) {
194
+ drawerCallbacks?.onResult?.(result);
195
+ }
196
+ else {
197
+ drawerCallbacks?.onResult?.({ type: 'close' });
198
+ }
199
+ });
200
+ return { mode, ref };
201
+ }
164
202
  return { mode, ref };
165
203
  }
166
204
  resolveOpenMode(action, metadata) {
@@ -248,6 +286,9 @@ class CrudLauncherService {
248
286
  if (resolved.apiUrlEntry != null) {
249
287
  inputs['apiUrlEntry'] = resolved.apiUrlEntry;
250
288
  }
289
+ if (resolved.layoutPolicy != null) {
290
+ inputs['layoutPolicy'] = resolved.layoutPolicy;
291
+ }
251
292
  if (action.form?.initialValue != null) {
252
293
  inputs['initialValue'] = action.form.initialValue;
253
294
  }
@@ -257,6 +298,7 @@ class CrudLauncherService {
257
298
  return inputs;
258
299
  }
259
300
  resolveActionForLaunch(action, metadata) {
301
+ action = this.normalizeActionForLaunch(action);
260
302
  if (action.formId) {
261
303
  return action;
262
304
  }
@@ -264,29 +306,61 @@ class CrudLauncherService {
264
306
  return action;
265
307
  }
266
308
  const formId = this.buildInferredFormId(action, metadata);
267
- return formId ? { ...action, formId } : action;
309
+ return formId
310
+ ? {
311
+ ...action,
312
+ formId,
313
+ form: {
314
+ ...(action.form || {}),
315
+ layoutPolicy: action.form?.layoutPolicy ?? this.defaultInferredLayoutPolicy(action),
316
+ },
317
+ }
318
+ : action;
319
+ }
320
+ resolveSubmitUrlTemplate(submitUrl, row, metadata) {
321
+ const template = String(submitUrl || '').trim();
322
+ if (!template || !template.includes('{')) {
323
+ return template;
324
+ }
325
+ const idField = String(metadata.resource?.idField ?? 'id');
326
+ const rowId = row?.[idField];
327
+ if (rowId == null) {
328
+ return template;
329
+ }
330
+ const encodedId = encodeURIComponent(String(rowId));
331
+ const idFieldPattern = new RegExp(`\\{${escapeRegExp(idField)}\\}`, 'g');
332
+ return template
333
+ .replace(idFieldPattern, encodedId)
334
+ .replace(/\{id\}/g, encodedId)
335
+ .replace(/\{resourceId\}/g, encodedId);
268
336
  }
269
337
  resolveActionFormContract(action, row, metadata, runtime) {
270
338
  if (this.isExplicitCrudAction(action)) {
271
339
  return {
272
340
  schemaUrl: action.form?.schemaUrl ?? null,
273
- submitUrl: action.form?.submitUrl ?? null,
341
+ submitUrl: action.form?.submitUrl
342
+ ? this.resolveSubmitUrlTemplate(action.form.submitUrl, row, metadata)
343
+ : null,
274
344
  submitMethod: action.form?.submitMethod ?? null,
275
345
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
276
346
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
347
+ layoutPolicy: action.form?.layoutPolicy ?? null,
277
348
  };
278
349
  }
279
350
  if (!this.isCanonicalCrudAction(action.action)) {
280
351
  return {
281
352
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
282
353
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
354
+ layoutPolicy: action.form?.layoutPolicy ?? null,
283
355
  };
284
356
  }
285
357
  const resourcePath = String(metadata.resource?.path ?? metadata.table?.resourcePath ?? '').trim();
358
+ const schemaResourcePath = String(metadata.resource?.schemaPath || '').trim() || null;
286
359
  if (!resourcePath) {
287
360
  return {
288
361
  apiEndpointKey: action.form?.apiEndpointKey ?? null,
289
362
  apiUrlEntry: action.form?.apiUrlEntry ?? null,
363
+ layoutPolicy: action.form?.layoutPolicy ?? null,
290
364
  };
291
365
  }
292
366
  const idField = String(metadata.resource?.idField ?? 'id');
@@ -294,6 +368,7 @@ class CrudLauncherService {
294
368
  const resolved = this.operationResolver.resolve({
295
369
  operation: action.action,
296
370
  resourcePath,
371
+ schemaResourcePath,
297
372
  resourceId: resourceId ?? null,
298
373
  capabilities: runtime?.capabilities ?? null,
299
374
  links: runtime?.links ?? null,
@@ -301,14 +376,15 @@ class CrudLauncherService {
301
376
  endpointKey: action.form?.apiEndpointKey ??
302
377
  metadata.resource?.endpointKey ??
303
378
  undefined,
304
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
379
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
305
380
  });
306
381
  return {
307
382
  schemaUrl: resolved?.schemaUrl ?? null,
308
383
  submitUrl: resolved?.submitUrl ?? null,
309
384
  submitMethod: resolved?.submitMethod ?? null,
310
385
  apiEndpointKey: action.form?.apiEndpointKey ?? metadata.resource?.endpointKey ?? null,
311
- apiUrlEntry: action.form?.apiUrlEntry ?? null,
386
+ apiUrlEntry: action.form?.apiUrlEntry ?? metadata.resource?.apiUrlEntry ?? null,
387
+ layoutPolicy: action.form?.layoutPolicy ?? null,
312
388
  };
313
389
  }
314
390
  resolveRuntimeContract(action, row, metadata, runtime) {
@@ -327,9 +403,20 @@ class CrudLauncherService {
327
403
  return `${sanitizedResource || 'crud'}-${String(action.action || '').trim().toLowerCase()}`;
328
404
  }
329
405
  isCanonicalCrudAction(actionName) {
330
- const normalized = String(actionName || '').trim().toLowerCase();
406
+ const normalized = this.normalizeCrudOperationName(actionName);
331
407
  return normalized === 'create' || normalized === 'view' || normalized === 'edit' || normalized === 'delete';
332
408
  }
409
+ normalizeActionForLaunch(action) {
410
+ const normalized = this.normalizeCrudOperationName(action.action);
411
+ return normalized === action.action ? action : { ...action, action: normalized };
412
+ }
413
+ normalizeCrudOperationName(actionName) {
414
+ const normalized = String(actionName || '').trim().toLowerCase();
415
+ if (['add', 'novo', 'new', 'incluir', 'inserir'].includes(normalized)) {
416
+ return 'create';
417
+ }
418
+ return normalized;
419
+ }
333
420
  isExplicitCrudAction(action) {
334
421
  if (action.mode === 'explicit') {
335
422
  return true;
@@ -338,6 +425,19 @@ class CrudLauncherService {
338
425
  String(action.form?.submitUrl || '').trim() ||
339
426
  String(action.form?.submitMethod || '').trim());
340
427
  }
428
+ defaultInferredLayoutPolicy(action) {
429
+ const operation = this.normalizeCrudOperationName(action.action);
430
+ if (operation !== 'create' && operation !== 'edit') {
431
+ return null;
432
+ }
433
+ return {
434
+ source: 'schema',
435
+ intent: 'command',
436
+ preset: 'groupedCommand',
437
+ persistence: 'transient',
438
+ schemaType: 'request',
439
+ };
440
+ }
341
441
  async mergeCrudOverrides(metadata, action, componentKeyId) {
342
442
  try {
343
443
  if (!componentKeyId)
@@ -392,6 +492,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
392
492
  type: Injectable,
393
493
  args: [{ providedIn: 'root' }]
394
494
  }] });
495
+ function mergeClassList(base, extra) {
496
+ if (!extra) {
497
+ return base;
498
+ }
499
+ if (typeof extra === 'string') {
500
+ return [...base, extra];
501
+ }
502
+ if (Array.isArray(extra)) {
503
+ return [...base, ...extra];
504
+ }
505
+ return [
506
+ ...base,
507
+ ...Object.keys(extra).filter((className) => !!extra[className]),
508
+ ];
509
+ }
510
+ function escapeRegExp(value) {
511
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
512
+ }
513
+ function toCrudDrawerResult(value) {
514
+ if (!value || typeof value !== 'object') {
515
+ return undefined;
516
+ }
517
+ const result = value;
518
+ return result.type === 'save' || result.type === 'delete' || result.type === 'close'
519
+ ? result
520
+ : undefined;
521
+ }
395
522
 
396
523
  const DOCUMENT_KIND = 'praxis.crud.editor';
397
524
  const DOCUMENT_VERSION = 1;
@@ -564,6 +691,7 @@ function normalizeCrudAction(action) {
564
691
  apiEndpointKey: trimString(action.form.apiEndpointKey) || undefined,
565
692
  apiUrlEntry: action.form.apiUrlEntry || undefined,
566
693
  initialValue: normalizeInitialValue(action.form.initialValue),
694
+ layoutPolicy: action.form.layoutPolicy ?? undefined,
567
695
  }
568
696
  : undefined,
569
697
  });
@@ -3527,6 +3655,7 @@ class PraxisCrudComponent {
3527
3655
  collectionCapabilitiesResolvedHref = null;
3528
3656
  collectionCapabilitiesRequestSeq = 0;
3529
3657
  currentAuthoringDocument;
3658
+ selectedRow = null;
3530
3659
  getResourceDiscovery() {
3531
3660
  const assigned = this.resourceDiscovery;
3532
3661
  return assigned ?? (this.resourceDiscoveryInstance ??= this.injector.get(ResourceDiscoveryService));
@@ -3553,11 +3682,27 @@ class PraxisCrudComponent {
3553
3682
  catch { }
3554
3683
  }
3555
3684
  onTableRowClick(event) {
3685
+ const row = this.extractRowFromEvent(event);
3686
+ if (row) {
3687
+ this.selectedRow = row;
3688
+ }
3556
3689
  this.rowClick.emit(event);
3557
3690
  }
3558
3691
  onTableSelectionChange(event) {
3692
+ this.selectedRow = this.extractSelectedRowFromEvent(event);
3559
3693
  this.selectionChange.emit(event);
3560
3694
  }
3695
+ async onBulkAction(event) {
3696
+ const action = String(event?.action || '').trim();
3697
+ if (!action) {
3698
+ return;
3699
+ }
3700
+ const row = this.extractSelectedRowFromBulkAction(event);
3701
+ if (row) {
3702
+ this.selectedRow = row;
3703
+ }
3704
+ await this.onAction(action, row ?? undefined, event);
3705
+ }
3561
3706
  ngOnChanges(changes) {
3562
3707
  if (!changes['metadata'] && !changes['context']) {
3563
3708
  return;
@@ -3595,33 +3740,41 @@ class PraxisCrudComponent {
3595
3740
  async onAction(action, row, runtimeEvent) {
3596
3741
  try {
3597
3742
  document.activeElement?.blur();
3598
- let actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3743
+ const normalizedAction = this.normalizeCrudActionName(action);
3744
+ const contextualRow = row ?? this.resolveSelectedRowForAction(normalizedAction);
3745
+ let actionMeta = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3746
+ if (!actionMeta && normalizedAction !== action) {
3747
+ actionMeta = this.resolvedMetadata.actions?.find((candidate) => candidate.action === action);
3748
+ }
3599
3749
  if (!actionMeta) {
3600
- const ctxAction = this.tableCrudContext?.actions?.find((candidate) => candidate.action === action);
3750
+ const ctxAction = this.tableCrudContext?.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
3601
3751
  if (ctxAction) {
3602
3752
  actionMeta = {
3603
- action: ctxAction.action,
3753
+ action: this.normalizeCrudActionName(ctxAction.action),
3604
3754
  openMode: ctxAction.openMode,
3605
3755
  formId: ctxAction.formId,
3606
3756
  route: ctxAction.route,
3607
3757
  };
3608
3758
  }
3609
3759
  else {
3610
- actionMeta = { action };
3760
+ actionMeta = { action: normalizedAction };
3611
3761
  }
3612
3762
  }
3613
- const effectiveAction = (actionMeta || { action });
3763
+ const effectiveAction = {
3764
+ ...(actionMeta || { action: normalizedAction }),
3765
+ action: this.normalizeCrudActionName(actionMeta?.action ?? normalizedAction),
3766
+ };
3614
3767
  if (!this.hasExplicitOpenBinding(effectiveAction)) {
3615
- const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(action, row, runtimeEvent);
3768
+ const openedByDiscovery = await this.tryOpenDiscoveredCrudSurface(effectiveAction.action, contextualRow, runtimeEvent);
3616
3769
  if (openedByDiscovery) {
3617
3770
  return;
3618
3771
  }
3619
- const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(action, row, runtimeEvent);
3772
+ const handledByWorkflowAction = await this.tryOpenDiscoveredWorkflowAction(effectiveAction.action, contextualRow, runtimeEvent);
3620
3773
  if (handledByWorkflowAction) {
3621
3774
  return;
3622
3775
  }
3623
3776
  }
3624
- const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, row);
3777
+ const handledByDelete = await this.tryHandleCanonicalDeleteAction(effectiveAction, contextualRow);
3625
3778
  if (handledByDelete) {
3626
3779
  return;
3627
3780
  }
@@ -3632,7 +3785,7 @@ class PraxisCrudComponent {
3632
3785
  drawerCloseEmitted = true;
3633
3786
  this.afterClose.emit();
3634
3787
  };
3635
- const { mode, ref } = await this.launcher.launch(effectiveAction, row, this.resolvedMetadata, this.componentKeyId(), {
3788
+ const { mode, ref } = await this.launcher.launch(effectiveAction, contextualRow, this.resolvedMetadata, this.componentKeyId(), {
3636
3789
  onClose: () => emitDrawerClose(),
3637
3790
  onResult: (result) => {
3638
3791
  emitDrawerClose();
@@ -3653,9 +3806,12 @@ class PraxisCrudComponent {
3653
3806
  capabilities: effectiveAction.action === 'create'
3654
3807
  ? this.collectionCapabilities
3655
3808
  : this.collectionCapabilities,
3656
- links: this.resolveCrudRuntimeLinks(effectiveAction.action, row),
3809
+ links: this.resolveCrudRuntimeLinks(effectiveAction.action, contextualRow),
3657
3810
  });
3658
3811
  this.afterOpen.emit({ mode, action: effectiveAction.action });
3812
+ if (mode === 'drawer') {
3813
+ return;
3814
+ }
3659
3815
  if (!ref) {
3660
3816
  return;
3661
3817
  }
@@ -3901,11 +4057,47 @@ class PraxisCrudComponent {
3901
4057
  }
3902
4058
  }
3903
4059
  resolveQueryContext(meta) {
4060
+ if (Array.isArray(meta?.data) && this.resolveResourcePath(meta).length === 0) {
4061
+ return null;
4062
+ }
3904
4063
  return this.isRecord(meta?.queryContext) ? meta.queryContext : null;
3905
4064
  }
3906
4065
  resolveFilterCriteria(meta) {
3907
4066
  return this.isRecord(meta?.filterCriteria) ? { ...meta.filterCriteria } : {};
3908
4067
  }
4068
+ extractRowFromEvent(event) {
4069
+ if (!this.isRecord(event)) {
4070
+ return null;
4071
+ }
4072
+ const row = event['row'];
4073
+ return this.isRecord(row) ? row : null;
4074
+ }
4075
+ extractSelectedRowFromEvent(event) {
4076
+ if (!this.isRecord(event)) {
4077
+ return null;
4078
+ }
4079
+ const selection = event['selection'];
4080
+ if (Array.isArray(selection)) {
4081
+ const first = selection[0];
4082
+ return this.isRecord(first) ? first : null;
4083
+ }
4084
+ const row = event['row'];
4085
+ if (this.isRecord(row)) {
4086
+ return row;
4087
+ }
4088
+ return null;
4089
+ }
4090
+ extractSelectedRowFromBulkAction(event) {
4091
+ const first = Array.isArray(event?.rows) ? event.rows[0] : null;
4092
+ return this.isRecord(first) ? first : null;
4093
+ }
4094
+ resolveSelectedRowForAction(action) {
4095
+ const normalized = String(action || '').trim().toLowerCase();
4096
+ if (!normalized || normalized === 'create' || normalized === 'add' || normalized === 'new') {
4097
+ return undefined;
4098
+ }
4099
+ return this.selectedRow ?? undefined;
4100
+ }
3909
4101
  async tryOpenDiscoveredCrudSurface(action, row, runtimeEvent) {
3910
4102
  const normalizedAction = String(action || '').trim().toLowerCase();
3911
4103
  if (!this.surfaceService) {
@@ -3999,7 +4191,7 @@ class PraxisCrudComponent {
3999
4191
  const providedAction = this.resolveProvidedWorkflowAction(normalizedAction, runtimeEvent?.actionConfig);
4000
4192
  const catalog = providedAction ? null : await this.resolveDiscoveredActionCatalog(row);
4001
4193
  const discoveredAction = providedAction || this.selectDiscoveredWorkflowAction(normalizedAction, catalog?.actions || []);
4002
- const resourcePath = String(this.resolveResourcePath(this.resolvedMetadata) || catalog?.resourcePath || '').trim();
4194
+ const resourcePath = String(catalog?.resourcePath || this.resolveResourcePath(this.resolvedMetadata) || '').trim();
4003
4195
  if (!discoveredAction || !resourcePath) {
4004
4196
  return false;
4005
4197
  }
@@ -4222,12 +4414,19 @@ class PraxisCrudComponent {
4222
4414
  }
4223
4415
  return cfg.toolbar;
4224
4416
  };
4225
- const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.isAddLike(action));
4417
+ const hasToolbarAdd = (cfg.toolbar?.actions || []).some((action) => this.shouldCanonicalizeToolbarCreateAction(action));
4226
4418
  const addAction = this.resolveCreateToolbarAction(meta, capabilities);
4227
4419
  if (addAction) {
4228
4420
  if (hasToolbarAdd) {
4229
4421
  const toolbar = ensureToolbar();
4230
4422
  toolbar.visible = true;
4423
+ toolbar.actions = (toolbar.actions || []).map((action) => this.shouldCanonicalizeToolbarCreateAction(action)
4424
+ ? {
4425
+ ...action,
4426
+ action: 'create',
4427
+ }
4428
+ : action);
4429
+ changed = true;
4231
4430
  }
4232
4431
  else {
4233
4432
  const toolbar = ensureToolbar();
@@ -4362,6 +4561,28 @@ class PraxisCrudComponent {
4362
4561
  }
4363
4562
  return false;
4364
4563
  }
4564
+ normalizeCrudActionName(action) {
4565
+ const raw = String(action || '').trim();
4566
+ const normalized = raw.toLowerCase();
4567
+ return this.isCreateActionAliasName(normalized) ? 'create' : raw;
4568
+ }
4569
+ shouldCanonicalizeToolbarCreateAction(action) {
4570
+ if (!action)
4571
+ return false;
4572
+ const normalize = (value) => String(value || '').trim().toLowerCase();
4573
+ const explicitId = normalize(action.action || action.id || action.code || action.key || action.name || action.type);
4574
+ if (this.isCreateActionAliasName(explicitId))
4575
+ return true;
4576
+ if (explicitId)
4577
+ return false;
4578
+ const icon = normalize(action.icon);
4579
+ const label = normalize(action.label);
4580
+ return ((icon === 'add' || icon === 'add_circle' || icon === 'add_box') &&
4581
+ (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir'));
4582
+ }
4583
+ isCreateActionAliasName(actionName) {
4584
+ return ['create', 'add', 'novo', 'new', 'incluir', 'inserir'].includes(actionName);
4585
+ }
4365
4586
  buildTableCrudContext(meta, capabilities) {
4366
4587
  if (!meta)
4367
4588
  return undefined;
@@ -4642,6 +4863,7 @@ class PraxisCrudComponent {
4642
4863
  (selectionChange)="onTableSelectionChange($event)"
4643
4864
  (rowAction)="onAction($event.action, $event.row, $event)"
4644
4865
  (toolbarAction)="onAction($event.action)"
4866
+ (bulkAction)="onBulkAction($event)"
4645
4867
  (collectionLinksChange)="onCollectionLinksChange($event)"
4646
4868
  (reset)="onResetPreferences()"
4647
4869
  (metadataChange)="onTableMetadataChange()"
@@ -4657,7 +4879,7 @@ class PraxisCrudComponent {
4657
4879
  />
4658
4880
  }
4659
4881
  }
4660
- `, isInline: true, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}\n"], dependencies: [{ kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "dense"], outputs: ["rowClick", "widgetEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone"] }] });
4882
+ `, isInline: true, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}\n"], dependencies: [{ kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "data", "tableId", "componentInstanceId", "configPersistenceStrategy", "title", "subtitle", "icon", "autoDelete", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "crudContext", "filterCriteria", "queryContext", "aiContext", "aiAssistantVoiceInputMode", "aiAssistantVoiceLanguage", "horizontalScroll", "enableCustomization", "dense"], outputs: ["rowClick", "widgetEvent", "resourceEvent", "rowDoubleClick", "rowExpansionChange", "rowAction", "toolbarAction", "bulkAction", "exportAction", "columnReorder", "columnReorderAttempt", "columnResize", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError", "schemaStatusChange", "configChange", "metadataChange", "loadingStateChange", "collectionLinksChange", "selectionChange"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline", "tone", "variant", "alignment", "density", "iconContainer"] }] });
4661
4883
  }
4662
4884
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, decorators: [{
4663
4885
  type: Component,
@@ -4679,6 +4901,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4679
4901
  (selectionChange)="onTableSelectionChange($event)"
4680
4902
  (rowAction)="onAction($event.action, $event.row, $event)"
4681
4903
  (toolbarAction)="onAction($event.action)"
4904
+ (bulkAction)="onBulkAction($event)"
4682
4905
  (collectionLinksChange)="onCollectionLinksChange($event)"
4683
4906
  (reset)="onResetPreferences()"
4684
4907
  (metadataChange)="onTableMetadataChange()"
@@ -4734,6 +4957,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
4734
4957
  args: [PraxisTable]
4735
4958
  }] } });
4736
4959
 
4960
+ function isCrudDebugEnabled() {
4961
+ try {
4962
+ if (globalThis.__PRAXIS_DEBUG_CRUD__) {
4963
+ return true;
4964
+ }
4965
+ if (typeof localStorage !== 'undefined') {
4966
+ return localStorage.getItem('praxis.debug.crud') === 'true';
4967
+ }
4968
+ }
4969
+ catch { }
4970
+ return false;
4971
+ }
4972
+ function debugCrudHost(message, data) {
4973
+ if (!isCrudDebugEnabled()) {
4974
+ return;
4975
+ }
4976
+ try {
4977
+ console.debug(message, data);
4978
+ }
4979
+ catch { }
4980
+ }
4737
4981
  class DynamicFormDialogHostComponent {
4738
4982
  dialogRef;
4739
4983
  data;
@@ -4742,6 +4986,7 @@ class DynamicFormDialogHostComponent {
4742
4986
  configStorage;
4743
4987
  formComp;
4744
4988
  modal = {};
4989
+ presentation = 'modal';
4745
4990
  maximized = false;
4746
4991
  initialSize = {};
4747
4992
  rememberState = false;
@@ -4756,6 +5001,8 @@ class DynamicFormDialogHostComponent {
4756
5001
  submitMethod;
4757
5002
  apiEndpointKey;
4758
5003
  apiUrlEntry;
5004
+ layoutPolicy;
5005
+ formConfig = {};
4759
5006
  formActions;
4760
5007
  mode = 'create';
4761
5008
  backConfig;
@@ -4778,6 +5025,7 @@ class DynamicFormDialogHostComponent {
4778
5025
  this.crud = crud;
4779
5026
  this.configStorage = configStorage;
4780
5027
  this.dialogRef.disableClose = true;
5028
+ this.presentation = this.data.presentation === 'drawer' ? 'drawer' : 'modal';
4781
5029
  // i18n
4782
5030
  this.texts = {
4783
5031
  ...this.texts,
@@ -4812,12 +5060,14 @@ class DynamicFormDialogHostComponent {
4812
5060
  this.apiUrlEntry = this.data.inputs?.['apiUrlEntry'] ?? null;
4813
5061
  const act = this.data.action?.action;
4814
5062
  this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
5063
+ this.formConfig = this.resolveFormConfig();
5064
+ this.layoutPolicy = this.resolveLayoutPolicy();
4815
5065
  this.formActions = this.resolveFormActions();
4816
5066
  // Back config: defaults from metadata/action, overridden by saved per-form config
4817
5067
  const defaults = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
4818
5068
  this.backDefaults = defaults;
4819
5069
  this.backConfig = { ...defaults };
4820
- console.debug('[CRUD:Host] constructed', {
5070
+ debugCrudHost('[CRUD:Host] constructed', {
4821
5071
  action: this.data?.action,
4822
5072
  resourcePath: this.resourcePath,
4823
5073
  resourceId: this.resourceId,
@@ -4857,6 +5107,7 @@ class DynamicFormDialogHostComponent {
4857
5107
  'apiEndpointKey',
4858
5108
  'apiUrlEntry',
4859
5109
  'initialValue',
5110
+ 'layoutPolicy',
4860
5111
  ]);
4861
5112
  const explicit = inputs['initialValue'] && typeof inputs['initialValue'] === 'object'
4862
5113
  ? { ...inputs['initialValue'] }
@@ -4869,10 +5120,32 @@ class DynamicFormDialogHostComponent {
4869
5120
  }
4870
5121
  return Object.keys(explicit).length ? explicit : null;
4871
5122
  }
5123
+ resolveFormConfig() {
5124
+ const config = this.data.metadata?.form;
5125
+ return config && typeof config === 'object' ? config : {};
5126
+ }
5127
+ get dialogTitle() {
5128
+ return stringOrUndefined(this.data.action?.label) ||
5129
+ stringOrUndefined(this.formConfig['title']) ||
5130
+ deriveModeTitle(this.mode, this.resolveResourceTitle()) ||
5131
+ this.texts.title;
5132
+ }
5133
+ resolveResourceTitle() {
5134
+ const resource = this.data.metadata?.resource ?? {};
5135
+ const table = this.data.metadata?.table ?? {};
5136
+ return stringOrUndefined(resource.formTitle ??
5137
+ resource.title ??
5138
+ resource.label ??
5139
+ table.title ??
5140
+ table.label) || titleFromResourcePath(this.resourcePath);
5141
+ }
4872
5142
  resolveFormActions() {
4873
5143
  if (this.mode === 'view') {
4874
5144
  return undefined;
4875
5145
  }
5146
+ if (this.formConfig?.actions) {
5147
+ return undefined;
5148
+ }
4876
5149
  const submitLabel = this.resolveSubmitLabel();
4877
5150
  if (!submitLabel) {
4878
5151
  return undefined;
@@ -4890,6 +5163,15 @@ class DynamicFormDialogHostComponent {
4890
5163
  },
4891
5164
  };
4892
5165
  }
5166
+ resolveLayoutPolicy() {
5167
+ const explicit = this.data.inputs?.['layoutPolicy'] ??
5168
+ this.data.action?.form?.layoutPolicy ??
5169
+ this.data.action?.layoutPolicy;
5170
+ if (explicit && typeof explicit === 'object') {
5171
+ return explicit;
5172
+ }
5173
+ return null;
5174
+ }
4893
5175
  resolveSubmitLabel() {
4894
5176
  const action = this.data.action ?? {};
4895
5177
  const explicit = stringOrUndefined(action.form?.submitLabel ??
@@ -4962,10 +5244,10 @@ class DynamicFormDialogHostComponent {
4962
5244
  ref
4963
5245
  .afterClosed()
4964
5246
  .pipe(filter((confirmed) => !!confirmed), takeUntilDestroyed(this.destroyRef))
4965
- .subscribe(() => this.dialogRef.close());
5247
+ .subscribe(() => this.dialogRef.close({ type: 'close' }));
4966
5248
  }
4967
5249
  else {
4968
- this.dialogRef.close();
5250
+ this.dialogRef.close({ type: 'close' });
4969
5251
  }
4970
5252
  }
4971
5253
  toggleMaximize(initial = false) {
@@ -5008,7 +5290,7 @@ class DynamicFormDialogHostComponent {
5008
5290
  width: saved?.width ?? this.modal.width,
5009
5291
  height: saved?.height ?? this.modal.height,
5010
5292
  };
5011
- console.debug('[CRUD:Host] ngOnInit', {
5293
+ debugCrudHost('[CRUD:Host] ngOnInit', {
5012
5294
  initialSize: this.initialSize,
5013
5295
  startMaximized: this.modal.startMaximized,
5014
5296
  fullscreenBreakpoint: this.modal.fullscreenBreakpoint,
@@ -5032,10 +5314,10 @@ class DynamicFormDialogHostComponent {
5032
5314
  }
5033
5315
  }
5034
5316
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, deps: [{ token: MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: DialogService }, { token: i2$1.GenericCrudService }, { token: ASYNC_CONFIG_STORAGE }], target: i0.ɵɵFactoryTarget.Component });
5035
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DynamicFormDialogHostComponent, isStandalone: true, selector: "praxis-dynamic-form-dialog-host", host: { properties: { "attr.data-density": "modal.density || \"default\"" }, classAttribute: "praxis-dialog" }, providers: [GenericCrudService], viewQueries: [{ propertyName: "formComp", first: true, predicate: PraxisDynamicForm, descendants: true }], ngImport: i0, template: `
5317
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DynamicFormDialogHostComponent, isStandalone: true, selector: "praxis-dynamic-form-dialog-host", host: { properties: { "attr.data-density": "modal.density || \"default\"", "attr.data-presentation": "presentation", "class.praxis-drawer": "presentation === \"drawer\"" }, classAttribute: "praxis-dialog" }, providers: [GenericCrudService], viewQueries: [{ propertyName: "formComp", first: true, predicate: PraxisDynamicForm, descendants: true }], ngImport: i0, template: `
5036
5318
  <div mat-dialog-title class="dialog-header">
5037
5319
  <h2 id="crudDialogTitle" class="dialog-title">
5038
- {{ data.action?.label || texts.title }}
5320
+ {{ dialogTitle }}
5039
5321
  </h2>
5040
5322
  <span class="spacer"></span>
5041
5323
  @if (modal.canMaximize) {
@@ -5069,11 +5351,13 @@ class DynamicFormDialogHostComponent {
5069
5351
  [resourceId]="resourceId"
5070
5352
  [initialValue]="initialValue"
5071
5353
  [mode]="mode"
5354
+ [config]="formConfig"
5072
5355
  [schemaUrl]="schemaUrl"
5073
5356
  [submitUrl]="submitUrl"
5074
5357
  [submitMethod]="submitMethod"
5075
5358
  [apiEndpointKey]="apiEndpointKey"
5076
5359
  [apiUrlEntry]="apiUrlEntry"
5360
+ [layoutPolicy]="layoutPolicy"
5077
5361
  [presentationModeGlobal]="mode === 'view' ? true : null"
5078
5362
  [backConfig]="backConfig"
5079
5363
  [actions]="formActions"
@@ -5081,7 +5365,7 @@ class DynamicFormDialogHostComponent {
5081
5365
  (formCancel)="onCancel()"
5082
5366
  ></praxis-dynamic-form>
5083
5367
  </mat-dialog-content>
5084
- `, isInline: true, styles: [":host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}:host([data-density=\"compact\"]){--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}.dialog-header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--dlg-pad);padding:0 var(--dlg-pad);height:var(--dlg-header-h);margin:0;background:var(--md-sys-color-surface-container-high);border-bottom:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface)}.dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}.spacer{flex:1}.dialog-content{overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}.dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}.dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}.dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}\n"], dependencies: [{ kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i6.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "initialValue", "editorialContext", "mode", "config", "actions", "schemaSource", "schemaUrl", "readUrl", "submitUrl", "submitMethod", "responseSchemaUrl", "apiEndpointKey", "apiUrlEntry", "enableCustomization", "showAiAssistant", "formId", "componentInstanceId", "configPersistenceStrategy", "layout", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "domainRules", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "configPatchChange", "formReady", "valueChange", "syncCompleted", "initializationError", "loadingStateChange", "enableCustomizationChange", "customAction", "actionConfirmation", "schemaStatusChange", "fieldRenderError", "ruleDiagnosticsChange"] }] });
5368
+ `, isInline: true, styles: ["praxis-dynamic-form-dialog-host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}praxis-dynamic-form-dialog-host[data-density=compact]{--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}praxis-dynamic-form-dialog-host .dialog-header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--dlg-pad);padding:0 var(--dlg-pad);height:var(--dlg-header-h);margin:0;background:var(--md-sys-color-surface-container-high);border-bottom:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host .dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host .spacer{flex:1}praxis-dynamic-form-dialog-host .dialog-content{flex:1 1 auto;overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}praxis-dynamic-form-dialog-host.praxis-drawer{--dlg-header-h: 64px;width:100%;min-width:0;max-width:100vw;height:100dvh;background:var(--pfx-form-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-header{background:color-mix(in srgb,var(--md-sys-color-surface-container-high, var(--md-sys-color-surface)),transparent 4%)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content{max-height:none;min-height:0;padding:clamp(12px,2.4vw,24px)}praxis-dynamic-form-dialog-host .dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}praxis-dynamic-form-dialog-host .dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}.pfx-blur-backdrop{background-color:var(--pfx-backdrop, rgba(15, 23, 42, .42))!important;backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%)}.pfx-transparent-backdrop{background-color:transparent!important}.cdk-overlay-pane.pfx-dialog-pane{overflow:hidden;transition:width .2s ease,height .2s ease,margin .2s ease}.cdk-overlay-pane.pfx-dialog-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane .mdc-dialog__surface{display:flex;flex-direction:column;width:100%;height:100%;max-height:inherit;overflow:hidden;background:var(--pfx-form-surface, var(--md-sys-color-surface))!important;border:1px solid var(--pfx-form-stroke, var(--md-sys-color-outline-variant))!important}.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mdc-dialog__surface{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.cdk-overlay-pane.pfx-drawer-pane{margin:0!important;border-radius:0!important;box-shadow:var(--md-sys-elevation-level3, 0 24px 80px rgba(15, 23, 42, .28)),0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline, #64748b) 24%,transparent)}.cdk-overlay-pane.pfx-drawer-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{border-radius:0!important}\n"], dependencies: [{ kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i6.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "initialValue", "editorialContext", "mode", "config", "actions", "schemaSource", "schemaUrl", "readUrl", "submitUrl", "submitMethod", "responseSchemaUrl", "apiEndpointKey", "apiUrlEntry", "enableCustomization", "showAiAssistant", "formId", "componentInstanceId", "configPersistenceStrategy", "layout", "generatedLayoutPreset", "layoutPolicy", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "fieldIconPolicy", "domainRules", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "configPatchChange", "formReady", "valueChange", "syncCompleted", "initializationError", "loadingStateChange", "enableCustomizationChange", "customAction", "actionConfirmation", "schemaStatusChange", "fieldRenderError", "ruleDiagnosticsChange"] }], encapsulation: i0.ViewEncapsulation.None });
5085
5369
  }
5086
5370
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
5087
5371
  type: Component,
@@ -5091,13 +5375,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5091
5375
  MatIconModule,
5092
5376
  PraxisIconDirective,
5093
5377
  PraxisDynamicForm
5094
- ], providers: [GenericCrudService], host: {
5378
+ ], encapsulation: ViewEncapsulation.None, providers: [GenericCrudService], host: {
5095
5379
  class: 'praxis-dialog',
5096
5380
  '[attr.data-density]': 'modal.density || "default"',
5381
+ '[attr.data-presentation]': 'presentation',
5382
+ '[class.praxis-drawer]': 'presentation === "drawer"',
5097
5383
  }, template: `
5098
5384
  <div mat-dialog-title class="dialog-header">
5099
5385
  <h2 id="crudDialogTitle" class="dialog-title">
5100
- {{ data.action?.label || texts.title }}
5386
+ {{ dialogTitle }}
5101
5387
  </h2>
5102
5388
  <span class="spacer"></span>
5103
5389
  @if (modal.canMaximize) {
@@ -5131,11 +5417,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5131
5417
  [resourceId]="resourceId"
5132
5418
  [initialValue]="initialValue"
5133
5419
  [mode]="mode"
5420
+ [config]="formConfig"
5134
5421
  [schemaUrl]="schemaUrl"
5135
5422
  [submitUrl]="submitUrl"
5136
5423
  [submitMethod]="submitMethod"
5137
5424
  [apiEndpointKey]="apiEndpointKey"
5138
5425
  [apiUrlEntry]="apiUrlEntry"
5426
+ [layoutPolicy]="layoutPolicy"
5139
5427
  [presentationModeGlobal]="mode === 'view' ? true : null"
5140
5428
  [backConfig]="backConfig"
5141
5429
  [actions]="formActions"
@@ -5143,7 +5431,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5143
5431
  (formCancel)="onCancel()"
5144
5432
  ></praxis-dynamic-form>
5145
5433
  </mat-dialog-content>
5146
- `, styles: [":host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}:host([data-density=\"compact\"]){--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}.dialog-header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--dlg-pad);padding:0 var(--dlg-pad);height:var(--dlg-header-h);margin:0;background:var(--md-sys-color-surface-container-high);border-bottom:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface)}.dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}.spacer{flex:1}.dialog-content{overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}.dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}.dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}.dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}\n"] }]
5434
+ `, styles: ["praxis-dynamic-form-dialog-host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}praxis-dynamic-form-dialog-host[data-density=compact]{--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}praxis-dynamic-form-dialog-host .dialog-header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--dlg-pad);padding:0 var(--dlg-pad);height:var(--dlg-header-h);margin:0;background:var(--md-sys-color-surface-container-high);border-bottom:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host .dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host .spacer{flex:1}praxis-dynamic-form-dialog-host .dialog-content{flex:1 1 auto;overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}praxis-dynamic-form-dialog-host.praxis-drawer{--dlg-header-h: 64px;width:100%;min-width:0;max-width:100vw;height:100dvh;background:var(--pfx-form-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-header{background:color-mix(in srgb,var(--md-sys-color-surface-container-high, var(--md-sys-color-surface)),transparent 4%)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content{max-height:none;min-height:0;padding:clamp(12px,2.4vw,24px)}praxis-dynamic-form-dialog-host .dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:var(--md-sys-color-primary-container)}praxis-dynamic-form-dialog-host .dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}.pfx-blur-backdrop{background-color:var(--pfx-backdrop, rgba(15, 23, 42, .42))!important;backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%);-webkit-backdrop-filter:blur(var(--pfx-backdrop-blur, 10px)) saturate(110%)}.pfx-transparent-backdrop{background-color:transparent!important}.cdk-overlay-pane.pfx-dialog-pane{overflow:hidden;transition:width .2s ease,height .2s ease,margin .2s ease}.cdk-overlay-pane.pfx-dialog-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane .mdc-dialog__surface{display:flex;flex-direction:column;width:100%;height:100%;max-height:inherit;overflow:hidden;background:var(--pfx-form-surface, var(--md-sys-color-surface))!important;border:1px solid var(--pfx-form-stroke, var(--md-sys-color-outline-variant))!important}.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-dialog-pane.pfx-dialog-frosted .mdc-dialog__surface{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.cdk-overlay-pane.pfx-drawer-pane{margin:0!important;border-radius:0!important;box-shadow:var(--md-sys-elevation-level3, 0 24px 80px rgba(15, 23, 42, .28)),0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline, #64748b) 24%,transparent)}.cdk-overlay-pane.pfx-drawer-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{border-radius:0!important}\n"] }]
5147
5435
  }], ctorParameters: () => [{ type: undefined, decorators: [{
5148
5436
  type: Inject,
5149
5437
  args: [MatDialogRef]
@@ -5180,6 +5468,61 @@ function deriveCreateSubmitLabel(actionLabel) {
5180
5468
  }
5181
5469
  return actionLabel;
5182
5470
  }
5471
+ function deriveModeTitle(mode, resourceTitle) {
5472
+ if (!resourceTitle) {
5473
+ return undefined;
5474
+ }
5475
+ const singular = lowerFirstLetter(singularizePtBrResourceTitle(resourceTitle));
5476
+ if (mode === 'create') {
5477
+ return `Adicionar ${singular}`;
5478
+ }
5479
+ if (mode === 'edit') {
5480
+ return `Editar ${singular}`;
5481
+ }
5482
+ return `Visualizar ${singular}`;
5483
+ }
5484
+ function titleFromResourcePath(resourcePath) {
5485
+ const lastSegment = resourcePath?.split('/').filter(Boolean).pop();
5486
+ if (!lastSegment) {
5487
+ return undefined;
5488
+ }
5489
+ return lastSegment
5490
+ .replace(/[-_]+/g, ' ')
5491
+ .replace(/\s+/g, ' ')
5492
+ .trim()
5493
+ .toLocaleLowerCase('pt-BR');
5494
+ }
5495
+ function singularizePtBrResourceTitle(title) {
5496
+ const normalized = title.trim();
5497
+ const lower = normalized.toLocaleLowerCase('pt-BR');
5498
+ if (lower === 'códigos de frequência' || lower === 'codigos de frequencia') {
5499
+ return 'código de frequência';
5500
+ }
5501
+ const [first, ...rest] = normalized.split(/\s+/);
5502
+ const singularFirst = singularizePtBrWord(first);
5503
+ return [singularFirst, ...rest].join(' ');
5504
+ }
5505
+ function singularizePtBrWord(word) {
5506
+ if (/ões$/i.test(word)) {
5507
+ return word.replace(/ões$/i, 'ão');
5508
+ }
5509
+ if (/ais$/i.test(word)) {
5510
+ return word.replace(/ais$/i, 'al');
5511
+ }
5512
+ if (/eis$/i.test(word)) {
5513
+ return word.replace(/eis$/i, 'el');
5514
+ }
5515
+ if (/res$/i.test(word)) {
5516
+ return word.replace(/es$/i, '');
5517
+ }
5518
+ if (/s$/i.test(word) && !/ss$/i.test(word)) {
5519
+ return word.replace(/s$/i, '');
5520
+ }
5521
+ return word;
5522
+ }
5523
+ function lowerFirstLetter(value) {
5524
+ return value.replace(/\p{L}/u, (letter) => letter.toLocaleLowerCase('pt-BR'));
5525
+ }
5183
5526
 
5184
5527
  var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
5185
5528
  __proto__: null,
@@ -5325,11 +5668,21 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
5325
5668
  type: 'PraxisDataQueryContext | null',
5326
5669
  description: 'Contexto semântico de consulta encaminhado para a tabela interna do CRUD. Preferir este contrato para novo authoring remoto.',
5327
5670
  },
5671
+ {
5672
+ name: 'metadata.resource.schemaPath',
5673
+ type: 'string | null',
5674
+ description: 'Template canonico opcional usado apenas para resolver /schemas/filtered quando resource.path precisa ser um alvo operacional concreto.',
5675
+ },
5328
5676
  {
5329
5677
  name: 'metadata.filterCriteria',
5330
5678
  type: 'Record<string, unknown> | null',
5331
5679
  description: 'Bridge declarativa de filtros encaminhada para a tabela interna. Para novo authoring, prefira metadata.queryContext.',
5332
5680
  },
5681
+ {
5682
+ name: 'metadata.form',
5683
+ type: 'FormConfig',
5684
+ description: 'Configuração do formulário encaminhada para o PraxisDynamicForm aberto por ações modal/drawer; layout, fieldMetadata, actions e helpPresentation permanecem propriedade do Dynamic Form.',
5685
+ },
5333
5686
  {
5334
5687
  name: 'metadata.actions[].form',
5335
5688
  type: 'CrudActionFormContract',
@@ -5699,6 +6052,7 @@ const CRUD_AI_CAPABILITIES = {
5699
6052
  { path: 'actions[].params[].to', category: 'actions', valueKind: 'enum', allowedValues: ENUMS.paramTarget, description: 'Destino do parametro.' },
5700
6053
  { path: 'actions[].params[].name', category: 'actions', valueKind: 'string', description: 'Nome do parametro no destino.' },
5701
6054
  { path: 'actions[].form.initialValue', category: 'actions', valueKind: 'object', description: 'Seed fixo do formulario injetado em inputs.initialValue antes da abertura.' },
6055
+ { path: 'actions[].form.layoutPolicy', category: 'actions', valueKind: 'object', description: 'Politica schema-driven opt-in encaminhada ao Dynamic Form para materializar detalhe ou command form sem FormConfig.sections local; deve acompanhar schemaUrl compativel, especialmente schemaType=request para comandos.' },
5702
6056
  { path: 'actions[].back', category: 'navigation', valueKind: 'object', description: 'BackConfig por acao.' },
5703
6057
  { path: 'actions[].back.strategy', category: 'navigation', valueKind: 'enum', allowedValues: ENUMS.backStrategy, description: 'Estrategia de retorno da acao (auto, close, navigate).' },
5704
6058
  { path: 'actions[].back.returnTo', category: 'navigation', valueKind: 'string', description: 'Rota ou destino usado quando a estrategia de retorno for navigate.' },
@@ -5736,6 +6090,7 @@ const surfaceConfigureSchema = {
5736
6090
  submitMethod: { enum: ['post', 'put', 'patch', 'delete'] },
5737
6091
  apiEndpointKey: { type: 'string' },
5738
6092
  initialValue: { type: 'object' },
6093
+ layoutPolicy: { type: 'object' },
5739
6094
  },
5740
6095
  },
5741
6096
  params: {
@@ -5854,14 +6209,14 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5854
6209
  { name: 'afterDelete', type: '{ id: string | number }', description: 'Emitted after delete; CRUD refetches the list.' },
5855
6210
  ],
5856
6211
  editableTargets: [
5857
- { kind: 'resourceBinding', resolver: 'crud-resource-by-path-or-key', description: 'Resource path/key, id field, endpoint key and query context owned by CRUD orchestration.' },
6212
+ { kind: 'resourceBinding', resolver: 'crud-resource-by-path-or-key', description: 'Resource path/key, optional schema path template, id field, endpoint key and query context owned by CRUD orchestration.' },
5858
6213
  { kind: 'listSurface', resolver: 'crud-list-surface', description: 'CRUD-hosted list surface and table delegation boundary.' },
5859
6214
  { kind: 'createSurface', resolver: 'crud-action-by-id:create', description: 'Create action open mode, route/form binding and launcher inputs.' },
5860
6215
  { kind: 'editSurface', resolver: 'crud-action-by-id:edit', description: 'Edit action open mode, route/form binding and launcher inputs.' },
5861
6216
  { kind: 'viewSurface', resolver: 'crud-action-by-id:view', description: 'View action open mode, route/form binding and launcher inputs.' },
5862
6217
  { kind: 'deleteBehavior', resolver: 'crud-action-by-id:delete', description: 'Delete action enablement, confirmation, endpoint and capability policy.' },
5863
- { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', description: 'Route/modal/drawer defaults consumed by CrudLauncherService and DynamicFormDialogHostComponent.' },
5864
- { kind: 'formBinding', resolver: 'crud-action-form-contract-by-action-id', description: 'CRUD-owned form binding fields: formId, schemaUrl, submitUrl, submitMethod, params and initialValue.' },
6218
+ { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', description: 'Route/modal/drawer defaults consumed by CrudLauncherService and DynamicFormDialogHostComponent, including contextual shell titles derived from action label, form title or resource metadata.' },
6219
+ { kind: 'formBinding', resolver: 'crud-action-form-contract-by-action-id', description: 'CRUD-owned form binding fields: formId, schemaUrl, submitUrl, submitMethod, params and initialValue. Shared FormConfig remains owned by Dynamic Form and is forwarded through CrudMetadata.form when present.' },
5865
6220
  { kind: 'permissions', resolver: 'crud-resource-capabilities', description: 'CRUD action availability derived from resource capabilities and action permissions.' },
5866
6221
  { kind: 'domainGovernanceContext', resolver: 'domain-catalog-context-by-resource-key', description: 'Read-only semantic/governance context resolved from Domain Catalog and referenced from CRUD queryContext.meta.' },
5867
6222
  { kind: 'childOperation', resolver: 'child-authoring-manifest-operation', description: 'Delegated form/table/dialog/settings-panel operation owned by the child component manifest.' },
@@ -5877,13 +6232,13 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
5877
6232
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-resource-bind', handlerContract: {
5878
6233
  reads: ['CrudMetadata.resource', 'api_metadata', 'ResourceDiscoveryService', 'GET /{resource}/capabilities'],
5879
6234
  writes: ['CrudMetadata.resource', 'CrudMetadata.queryContext', 'PraxisCrudComponent.tableCrudContext'],
5880
- identityKeys: ['resourcePath', 'resourceKey'],
6235
+ identityKeys: ['resourcePath', 'resourceKey', 'schemaPath'],
5881
6236
  inputSchema: resourceBindSchema,
5882
6237
  failureModes: ['resource-not-found', 'schema-url-not-canonical', 'capabilities-unavailable', 'id-field-missing'],
5883
- description: 'Binds CRUD to a canonical resource and validates it against api_metadata/resource capabilities before runtime use.',
6238
+ description: 'Binds CRUD to a canonical resource and validates it against api_metadata/resource capabilities before runtime use. Use CrudMetadata.resource.schemaPath when schema discovery must use a templated path while resource.path targets a concrete nested collection.',
5884
6239
  } }],
5885
6240
  validators: ['resource-exists-in-api-metadata', 'resource-path-canonical', 'resource-key-stable', 'id-field-known', 'resource-capabilities-resolvable'],
5886
- affectedPaths: ['resource.path', 'resource.idField', 'resource.endpointKey', 'queryContext', 'filterCriteria'],
6241
+ affectedPaths: ['resource.path', 'resource.schemaPath', 'resource.idField', 'resource.endpointKey', 'resource.apiUrlEntry', 'queryContext', 'filterCriteria'],
5887
6242
  submissionImpact: 'affects-remote-binding',
5888
6243
  preconditions: ['crud-metadata-loaded', 'api-metadata-available'],
5889
6244
  },
@@ -6017,14 +6372,14 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6017
6372
  target: { kind: 'dialogHost', resolver: 'crud-dialog-host-defaults', ambiguityPolicy: 'fail', required: true },
6018
6373
  inputSchema: dialogHostSchema,
6019
6374
  effects: [{ kind: 'compile-domain-patch', handler: 'crud-dialog-host-set', handlerContract: {
6020
- reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'CRUD_DRAWER_ADAPTER'],
6375
+ reads: ['CrudMetadata.defaults', 'CrudLauncherService.resolveOpenMode', 'DialogService', 'DynamicFormDialogHostComponent', 'CRUD_DRAWER_ADAPTER'],
6021
6376
  writes: ['CrudMetadata.defaults.openMode', 'CrudMetadata.defaults.modal', 'CrudMetadata.defaults.back'],
6022
6377
  identityKeys: ['crudId'],
6023
6378
  inputSchema: dialogHostSchema,
6024
- failureModes: ['open-mode-unsupported', 'drawer-adapter-missing', 'modal-size-invalid', 'back-policy-invalid'],
6025
- description: 'Configures CRUD-owned route/modal/drawer defaults consumed by the launcher and dialog host.',
6379
+ failureModes: ['open-mode-unsupported', 'drawer-runtime-unavailable', 'modal-size-invalid', 'back-policy-invalid'],
6380
+ description: 'Configures CRUD-owned route/modal/drawer defaults consumed by the launcher, default dialog-backed drawer runtime and optional host drawer adapter.',
6026
6381
  } }],
6027
- validators: ['open-mode-supported', 'modal-size-valid', 'drawer-adapter-available-when-needed', 'back-policy-valid', 'settings-panel-shell-compatible'],
6382
+ validators: ['open-mode-supported', 'modal-size-valid', 'drawer-runtime-available', 'back-policy-valid', 'settings-panel-shell-compatible'],
6028
6383
  affectedPaths: ['defaults.openMode', 'defaults.modal', 'defaults.back'],
6029
6384
  submissionImpact: 'config-only',
6030
6385
  preconditions: ['crud-metadata-loaded'],
@@ -6100,7 +6455,7 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6100
6455
  { validatorId: 'permissions-delete-valid', level: 'error', code: 'CRUD_DELETE_PERMISSION_VALID', description: 'Delete permission cannot bypass resource capabilities or confirmation policy.' },
6101
6456
  { validatorId: 'open-mode-supported', level: 'error', code: 'CRUD_OPEN_MODE_SUPPORTED', description: 'Open mode must be route, modal or drawer.' },
6102
6457
  { validatorId: 'modal-size-valid', level: 'error', code: 'CRUD_MODAL_SIZE_VALID', description: 'Modal sizing defaults must be valid DialogConfig values.' },
6103
- { validatorId: 'drawer-adapter-available-when-needed', level: 'error', code: 'CRUD_DRAWER_ADAPTER_AVAILABLE', description: 'Drawer open mode requires a host-provided drawer adapter.' },
6458
+ { validatorId: 'drawer-runtime-available', level: 'error', code: 'CRUD_DRAWER_RUNTIME_AVAILABLE', description: 'Drawer open mode uses the default dialog-backed drawer runtime with CRUD-owned chrome, responsive right-panel sizing, Escape/backdrop handling and semantic close/save results; a host drawer adapter is optional only for shell-specific presentation.' },
6104
6459
  { validatorId: 'back-policy-valid', level: 'error', code: 'CRUD_BACK_POLICY_VALID', description: 'Back policy must be valid for route/modal/drawer behavior.' },
6105
6460
  { validatorId: 'settings-panel-shell-compatible', level: 'warning', code: 'CRUD_SETTINGS_PANEL_COMPATIBLE', description: 'Authoring shell must preserve apply/save/reset semantics.' },
6106
6461
  { validatorId: 'action-permission-supported', level: 'error', code: 'CRUD_ACTION_PERMISSION_SUPPORTED', description: 'Action permissions must map to supported resource capabilities.' },