@praxisui/crud 9.0.4-rc.9 → 9.0.4

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,12 +1,12 @@
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, ViewEncapsulation, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, InjectionToken, inject, input, signal, computed, effect, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, ChangeDetectorRef, Injector, ViewChild, Output, Input, Optional, 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';
6
6
  import { MatSnackBar } from '@angular/material/snack-bar';
7
7
  import { firstValueFrom, BehaviorSubject, Subscription } from 'rxjs';
8
8
  import * as i2$1 from '@praxisui/core';
9
- import { ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, createDefaultTableConfig, EnterpriseRuntimeContextService, GLOBAL_SURFACE_SERVICE, SurfaceOutletRegistryService, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, translateUnavailableWorkflowMessage, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, PraxisIconButtonComponent, PraxisResourceIdentityComponent, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
9
+ import { ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, LoggerService, SurfaceNavigationError, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, createDefaultTableConfig, EnterpriseRuntimeContextService, GLOBAL_SURFACE_SERVICE, SurfaceOutletRegistryService, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, normalizeSurfaceOperationContext, translateUnavailableWorkflowMessage, isSurfaceNavigationError, translateSurfaceNavigationRejected, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, SURFACE_DRAWER_REF, SURFACE_DRAWER_CONTENT_DATA, PraxisIconButtonComponent, PraxisResourceIdentityComponent, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
10
10
  import { SettingsPanelService } from '@praxisui/settings-panel';
11
11
  import { PraxisTableInlineAuthoringEditorComponent, PraxisTable } from '@praxisui/table';
12
12
  import { ConfirmDialogComponent } from '@praxisui/dynamic-fields';
@@ -97,12 +97,44 @@ function debugCrudLauncher(message, ...data) {
97
97
  }
98
98
  catch { }
99
99
  }
100
+ /**
101
+ * Captures the interaction origin before metadata, capability, or lazy-host
102
+ * work yields the event loop. Material can restore focus to an explicit
103
+ * element, which is essential when the action originated in a closing menu.
104
+ */
105
+ function captureCrudActionFocusOrigin() {
106
+ if (typeof document === 'undefined') {
107
+ return null;
108
+ }
109
+ const active = document.activeElement;
110
+ if (!(active instanceof HTMLElement) || active === document.body || !active.isConnected) {
111
+ return null;
112
+ }
113
+ return active;
114
+ }
115
+ function isRuntimeFocusOrigin(value) {
116
+ return typeof HTMLElement !== 'undefined'
117
+ && value instanceof HTMLElement
118
+ && typeof value.focus === 'function';
119
+ }
120
+ function resolveCrudRestoreFocus(configured, actionFocusOrigin) {
121
+ if (configured === false || typeof configured === 'string') {
122
+ return configured;
123
+ }
124
+ if (typeof HTMLElement !== 'undefined' && configured instanceof HTMLElement) {
125
+ return configured;
126
+ }
127
+ return actionFocusOrigin ?? true;
128
+ }
100
129
  class CrudLauncherService {
101
130
  router = inject(Router);
102
131
  dialog = inject(DialogService);
103
132
  storage = inject(ASYNC_CONFIG_STORAGE);
104
133
  global = inject(GlobalConfigService);
105
134
  operationResolver = inject(CrudOperationResolutionService);
135
+ logger = inject(LoggerService, { optional: true });
136
+ dialogHostPromise;
137
+ nextSurfaceFrameId = 0;
106
138
  drawerAdapter = (() => {
107
139
  try {
108
140
  return inject(CRUD_DRAWER_ADAPTER);
@@ -112,6 +144,18 @@ class CrudLauncherService {
112
144
  }
113
145
  })();
114
146
  async launch(action, row, metadata, componentKeyId, drawerCallbacks, runtime) {
147
+ // Capture before every await below. A toolbar/row menu closes before the
148
+ // form host is resolved, so the browser's later activeElement is not a
149
+ // reliable representation of the user-facing action trigger.
150
+ // A responsive host can replace the menu trigger while the menu is
151
+ // closing. Keep the original runtime-only reference even when detached:
152
+ // the overlay host resolves its stable data-praxis-focus-key on close.
153
+ const actionFocusOrigin = isRuntimeFocusOrigin(runtime?.focusOrigin)
154
+ ? runtime.focusOrigin
155
+ : captureCrudActionFocusOrigin();
156
+ const prefetchedDialogHost = this.isLikelyOverlayAction(action, metadata)
157
+ ? this.loadDialogHost()
158
+ : undefined;
115
159
  // Carregar overrides de CRUD (se houver) e mesclar em uma cópia local
116
160
  const merged = await this.mergeCrudOverrides(metadata, action, componentKeyId || undefined);
117
161
  merged.action = this.normalizeActionForLaunch(merged.action);
@@ -137,21 +181,71 @@ class CrudLauncherService {
137
181
  row[idField] !== undefined) {
138
182
  inputs[idField] = row[idField];
139
183
  }
140
- const resourceIdentity = actionForLaunch.action === 'edit'
184
+ const resourceIdentity = actionForLaunch.action === 'edit' || actionForLaunch.action === 'view'
141
185
  ? runtime?.resourceIdentity ?? null
142
186
  : null;
187
+ const contextIdentity = runtime?.contextIdentity ?? null;
188
+ const dialogData = {
189
+ action: actionForLaunch,
190
+ row,
191
+ metadata: merged.metadata,
192
+ inputs,
193
+ presentation: mode === 'drawer' ? 'drawer' : 'modal',
194
+ resourceIdentity,
195
+ contextIdentity,
196
+ };
197
+ const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
198
+ const restoreFocus = resolveCrudRestoreFocus(modalCfg.restoreFocus, actionFocusOrigin);
199
+ if (mode === 'drawer' && runtime?.surfaceRuntime?.push) {
200
+ const frameId = this.buildSurfaceFrameId(actionForLaunch);
201
+ const frameRef = await runtime.surfaceRuntime.push({
202
+ id: frameId,
203
+ title: this.resolveSurfaceFrameTitle(actionForLaunch, merged.metadata),
204
+ titleIcon: stringOrUndefined$1(actionForLaunch['icon']),
205
+ subtitle: this.resolveSurfaceFrameSubtitle(actionForLaunch, merged.metadata),
206
+ ...(actionFocusOrigin
207
+ ? { returnFocusTo: actionFocusOrigin }
208
+ : {}),
209
+ content: {
210
+ component: await (prefetchedDialogHost ?? this.loadDialogHost()),
211
+ inputs: { data: dialogData },
212
+ },
213
+ });
214
+ if (!frameRef) {
215
+ const error = new SurfaceNavigationError('SURFACE_SESSION_NAVIGATION_REJECTED', 'push', frameId);
216
+ this.logger?.warn('Surface session rejected nested CRUD frame.', {
217
+ context: {
218
+ lib: '@praxisui/crud',
219
+ component: 'CrudLauncherService',
220
+ actionId: `crud.${String(actionForLaunch.action || 'unknown')}`,
221
+ },
222
+ data: {
223
+ code: error.code,
224
+ operation: error.operation,
225
+ frameId: error.frameId,
226
+ formId: actionForLaunch.formId,
227
+ openMode: mode,
228
+ },
229
+ throttleKey: `${error.code}:crud:${frameId}`,
230
+ });
231
+ throw error;
232
+ }
233
+ this.bindSurfaceFrameLifecycle(frameRef, drawerCallbacks);
234
+ return { mode };
235
+ }
143
236
  if (mode === 'drawer' && this.drawerAdapter) {
144
237
  await Promise.resolve(this.drawerAdapter.open({
145
238
  action: actionForLaunch,
146
239
  metadata: merged.metadata,
147
240
  inputs,
148
241
  resourceIdentity,
242
+ contextIdentity,
243
+ restoreFocus,
149
244
  onClose: drawerCallbacks?.onClose,
150
245
  onResult: drawerCallbacks?.onResult,
151
246
  }));
152
247
  return { mode };
153
248
  }
154
- const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
155
249
  debugCrudLauncher('[CRUD:Launcher] opening dialog with:', {
156
250
  action: merged.action.action,
157
251
  formId: actionForLaunch.formId,
@@ -177,12 +271,12 @@ class CrudLauncherService {
177
271
  backdropClasses.push('pfx-transparent-backdrop');
178
272
  }
179
273
  const mergedBackdropClasses = mergeClassList(backdropClasses, modalCfg.backdropClass);
180
- const ref = await this.dialog.openAsync(() => Promise.resolve().then(function () { return dynamicFormDialogHost_component; }).then((m) => m.DynamicFormDialogHostComponent), {
274
+ const ref = await this.dialog.openAsync(() => prefetchedDialogHost ?? this.loadDialogHost(), {
181
275
  ...modalCfg,
182
276
  panelClass: panelClasses,
183
277
  backdropClass: mergedBackdropClasses,
184
278
  autoFocus: modalCfg.autoFocus ?? true,
185
- restoreFocus: modalCfg.restoreFocus ?? true,
279
+ restoreFocus,
186
280
  minWidth: drawerMode
187
281
  ? (modalCfg.minWidth ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.minWidth)
188
282
  : '360px',
@@ -201,13 +295,9 @@ class CrudLauncherService {
201
295
  position: dialogPosition,
202
296
  ariaLabelledBy: 'crudDialogTitle',
203
297
  data: {
204
- action: actionForLaunch,
205
- row,
206
- metadata: merged.metadata,
207
- inputs,
298
+ ...dialogData,
208
299
  presentation: drawerMode ? 'drawer' : 'modal',
209
300
  dialogPosition,
210
- resourceIdentity,
211
301
  },
212
302
  });
213
303
  if (drawerMode) {
@@ -256,6 +346,13 @@ class CrudLauncherService {
256
346
  return 'route';
257
347
  }
258
348
  }
349
+ isLikelyOverlayAction(action, metadata) {
350
+ const configuredMode = action.openMode ?? metadata.defaults?.openMode;
351
+ return configuredMode === 'drawer' || configuredMode === 'modal';
352
+ }
353
+ loadDialogHost() {
354
+ return this.dialogHostPromise ??= Promise.resolve().then(function () { return dynamicFormDialogHost_component; }).then((module) => module.DynamicFormDialogHostComponent);
355
+ }
259
356
  buildRoute(action, row, metadata) {
260
357
  let route = action.route;
261
358
  const query = {};
@@ -468,8 +565,51 @@ class CrudLauncherService {
468
565
  preset: 'groupedCommand',
469
566
  persistence: 'transient',
470
567
  schemaType: 'request',
568
+ groupedCommand: {
569
+ partialRowStrategy: 'fill-compatible',
570
+ },
471
571
  };
472
572
  }
573
+ buildSurfaceFrameId(action) {
574
+ this.nextSurfaceFrameId += 1;
575
+ return `crud:${action.action}:${action.formId || 'form'}:${this.nextSurfaceFrameId}`;
576
+ }
577
+ resolveSurfaceFrameTitle(action, metadata) {
578
+ return stringOrUndefined$1(action['label'])
579
+ ?? stringOrUndefined$1(metadata.form?.['title'])
580
+ ?? stringOrUndefined$1(metadata.table?.['title'])
581
+ ?? (action.action === 'create'
582
+ ? 'Adicionar registro'
583
+ : action.action === 'view'
584
+ ? 'Consultar registro'
585
+ : 'Editar registro');
586
+ }
587
+ resolveSurfaceFrameSubtitle(action, metadata) {
588
+ return stringOrUndefined$1(action['description'])
589
+ ?? stringOrUndefined$1(action['tooltip'])
590
+ ?? stringOrUndefined$1(metadata.form?.['description'])
591
+ ?? stringOrUndefined$1(metadata.table?.['subtitle']);
592
+ }
593
+ bindSurfaceFrameLifecycle(frameRef, callbacks) {
594
+ let semanticResultHandled = false;
595
+ const handleResult = (value) => {
596
+ if (semanticResultHandled)
597
+ return;
598
+ const result = toCrudDrawerResult(value);
599
+ if (!result || result.type === 'close')
600
+ return;
601
+ semanticResultHandled = true;
602
+ callbacks?.onResult?.(result);
603
+ };
604
+ frameRef.result$?.pipe(take(1)).subscribe(handleResult);
605
+ frameRef.closed$.pipe(take(1)).subscribe((value) => {
606
+ handleResult(value);
607
+ callbacks?.onClose?.();
608
+ if (!semanticResultHandled) {
609
+ callbacks?.onResult?.({ type: 'close' });
610
+ }
611
+ });
612
+ }
473
613
  async mergeCrudOverrides(metadata, action, componentKeyId) {
474
614
  try {
475
615
  if (!componentKeyId)
@@ -551,6 +691,10 @@ function toCrudDrawerResult(value) {
551
691
  ? result
552
692
  : undefined;
553
693
  }
694
+ function stringOrUndefined$1(value) {
695
+ const text = String(value ?? '').trim();
696
+ return text || undefined;
697
+ }
554
698
 
555
699
  const DOCUMENT_KIND = 'praxis.crud.editor';
556
700
  const DOCUMENT_VERSION = 1;
@@ -888,7 +1032,7 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
888
1032
  'crud.emptyState.primaryAction': 'Configurar metadados',
889
1033
  'crud.table.emptyState.initial.title': 'Sem registros em {label}',
890
1034
  'crud.table.emptyState.initial.titleFallback': 'Nenhum registro disponível.',
891
- 'crud.table.emptyState.initial.descriptionWithAction': 'Use a ação principal para adicionar o primeiro registro quando houver informações para cadastrar.',
1035
+ 'crud.table.emptyState.initial.descriptionWithToolbarAction': 'Use "{action}" na barra da tabela para adicionar o primeiro registro quando houver informações para cadastrar.',
892
1036
  'crud.table.emptyState.filtered.title': 'Nenhum resultado encontrado.',
893
1037
  'crud.table.emptyState.filtered.description': 'Revise os filtros ou ajuste o termo de busca.',
894
1038
  'crud.preferences.resetSuccess': 'Overrides de CRUD redefinidos',
@@ -896,7 +1040,12 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
896
1040
  'crud.actions.view': 'Ver',
897
1041
  'crud.actions.edit': 'Editar',
898
1042
  'crud.actions.delete': 'Excluir',
1043
+ 'crud.actions.default': 'ação',
1044
+ 'crud.actions.opening': 'Abrindo {action}…',
1045
+ 'crud.surface.openFailed': 'Não foi possível abrir {title}. Tente novamente ou confirme se esse recurso continua disponível.',
899
1046
  'crud.dialog.recordContextLabel': 'Registro em edição',
1047
+ 'crud.dialog.viewRecordContextLabel': 'Registro em consulta',
1048
+ 'crud.dialog.relatedContextLabel': 'Vinculado a',
900
1049
  'crud.delete.confirmMessage': 'Esta ação não pode ser desfeita. Deseja continuar?',
901
1050
  'crud.delete.cancel': 'Cancelar',
902
1051
  },
@@ -906,7 +1055,7 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
906
1055
  'crud.emptyState.primaryAction': 'Configure metadata',
907
1056
  'crud.table.emptyState.initial.title': 'No records in {label}',
908
1057
  'crud.table.emptyState.initial.titleFallback': 'No records available.',
909
- 'crud.table.emptyState.initial.descriptionWithAction': 'Use the primary action to add the first record when there is information to register.',
1058
+ 'crud.table.emptyState.initial.descriptionWithToolbarAction': 'Use "{action}" in the table toolbar to add the first record when there is information to register.',
910
1059
  'crud.table.emptyState.filtered.title': 'No results found.',
911
1060
  'crud.table.emptyState.filtered.description': 'Review the filters or adjust the search term.',
912
1061
  'crud.preferences.resetSuccess': 'CRUD overrides reset',
@@ -914,7 +1063,12 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
914
1063
  'crud.actions.view': 'View',
915
1064
  'crud.actions.edit': 'Edit',
916
1065
  'crud.actions.delete': 'Delete',
1066
+ 'crud.actions.default': 'action',
1067
+ 'crud.actions.opening': 'Opening {action}…',
1068
+ 'crud.surface.openFailed': 'Could not open {title}. Try again or confirm that this resource is still available.',
917
1069
  'crud.dialog.recordContextLabel': 'Record being edited',
1070
+ 'crud.dialog.viewRecordContextLabel': 'Record being viewed',
1071
+ 'crud.dialog.relatedContextLabel': 'Related to',
918
1072
  'crud.delete.confirmMessage': 'This action cannot be undone. Do you want to continue?',
919
1073
  'crud.delete.cancel': 'Cancel',
920
1074
  },
@@ -3643,6 +3797,8 @@ class PraxisCrudComponent {
3643
3797
  metadata;
3644
3798
  crudId;
3645
3799
  componentInstanceId;
3800
+ /** Controls whether the CRUD's internal table trusts host input or stored user configuration. */
3801
+ tableConfigPersistenceStrategy = 'local-first';
3646
3802
  context;
3647
3803
  enableCustomization = false;
3648
3804
  /** Capability publica exigida para authoring governado do CRUD e da tabela interna. */
@@ -3654,6 +3810,8 @@ class PraxisCrudComponent {
3654
3810
  afterDelete = new EventEmitter();
3655
3811
  error = new EventEmitter();
3656
3812
  rowClick = new EventEmitter();
3813
+ /** Emits the canonical table row-action envelope before the CRUD handles its surface/action. */
3814
+ rowAction = new EventEmitter();
3657
3815
  selectionChange = new EventEmitter();
3658
3816
  tableRuntimeConfigChange = new EventEmitter();
3659
3817
  crudAuthoringDocumentApplied = new EventEmitter();
@@ -3664,6 +3822,7 @@ class PraxisCrudComponent {
3664
3822
  tableQueryContext = null;
3665
3823
  tableFilterCriteria = {};
3666
3824
  tableCrudContext;
3825
+ openingActionLabel = signal(null, ...(ngDevMode ? [{ debugName: "openingActionLabel" }] : /* istanbul ignore next */ []));
3667
3826
  launcher = inject(CrudLauncherService);
3668
3827
  http = inject(HttpClient);
3669
3828
  destroyRef = inject(DestroyRef);
@@ -3723,6 +3882,10 @@ class PraxisCrudComponent {
3723
3882
  collectionCapabilitiesRequestSeq = 0;
3724
3883
  currentAuthoringDocument;
3725
3884
  selectedRow = null;
3885
+ onTableRowAction(event) {
3886
+ this.rowAction.emit(event);
3887
+ void this.onAction(event.action || '', event.row, event);
3888
+ }
3726
3889
  getResourceDiscovery() {
3727
3890
  const assigned = this.resourceDiscovery;
3728
3891
  return assigned ?? (this.resourceDiscoveryInstance ??= this.injector.get(ResourceDiscoveryService));
@@ -3815,8 +3978,11 @@ class PraxisCrudComponent {
3815
3978
  }
3816
3979
  }
3817
3980
  async onAction(action, row, runtimeEvent) {
3981
+ if (this.openingActionLabel()) {
3982
+ return;
3983
+ }
3984
+ this.openingActionLabel.set(this.resolveOpeningActionLabel(action, runtimeEvent));
3818
3985
  try {
3819
- document.activeElement?.blur();
3820
3986
  const normalizedAction = this.normalizeCrudActionName(action);
3821
3987
  const contextualRow = row ?? this.resolveSelectedRowForAction(normalizedAction);
3822
3988
  let actionMeta = this.resolvedMetadata.actions?.find((candidate) => this.normalizeCrudActionName(candidate.action) === normalizedAction);
@@ -3842,6 +4008,7 @@ class PraxisCrudComponent {
3842
4008
  action: this.normalizeCrudActionName(actionMeta?.action ?? normalizedAction),
3843
4009
  };
3844
4010
  const resourceIdentity = runtimeEvent?.resourceIdentity ?? null;
4011
+ const contextIdentity = this.resolveTaskScopeIdentity();
3845
4012
  const handledByDuplicateDraft = await this.tryHandleCanonicalDuplicateDraftAction(effectiveAction, contextualRow);
3846
4013
  if (handledByDuplicateDraft) {
3847
4014
  return;
@@ -3890,6 +4057,9 @@ class PraxisCrudComponent {
3890
4057
  : this.collectionCapabilities,
3891
4058
  links: this.resolveCrudRuntimeLinks(effectiveAction.action, contextualRow),
3892
4059
  resourceIdentity,
4060
+ contextIdentity,
4061
+ focusOrigin: runtimeEvent?.focusOrigin ?? null,
4062
+ surfaceRuntime: this.resolveSurfaceRuntime(),
3893
4063
  });
3894
4064
  this.afterOpen.emit({ mode, action: effectiveAction.action });
3895
4065
  if (mode === 'drawer') {
@@ -3920,7 +4090,32 @@ class PraxisCrudComponent {
3920
4090
  }
3921
4091
  catch (err) {
3922
4092
  this.error.emit(err);
4093
+ this.notifySurfaceNavigationFailure(err);
4094
+ }
4095
+ finally {
4096
+ this.openingActionLabel.set(null);
4097
+ }
4098
+ }
4099
+ resolveTaskScopeIdentity() {
4100
+ const operation = normalizeSurfaceOperationContext(this.context?.['operation']);
4101
+ return operation?.taskScope?.identity ?? null;
4102
+ }
4103
+ getOpeningActionStatus(actionLabel) {
4104
+ return this.txWithParams('crud.actions.opening', 'Abrindo {action}…', { action: actionLabel });
4105
+ }
4106
+ resolveOpeningActionLabel(action, runtimeEvent) {
4107
+ const configuredLabel = String(runtimeEvent?.actionConfig?.label || '').trim();
4108
+ if (configuredLabel) {
4109
+ return configuredLabel;
4110
+ }
4111
+ const normalized = this.normalizeCrudActionName(action);
4112
+ if (normalized === 'create'
4113
+ || normalized === 'view'
4114
+ || normalized === 'edit'
4115
+ || normalized === 'delete') {
4116
+ return this.getCrudActionLabel(normalized);
3923
4117
  }
4118
+ return String(action || '').trim() || this.tx('crud.actions.default', 'ação');
3924
4119
  }
3925
4120
  hasExplicitOpenBinding(action) {
3926
4121
  const mode = action.openMode;
@@ -4132,6 +4327,7 @@ class PraxisCrudComponent {
4132
4327
  }, {
4133
4328
  capabilities: this.collectionCapabilities,
4134
4329
  links: this.tableCollectionLinks,
4330
+ surfaceRuntime: this.resolveSurfaceRuntime(),
4135
4331
  });
4136
4332
  this.afterOpen.emit({ mode, action: normalizedAction });
4137
4333
  if (mode !== 'drawer' && ref) {
@@ -4151,6 +4347,14 @@ class PraxisCrudComponent {
4151
4347
  }
4152
4348
  this.table.refetch();
4153
4349
  }
4350
+ resolveSurfaceRuntime() {
4351
+ const candidate = this.context?.['surfaceRuntime'];
4352
+ if (!candidate || typeof candidate !== 'object') {
4353
+ return null;
4354
+ }
4355
+ const runtime = candidate;
4356
+ return typeof runtime.push === 'function' ? runtime : null;
4357
+ }
4154
4358
  tryRefreshMaterializedLocalData() {
4155
4359
  const readUrl = this.resolveMaterializedReadUrl();
4156
4360
  if (!readUrl || this.resolveResourcePath(this.resolvedMetadata)) {
@@ -4316,6 +4520,17 @@ class PraxisCrudComponent {
4316
4520
  const first = Array.isArray(event?.selectedRows) ? event.selectedRows[0] : null;
4317
4521
  return this.isRecord(first) ? first : null;
4318
4522
  }
4523
+ /**
4524
+ * Keeps the DOM trigger exclusively in transient runtime context. Discovery
4525
+ * payloads remain JSON-safe and can still be authored, inspected and
4526
+ * persisted independently from the browser that opened the surface.
4527
+ */
4528
+ resolveRuntimeFocusOrigin(event) {
4529
+ const focusOrigin = event?.focusOrigin;
4530
+ return focusOrigin && typeof focusOrigin.focus === 'function'
4531
+ ? focusOrigin
4532
+ : undefined;
4533
+ }
4319
4534
  extractDiscoveryActionConfig(event) {
4320
4535
  const config = event?.actionConfig;
4321
4536
  return this.isRecord(config) && typeof config['resourceKey'] === 'string'
@@ -4335,9 +4550,6 @@ class PraxisCrudComponent {
4335
4550
  return false;
4336
4551
  }
4337
4552
  const providedSurface = this.resolveProvidedSurface(normalizedAction, this.extractDiscoveryActionConfig(runtimeEvent));
4338
- if (!providedSurface && !this.isDiscoveryManagedCrudAction(normalizedAction)) {
4339
- return false;
4340
- }
4341
4553
  const catalog = providedSurface
4342
4554
  ? null
4343
4555
  : await this.resolveDiscoveredSurfaceCatalog(normalizedAction, row);
@@ -4358,11 +4570,18 @@ class PraxisCrudComponent {
4358
4570
  endpointKey: this.resolvedMetadata?.resource?.endpointKey,
4359
4571
  apiUrlEntry: this.resolveDiscoveryApiEntry(),
4360
4572
  group: catalog?.group ?? null,
4573
+ presentation: 'drawer',
4574
+ title: surface.title,
4575
+ subtitle: surface.description ?? undefined,
4576
+ parentIdentity: runtimeEvent?.resourceIdentity ?? null,
4361
4577
  });
4362
4578
  }
4363
- catch {
4364
- return false;
4579
+ catch (error) {
4580
+ this.error.emit(error);
4581
+ this.snack.open(this.txWithParams('crud.surface.openFailed', 'Não foi possível abrir {title}. Tente novamente ou confirme se esse recurso continua disponível.', { title: surface.title || surface.id }), undefined, { duration: 4500 });
4582
+ return true;
4365
4583
  }
4584
+ const focusOrigin = this.resolveRuntimeFocusOrigin(runtimeEvent);
4366
4585
  let openPromise;
4367
4586
  try {
4368
4587
  const surfaceContext = {
@@ -4381,23 +4600,29 @@ class PraxisCrudComponent {
4381
4600
  action: normalizedAction,
4382
4601
  resourcePath,
4383
4602
  },
4603
+ ...(focusOrigin ? { focusOrigin } : {}),
4384
4604
  },
4385
4605
  };
4386
4606
  const handledInline = await this.surfaceOutlets.tryActivate(payload, surfaceContext);
4387
4607
  openPromise = handledInline ? Promise.resolve(undefined) : Promise.resolve(this.surfaceService.open(payload, surfaceContext));
4388
4608
  }
4389
- catch {
4609
+ catch (error) {
4610
+ if (this.notifySurfaceNavigationFailure(error)) {
4611
+ this.error.emit(error);
4612
+ return true;
4613
+ }
4390
4614
  return false;
4391
4615
  }
4392
- this.afterOpen.emit({
4393
- mode: this.mapSurfacePresentationToCrudMode(payload?.presentation),
4394
- action,
4395
- });
4396
4616
  try {
4397
4617
  this.bindDiscoveredSurfaceLifecycle(await openPromise);
4618
+ this.afterOpen.emit({
4619
+ mode: this.mapSurfacePresentationToCrudMode(payload?.presentation),
4620
+ action,
4621
+ });
4398
4622
  }
4399
4623
  catch (err) {
4400
4624
  this.error.emit(err);
4625
+ this.notifySurfaceNavigationFailure(err);
4401
4626
  }
4402
4627
  return true;
4403
4628
  }
@@ -4437,17 +4662,37 @@ class PraxisCrudComponent {
4437
4662
  }
4438
4663
  let payload;
4439
4664
  try {
4665
+ const resourceId = this.resolveRowResourceId(row);
4440
4666
  payload = this.getActionOpenAdapter().toPayload(discoveredAction, {
4441
4667
  resourcePath,
4442
- resourceId: this.resolveRowResourceId(row),
4668
+ resourceId,
4443
4669
  endpointKey: this.resolvedMetadata?.resource?.endpointKey,
4444
4670
  apiUrlEntry: this.resolveDiscoveryApiEntry(),
4445
4671
  group: catalog?.group ?? null,
4446
4672
  });
4673
+ const resourceIdentity = runtimeEvent?.resourceIdentity ?? null;
4674
+ if (resourceId != null && resourceIdentity) {
4675
+ payload.context = {
4676
+ ...(payload.context || {}),
4677
+ operation: {
4678
+ ...(payload.context?.operation || {}),
4679
+ subject: {
4680
+ resourceKey: discoveredAction.resourceKey,
4681
+ resourceId,
4682
+ identity: resourceIdentity,
4683
+ },
4684
+ },
4685
+ };
4686
+ }
4447
4687
  }
4448
- catch {
4688
+ catch (error) {
4689
+ if (this.notifySurfaceNavigationFailure(error)) {
4690
+ this.error.emit(error);
4691
+ return true;
4692
+ }
4449
4693
  return false;
4450
4694
  }
4695
+ const focusOrigin = this.resolveRuntimeFocusOrigin(runtimeEvent);
4451
4696
  let openPromise;
4452
4697
  try {
4453
4698
  openPromise = Promise.resolve(this.surfaceService.open(payload, {
@@ -4466,19 +4711,35 @@ class PraxisCrudComponent {
4466
4711
  action: normalizedAction,
4467
4712
  resourcePath,
4468
4713
  },
4714
+ ...(focusOrigin ? { focusOrigin } : {}),
4469
4715
  },
4470
4716
  }));
4471
4717
  }
4472
- catch {
4718
+ catch (error) {
4719
+ if (this.notifySurfaceNavigationFailure(error)) {
4720
+ this.error.emit(error);
4721
+ return true;
4722
+ }
4473
4723
  return false;
4474
4724
  }
4475
- this.afterOpen.emit({
4476
- mode: this.mapSurfacePresentationToCrudMode(payload?.presentation),
4477
- action,
4478
- });
4479
- void openPromise
4480
- .then((result) => this.handleDiscoveredSurfaceResult(result))
4481
- .catch((err) => this.error.emit(err));
4725
+ try {
4726
+ this.handleDiscoveredSurfaceResult(await openPromise);
4727
+ this.afterOpen.emit({
4728
+ mode: this.mapSurfacePresentationToCrudMode(payload?.presentation),
4729
+ action,
4730
+ });
4731
+ }
4732
+ catch (err) {
4733
+ this.error.emit(err);
4734
+ this.notifySurfaceNavigationFailure(err);
4735
+ }
4736
+ return true;
4737
+ }
4738
+ notifySurfaceNavigationFailure(error) {
4739
+ if (!isSurfaceNavigationError(error)) {
4740
+ return false;
4741
+ }
4742
+ this.snack.open(translateSurfaceNavigationRejected(this.i18n), undefined, { duration: 4500 });
4482
4743
  return true;
4483
4744
  }
4484
4745
  async resolveDiscoveredActionCatalog(row) {
@@ -4521,13 +4782,13 @@ class PraxisCrudComponent {
4521
4782
  }
4522
4783
  return catalogCandidate;
4523
4784
  }
4524
- resolveProvidedSurface(action, candidate) {
4785
+ resolveProvidedSurface(_action, candidate) {
4525
4786
  if (!candidate || typeof candidate !== 'object') {
4526
4787
  return null;
4527
4788
  }
4528
4789
  const catalogCandidate = candidate;
4529
4790
  const normalizedId = String(catalogCandidate.id || '').trim().toLowerCase();
4530
- if (!normalizedId || normalizedId !== action) {
4791
+ if (!normalizedId) {
4531
4792
  return null;
4532
4793
  }
4533
4794
  const surface = catalogCandidate;
@@ -4543,6 +4804,10 @@ class PraxisCrudComponent {
4543
4804
  const candidates = surfaces
4544
4805
  .filter((surface) => surface.availability?.allowed !== false)
4545
4806
  .sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
4807
+ const exact = candidates.find((surface) => String(surface.id || '').trim().toLowerCase() === action);
4808
+ if (exact) {
4809
+ return exact;
4810
+ }
4546
4811
  const preferredIds = this.getPreferredSurfaceIdsForCrudAction(action);
4547
4812
  const preferred = candidates.find((surface) => preferredIds.includes(String(surface.id || '').trim().toLowerCase()));
4548
4813
  if (preferred) {
@@ -5004,7 +5269,8 @@ class PraxisCrudComponent {
5004
5269
  return [];
5005
5270
  }
5006
5271
  return (capabilities.actions || [])
5007
- .filter((action) => action.scope === 'COLLECTION')
5272
+ .filter((action) => action.scope === 'COLLECTION' &&
5273
+ this.isCatalogActionAvailable(capabilities, action.id))
5008
5274
  .sort((left, right) => (left.order ?? 0) - (right.order ?? 0))
5009
5275
  .map((action) => ({
5010
5276
  action: action.id,
@@ -5025,28 +5291,49 @@ class PraxisCrudComponent {
5025
5291
  appearance: 'outlined',
5026
5292
  position: 'end',
5027
5293
  action: action.id,
5028
- disabled: action.availability?.allowed === false,
5294
+ disabled: !this.isCatalogActionAvailable(capabilities, action.id),
5029
5295
  tooltip: action.description ||
5030
- (action.availability?.allowed === false
5031
- ? translateUnavailableWorkflowMessage(this.i18n, action.availability)
5296
+ (!this.isCatalogActionAvailable(capabilities, action.id)
5297
+ ? translateUnavailableWorkflowMessage(this.i18n, capabilities.operations?.[action.id]?.availability || action.availability)
5032
5298
  : undefined),
5033
5299
  }));
5034
5300
  }
5035
5301
  supportsCreateCapability(snapshot) {
5036
- return (snapshot.operations?.create?.supported === true ||
5037
- this.hasCanonicalOperation(snapshot, 'create') ||
5302
+ const operationAvailability = this.resolveOperationAvailability(snapshot, 'create');
5303
+ if (operationAvailability !== null) {
5304
+ return operationAvailability;
5305
+ }
5306
+ return (this.hasCanonicalOperation(snapshot, 'create') ||
5038
5307
  snapshot.surfaces.some((surface) => surface.scope === 'COLLECTION' &&
5039
5308
  this.isWritableCrudSurface(surface) &&
5040
5309
  surface.availability?.allowed !== false));
5041
5310
  }
5042
5311
  supportsViewCapability(snapshot) {
5043
- return snapshot.operations?.view?.supported === true || this.hasCanonicalOperation(snapshot, 'byId');
5312
+ const operationAvailability = this.resolveOperationAvailability(snapshot, 'view');
5313
+ return operationAvailability ?? this.hasCanonicalOperation(snapshot, 'byId');
5044
5314
  }
5045
5315
  supportsEditCapability(snapshot) {
5046
- return snapshot.operations?.edit?.supported === true || this.hasCanonicalOperation(snapshot, 'update');
5316
+ const operationAvailability = this.resolveOperationAvailability(snapshot, 'edit');
5317
+ return operationAvailability ?? this.hasCanonicalOperation(snapshot, 'update');
5047
5318
  }
5048
5319
  supportsDeleteCapability(snapshot) {
5049
- return snapshot.operations?.delete?.supported === true || this.hasCanonicalOperation(snapshot, 'delete');
5320
+ const operationAvailability = this.resolveOperationAvailability(snapshot, 'delete');
5321
+ return operationAvailability ?? this.hasCanonicalOperation(snapshot, 'delete');
5322
+ }
5323
+ resolveOperationAvailability(snapshot, operationId) {
5324
+ const operation = snapshot.operations?.[operationId];
5325
+ if (!operation) {
5326
+ return null;
5327
+ }
5328
+ return operation.supported === true && operation.availability?.allowed !== false;
5329
+ }
5330
+ isCatalogActionAvailable(snapshot, actionId) {
5331
+ const action = (snapshot.actions || []).find((candidate) => candidate.id === actionId);
5332
+ if (action?.availability?.allowed === false) {
5333
+ return false;
5334
+ }
5335
+ const operationAvailability = this.resolveOperationAvailability(snapshot, actionId);
5336
+ return operationAvailability ?? true;
5050
5337
  }
5051
5338
  hasCanonicalOperation(snapshot, operation) {
5052
5339
  return snapshot.canonicalOperations?.[operation] === true;
@@ -5137,9 +5424,7 @@ class PraxisCrudComponent {
5137
5424
  const title = resourceLabel
5138
5425
  ? this.txWithParams('crud.table.emptyState.initial.title', 'Sem registros em {label}', { label: resourceLabel })
5139
5426
  : this.tx('crud.table.emptyState.initial.titleFallback', 'Nenhum registro disponível.');
5140
- const actionId = String(createAction.action || createAction.id || 'create').trim() || 'create';
5141
5427
  const actionLabel = String(createAction.label || this.getCrudActionLabel('create')).trim();
5142
- const actionIcon = String(createAction.icon || 'add').trim();
5143
5428
  config.behavior = {
5144
5429
  ...behavior,
5145
5430
  emptyState: {
@@ -5147,15 +5432,10 @@ class PraxisCrudComponent {
5147
5432
  contexts: {
5148
5433
  initial: {
5149
5434
  title,
5150
- description: this.tx('crud.table.emptyState.initial.descriptionWithAction', 'Use a ação principal para adicionar o primeiro registro quando houver informações para cadastrar.'),
5151
- actions: [
5152
- {
5153
- label: actionLabel,
5154
- action: actionId,
5155
- icon: actionIcon,
5156
- primary: true,
5157
- },
5158
- ],
5435
+ description: this.txWithParams('crud.table.emptyState.initial.descriptionWithToolbarAction', 'Use "{action}" na barra da tabela para adicionar o primeiro registro quando houver informações para cadastrar.', { action: actionLabel }),
5436
+ // The create action is always materialized in the visible collection toolbar.
5437
+ // Keep the generated empty state instructional so it does not duplicate that CTA.
5438
+ actions: [],
5159
5439
  },
5160
5440
  filtered: {
5161
5441
  title: this.tx('crud.table.emptyState.filtered.title', 'Nenhum resultado encontrado.'),
@@ -5187,42 +5467,51 @@ class PraxisCrudComponent {
5187
5467
  return Object.entries(params).reduce((current, [name, value]) => current.split(`{${name}}`).join(value), text);
5188
5468
  }
5189
5469
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5190
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisCrudComponent, isStandalone: true, selector: "praxis-crud", inputs: { metadata: "metadata", crudId: "crudId", componentInstanceId: "componentInstanceId", context: "context", enableCustomization: "enableCustomization", authoringCapability: "authoringCapability" }, outputs: { configureRequested: "configureRequested", afterOpen: "afterOpen", afterClose: "afterClose", afterSave: "afterSave", afterDelete: "afterDelete", error: "error", rowClick: "rowClick", selectionChange: "selectionChange", tableRuntimeConfigChange: "tableRuntimeConfigChange", crudAuthoringDocumentApplied: "crudAuthoringDocumentApplied", crudAuthoringDocumentSaved: "crudAuthoringDocumentSaved" }, providers: [
5470
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisCrudComponent, isStandalone: true, selector: "praxis-crud", inputs: { metadata: "metadata", crudId: "crudId", componentInstanceId: "componentInstanceId", tableConfigPersistenceStrategy: "tableConfigPersistenceStrategy", context: "context", enableCustomization: "enableCustomization", authoringCapability: "authoringCapability" }, outputs: { configureRequested: "configureRequested", afterOpen: "afterOpen", afterClose: "afterClose", afterSave: "afterSave", afterDelete: "afterDelete", error: "error", rowClick: "rowClick", rowAction: "rowAction", selectionChange: "selectionChange", tableRuntimeConfigChange: "tableRuntimeConfigChange", crudAuthoringDocumentApplied: "crudAuthoringDocumentApplied", crudAuthoringDocumentSaved: "crudAuthoringDocumentSaved" }, providers: [
5191
5471
  providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
5192
5472
  providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
5193
5473
  ], viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
5194
- @if (shouldRenderTable(resolvedMetadata)) {
5195
- <praxis-table
5196
- [config]="tableConfigForBinding"
5197
- [resourcePath]="resolveResourcePath(resolvedMetadata)"
5198
- [data]="resolveTableData(resolvedMetadata)"
5199
- [queryContext]="tableQueryContext"
5200
- [filterCriteria]="tableFilterCriteria"
5201
- [tableId]="crudId || 'default'"
5202
- [crudContext]="tableCrudContext"
5203
- [enableCustomization]="enableCustomization"
5204
- [authoringCapability]="authoringCapability"
5205
- (rowClick)="onTableRowClick($event)"
5206
- (selectionChange)="onTableSelectionChange($event)"
5207
- (rowAction)="onAction($event.action, $event.row, $event)"
5208
- (toolbarAction)="onToolbarAction($event)"
5209
- (bulkAction)="onBulkAction($event)"
5210
- (collectionLinksChange)="onCollectionLinksChange($event)"
5211
- (reset)="onResetPreferences()"
5212
- (metadataChange)="onTableMetadataChange()"
5213
- (loadingStateChange)="onTableLoadingStateChange($event)"
5214
- ></praxis-table>
5215
- } @else {
5216
- @if (isCustomizationAvailable()) {
5217
- <praxis-empty-state-card
5218
- icon="table_rows"
5219
- [title]="getEmptyStateTitle()"
5220
- [description]="getEmptyStateDescription()"
5221
- [primaryAction]="{ label: getEmptyStatePrimaryAction(), icon: 'bolt', action: onConfigureRequested.bind(this) }"
5222
- />
5474
+ <div class="praxis-crud-runtime" [attr.aria-busy]="openingActionLabel() ? 'true' : null">
5475
+ @if (openingActionLabel(); as actionLabel) {
5476
+ <div class="praxis-crud-opening-status" role="status" aria-live="polite" data-testid="praxis-crud-opening-status">
5477
+ <span class="praxis-crud-opening-status__spinner" aria-hidden="true"></span>
5478
+ <span>{{ getOpeningActionStatus(actionLabel) }}</span>
5479
+ </div>
5223
5480
  }
5224
- }
5225
- `, 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", "authoringCapability", "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"] }] });
5481
+ @if (shouldRenderTable(resolvedMetadata)) {
5482
+ <praxis-table
5483
+ [config]="tableConfigForBinding"
5484
+ [resourcePath]="resolveResourcePath(resolvedMetadata)"
5485
+ [data]="resolveTableData(resolvedMetadata)"
5486
+ [queryContext]="tableQueryContext"
5487
+ [filterCriteria]="tableFilterCriteria"
5488
+ [tableId]="crudId || 'default'"
5489
+ [configPersistenceStrategy]="tableConfigPersistenceStrategy"
5490
+ [crudContext]="tableCrudContext"
5491
+ [enableCustomization]="enableCustomization"
5492
+ [authoringCapability]="authoringCapability"
5493
+ (rowClick)="onTableRowClick($event)"
5494
+ (selectionChange)="onTableSelectionChange($event)"
5495
+ (rowAction)="onTableRowAction($event)"
5496
+ (toolbarAction)="onToolbarAction($event)"
5497
+ (bulkAction)="onBulkAction($event)"
5498
+ (collectionLinksChange)="onCollectionLinksChange($event)"
5499
+ (reset)="onResetPreferences()"
5500
+ (metadataChange)="onTableMetadataChange()"
5501
+ (loadingStateChange)="onTableLoadingStateChange($event)"
5502
+ ></praxis-table>
5503
+ } @else {
5504
+ @if (isCustomizationAvailable()) {
5505
+ <praxis-empty-state-card
5506
+ icon="table_rows"
5507
+ [title]="getEmptyStateTitle()"
5508
+ [description]="getEmptyStateDescription()"
5509
+ [primaryAction]="{ label: getEmptyStatePrimaryAction(), icon: 'bolt', action: onConfigureRequested.bind(this) }"
5510
+ />
5511
+ }
5512
+ }
5513
+ </div>
5514
+ `, isInline: true, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}.praxis-crud-runtime{position:relative;min-width:0}.praxis-crud-opening-status{position:absolute;z-index:6;top:.7rem;right:.7rem;display:inline-flex;align-items:center;gap:.55rem;min-height:2.25rem;max-width:min(24rem,calc(100% - 1.4rem));padding:.48rem .72rem;border:1px solid color-mix(in srgb,var(--md-sys-color-primary, #3f51b5) 32%,transparent);border-radius:999px;color:var(--md-sys-color-on-surface, currentColor);background:color-mix(in srgb,var(--md-sys-color-surface-container-high, #fff) 94%,transparent);box-shadow:0 10px 28px #0f172a29;font-size:.82rem;font-weight:650;line-height:1.25;pointer-events:none}.praxis-crud-opening-status__spinner{width:.95rem;height:.95rem;flex:0 0 auto;border:2px solid color-mix(in srgb,var(--md-sys-color-primary, #3f51b5) 26%,transparent);border-top-color:var(--md-sys-color-primary, #3f51b5);border-radius:50%;animation:praxis-crud-opening-spin .72s linear infinite}@keyframes praxis-crud-opening-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.praxis-crud-opening-status__spinner{animation:none;border-top-color:currentColor}}\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", "authoringCapability", "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"] }] });
5226
5515
  }
5227
5516
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudComponent, decorators: [{
5228
5517
  type: Component,
@@ -5230,38 +5519,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5230
5519
  providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
5231
5520
  providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
5232
5521
  ], template: `
5233
- @if (shouldRenderTable(resolvedMetadata)) {
5234
- <praxis-table
5235
- [config]="tableConfigForBinding"
5236
- [resourcePath]="resolveResourcePath(resolvedMetadata)"
5237
- [data]="resolveTableData(resolvedMetadata)"
5238
- [queryContext]="tableQueryContext"
5239
- [filterCriteria]="tableFilterCriteria"
5240
- [tableId]="crudId || 'default'"
5241
- [crudContext]="tableCrudContext"
5242
- [enableCustomization]="enableCustomization"
5243
- [authoringCapability]="authoringCapability"
5244
- (rowClick)="onTableRowClick($event)"
5245
- (selectionChange)="onTableSelectionChange($event)"
5246
- (rowAction)="onAction($event.action, $event.row, $event)"
5247
- (toolbarAction)="onToolbarAction($event)"
5248
- (bulkAction)="onBulkAction($event)"
5249
- (collectionLinksChange)="onCollectionLinksChange($event)"
5250
- (reset)="onResetPreferences()"
5251
- (metadataChange)="onTableMetadataChange()"
5252
- (loadingStateChange)="onTableLoadingStateChange($event)"
5253
- ></praxis-table>
5254
- } @else {
5255
- @if (isCustomizationAvailable()) {
5256
- <praxis-empty-state-card
5257
- icon="table_rows"
5258
- [title]="getEmptyStateTitle()"
5259
- [description]="getEmptyStateDescription()"
5260
- [primaryAction]="{ label: getEmptyStatePrimaryAction(), icon: 'bolt', action: onConfigureRequested.bind(this) }"
5261
- />
5522
+ <div class="praxis-crud-runtime" [attr.aria-busy]="openingActionLabel() ? 'true' : null">
5523
+ @if (openingActionLabel(); as actionLabel) {
5524
+ <div class="praxis-crud-opening-status" role="status" aria-live="polite" data-testid="praxis-crud-opening-status">
5525
+ <span class="praxis-crud-opening-status__spinner" aria-hidden="true"></span>
5526
+ <span>{{ getOpeningActionStatus(actionLabel) }}</span>
5527
+ </div>
5262
5528
  }
5263
- }
5264
- `, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}\n"] }]
5529
+ @if (shouldRenderTable(resolvedMetadata)) {
5530
+ <praxis-table
5531
+ [config]="tableConfigForBinding"
5532
+ [resourcePath]="resolveResourcePath(resolvedMetadata)"
5533
+ [data]="resolveTableData(resolvedMetadata)"
5534
+ [queryContext]="tableQueryContext"
5535
+ [filterCriteria]="tableFilterCriteria"
5536
+ [tableId]="crudId || 'default'"
5537
+ [configPersistenceStrategy]="tableConfigPersistenceStrategy"
5538
+ [crudContext]="tableCrudContext"
5539
+ [enableCustomization]="enableCustomization"
5540
+ [authoringCapability]="authoringCapability"
5541
+ (rowClick)="onTableRowClick($event)"
5542
+ (selectionChange)="onTableSelectionChange($event)"
5543
+ (rowAction)="onTableRowAction($event)"
5544
+ (toolbarAction)="onToolbarAction($event)"
5545
+ (bulkAction)="onBulkAction($event)"
5546
+ (collectionLinksChange)="onCollectionLinksChange($event)"
5547
+ (reset)="onResetPreferences()"
5548
+ (metadataChange)="onTableMetadataChange()"
5549
+ (loadingStateChange)="onTableLoadingStateChange($event)"
5550
+ ></praxis-table>
5551
+ } @else {
5552
+ @if (isCustomizationAvailable()) {
5553
+ <praxis-empty-state-card
5554
+ icon="table_rows"
5555
+ [title]="getEmptyStateTitle()"
5556
+ [description]="getEmptyStateDescription()"
5557
+ [primaryAction]="{ label: getEmptyStatePrimaryAction(), icon: 'bolt', action: onConfigureRequested.bind(this) }"
5558
+ />
5559
+ }
5560
+ }
5561
+ </div>
5562
+ `, styles: [":host{display:block;width:100%;min-width:0;max-width:100%}.praxis-crud-runtime{position:relative;min-width:0}.praxis-crud-opening-status{position:absolute;z-index:6;top:.7rem;right:.7rem;display:inline-flex;align-items:center;gap:.55rem;min-height:2.25rem;max-width:min(24rem,calc(100% - 1.4rem));padding:.48rem .72rem;border:1px solid color-mix(in srgb,var(--md-sys-color-primary, #3f51b5) 32%,transparent);border-radius:999px;color:var(--md-sys-color-on-surface, currentColor);background:color-mix(in srgb,var(--md-sys-color-surface-container-high, #fff) 94%,transparent);box-shadow:0 10px 28px #0f172a29;font-size:.82rem;font-weight:650;line-height:1.25;pointer-events:none}.praxis-crud-opening-status__spinner{width:.95rem;height:.95rem;flex:0 0 auto;border:2px solid color-mix(in srgb,var(--md-sys-color-primary, #3f51b5) 26%,transparent);border-top-color:var(--md-sys-color-primary, #3f51b5);border-radius:50%;animation:praxis-crud-opening-spin .72s linear infinite}@keyframes praxis-crud-opening-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.praxis-crud-opening-status__spinner{animation:none;border-top-color:currentColor}}\n"] }]
5265
5563
  }], ctorParameters: () => [], propDecorators: { metadata: [{
5266
5564
  type: Input,
5267
5565
  args: [{ required: true }]
@@ -5270,6 +5568,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5270
5568
  args: [{ required: true }]
5271
5569
  }], componentInstanceId: [{
5272
5570
  type: Input
5571
+ }], tableConfigPersistenceStrategy: [{
5572
+ type: Input
5273
5573
  }], context: [{
5274
5574
  type: Input
5275
5575
  }], enableCustomization: [{
@@ -5290,6 +5590,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5290
5590
  type: Output
5291
5591
  }], rowClick: [{
5292
5592
  type: Output
5593
+ }], rowAction: [{
5594
+ type: Output
5293
5595
  }], selectionChange: [{
5294
5596
  type: Output
5295
5597
  }], tableRuntimeConfigChange: [{
@@ -5326,10 +5628,10 @@ function debugCrudHost(message, data) {
5326
5628
  }
5327
5629
  class DynamicFormDialogHostComponent {
5328
5630
  dialogRef;
5329
- data;
5330
5631
  dialogService;
5331
5632
  crud;
5332
5633
  configStorage;
5634
+ surfaceRef;
5333
5635
  formComp;
5334
5636
  modal = {};
5335
5637
  presentation = 'modal';
@@ -5339,12 +5641,14 @@ class DynamicFormDialogHostComponent {
5339
5641
  rememberState = false;
5340
5642
  stateKey;
5341
5643
  backDefaults = {};
5644
+ terminalClose = false;
5342
5645
  destroyRef = inject(DestroyRef);
5343
5646
  i18n = inject(PraxisI18nService);
5344
5647
  resourcePath;
5345
5648
  resourceId;
5346
5649
  initialValue;
5347
5650
  resourceIdentity = null;
5651
+ contextIdentity = null;
5348
5652
  schemaUrl;
5349
5653
  submitUrl;
5350
5654
  submitMethod;
@@ -5354,9 +5658,12 @@ class DynamicFormDialogHostComponent {
5354
5658
  formConfig = {};
5355
5659
  formActions;
5356
5660
  formConfigPersistenceStrategy = 'input-first';
5661
+ fieldIconPolicy = 'presentation-only';
5357
5662
  mode = 'create';
5358
5663
  backConfig;
5359
5664
  idField = 'id';
5665
+ isSurfaceFrame;
5666
+ data;
5360
5667
  texts = {
5361
5668
  title: 'Formulário',
5362
5669
  close: 'Fechar',
@@ -5364,19 +5671,32 @@ class DynamicFormDialogHostComponent {
5364
5671
  maximizeLabel: 'Maximizar',
5365
5672
  restoreLabel: 'Restaurar',
5366
5673
  recordContextLabel: translateCrudRuntimeText(this.i18n, 'crud.dialog.recordContextLabel', 'Registro em edição'),
5674
+ viewRecordContextLabel: translateCrudRuntimeText(this.i18n, 'crud.dialog.viewRecordContextLabel', 'Registro em consulta'),
5675
+ relatedContextLabel: translateCrudRuntimeText(this.i18n, 'crud.dialog.relatedContextLabel', 'Vinculado a'),
5367
5676
  discardTitle: 'Descartar alterações?',
5368
5677
  discardMessage: 'Você tem alterações não salvas. Deseja fechar assim mesmo?',
5369
5678
  discardConfirm: 'Descartar',
5370
5679
  discardCancel: 'Cancelar',
5371
5680
  };
5372
- constructor(dialogRef, data, dialogService, crud, configStorage) {
5681
+ constructor(dialogRef, dialogData, dialogService, crud, configStorage, surfaceRef = null, surfaceContentData = null) {
5373
5682
  this.dialogRef = dialogRef;
5374
- this.data = data;
5375
5683
  this.dialogService = dialogService;
5376
5684
  this.crud = crud;
5377
5685
  this.configStorage = configStorage;
5378
- this.dialogRef.disableClose = true;
5379
- this.presentation = this.data.presentation === 'drawer' ? 'drawer' : 'modal';
5686
+ this.surfaceRef = surfaceRef;
5687
+ this.data = dialogData ?? surfaceContentData?.['data'] ?? {};
5688
+ this.isSurfaceFrame = !!this.surfaceRef;
5689
+ if (!this.dialogRef && !this.surfaceRef) {
5690
+ throw new Error('DynamicFormDialogHostComponent requires a MatDialogRef or an active SurfaceDrawerRef.');
5691
+ }
5692
+ if (this.dialogRef) {
5693
+ this.dialogRef.disableClose = true;
5694
+ }
5695
+ this.presentation = this.isSurfaceFrame
5696
+ ? 'surface-frame'
5697
+ : this.data.presentation === 'drawer'
5698
+ ? 'drawer'
5699
+ : 'modal';
5380
5700
  // i18n
5381
5701
  this.texts = {
5382
5702
  ...this.texts,
@@ -5414,9 +5734,12 @@ class DynamicFormDialogHostComponent {
5414
5734
  this.apiUrlEntry = this.data.inputs?.['apiUrlEntry'] ?? null;
5415
5735
  const act = this.data.action?.action;
5416
5736
  this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
5417
- this.resourceIdentity = this.mode === 'edit' && this.data.resourceIdentity
5737
+ this.resourceIdentity = this.mode !== 'create' && this.data.resourceIdentity
5418
5738
  ? this.data.resourceIdentity
5419
5739
  : null;
5740
+ this.contextIdentity = this.data.contextIdentity
5741
+ ? this.data.contextIdentity
5742
+ : null;
5420
5743
  this.formConfig = this.resolveFormConfig();
5421
5744
  this.layoutPolicy = this.resolveLayoutPolicy();
5422
5745
  this.formActions = this.resolveFormActions();
@@ -5433,14 +5756,14 @@ class DynamicFormDialogHostComponent {
5433
5756
  backConfig: this.backConfig,
5434
5757
  });
5435
5758
  // Esc
5436
- if (!this.modal.disableCloseOnEsc) {
5759
+ if (this.dialogRef && !this.modal.disableCloseOnEsc) {
5437
5760
  this.dialogRef
5438
5761
  .keydownEvents()
5439
5762
  .pipe(filter((e) => e.key === 'Escape'), takeUntilDestroyed(this.destroyRef))
5440
5763
  .subscribe(() => this.onCancel());
5441
5764
  }
5442
5765
  // Backdrop
5443
- if (!this.modal.disableCloseOnBackdrop) {
5766
+ if (this.dialogRef && !this.modal.disableCloseOnBackdrop) {
5444
5767
  this.dialogRef
5445
5768
  .backdropClick()
5446
5769
  .pipe(takeUntilDestroyed(this.destroyRef))
@@ -5448,10 +5771,36 @@ class DynamicFormDialogHostComponent {
5448
5771
  }
5449
5772
  // Salvar estado ao fechar, se aplicável
5450
5773
  this.dialogRef
5451
- .afterClosed()
5774
+ ?.afterClosed()
5452
5775
  .pipe(takeUntilDestroyed(this.destroyRef))
5453
5776
  .subscribe(() => this.saveState());
5454
5777
  }
5778
+ get resourceIdentityLabel() {
5779
+ return this.mode === 'view'
5780
+ ? this.texts['viewRecordContextLabel']
5781
+ : this.texts['recordContextLabel'];
5782
+ }
5783
+ get showContextIdentity() {
5784
+ return !!this.contextIdentity && !this.sameIdentity(this.contextIdentity, this.resourceIdentity);
5785
+ }
5786
+ sameIdentity(left, right) {
5787
+ if (!left || !right)
5788
+ return false;
5789
+ const leftKey = left.key;
5790
+ const rightKey = right.key;
5791
+ const leftLabel = this.identityLabel(left);
5792
+ const rightLabel = this.identityLabel(right);
5793
+ if (leftKey && rightKey) {
5794
+ return leftKey.field === rightKey.field
5795
+ && String(leftKey.value) === String(rightKey.value)
5796
+ && !!leftLabel
5797
+ && leftLabel === rightLabel;
5798
+ }
5799
+ return !!leftLabel && leftLabel === rightLabel;
5800
+ }
5801
+ identityLabel(identity) {
5802
+ return String(identity.displayLabel || identity.title?.value || '').trim();
5803
+ }
5455
5804
  extractInitialValue(inputs) {
5456
5805
  if (!inputs || typeof inputs !== 'object') {
5457
5806
  return null;
@@ -5488,6 +5837,22 @@ class DynamicFormDialogHostComponent {
5488
5837
  deriveModeTitle(this.mode, this.resolveResourceTitle()) ||
5489
5838
  this.texts.title;
5490
5839
  }
5840
+ get dialogSubtitle() {
5841
+ const action = this.data.action ?? {};
5842
+ const form = this.formConfig;
5843
+ const resource = this.data.metadata?.resource ?? {};
5844
+ const table = this.data.metadata?.table ?? {};
5845
+ return stringOrUndefined(action.description ??
5846
+ action.tooltip ??
5847
+ form['description'] ??
5848
+ resource.description ??
5849
+ table.subtitle ??
5850
+ table.description);
5851
+ }
5852
+ get dialogIcon() {
5853
+ const action = this.data.action ?? {};
5854
+ return stringOrUndefined(action.icon ?? this.modal.titleIcon ?? this.modal.icon);
5855
+ }
5491
5856
  resolveActionDialogTitle() {
5492
5857
  const actionLabel = stringOrUndefined(this.data.action?.label);
5493
5858
  if (!actionLabel) {
@@ -5518,7 +5883,7 @@ class DynamicFormDialogHostComponent {
5518
5883
  if (this.mode === 'view') {
5519
5884
  return undefined;
5520
5885
  }
5521
- if (this.formConfig?.actions) {
5886
+ if (this.formConfig?.actions?.submit) {
5522
5887
  return undefined;
5523
5888
  }
5524
5889
  const submitLabel = this.resolveSubmitLabel();
@@ -5590,6 +5955,13 @@ class DynamicFormDialogHostComponent {
5590
5955
  });
5591
5956
  }
5592
5957
  }
5958
+ ngAfterViewInit() {
5959
+ if (!this.surfaceRef?.setCanLeave) {
5960
+ return;
5961
+ }
5962
+ this.surfaceRef.updateTitle?.(this.dialogTitle);
5963
+ this.surfaceRef.setCanLeave(() => this.canLeaveSurfaceFrame());
5964
+ }
5593
5965
  onSave(result) {
5594
5966
  const stage = getFormSubmitStage(result);
5595
5967
  if (stage === 'before' || stage === 'error') {
@@ -5599,9 +5971,29 @@ class DynamicFormDialogHostComponent {
5599
5971
  this.saveState();
5600
5972
  return;
5601
5973
  }
5602
- this.dialogRef.close({ type: 'save', data: result });
5974
+ this.terminalClose = true;
5975
+ this.formComp?.form.markAsPristine();
5976
+ if (this.surfaceRef) {
5977
+ this.surfaceRef.emitResult?.({ type: 'save', data: result });
5978
+ this.surfaceRef.close?.({ type: 'save', data: result });
5979
+ }
5980
+ else {
5981
+ this.dialogRef?.close({ type: 'save', data: result });
5982
+ }
5603
5983
  }
5604
5984
  onCancel() {
5985
+ if (this.surfaceRef) {
5986
+ void this.canLeaveSurfaceFrame().then((allowed) => {
5987
+ if (!allowed)
5988
+ return;
5989
+ // `close()` on a nested frame consults the registered guard again.
5990
+ // Mark this user-confirmed transition so both root and nested frames
5991
+ // share one prompt and one close result.
5992
+ this.terminalClose = true;
5993
+ this.surfaceRef?.close?.({ type: 'close' });
5994
+ });
5995
+ return;
5996
+ }
5605
5997
  const dirty = this.formComp?.form.dirty;
5606
5998
  const backCfg = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
5607
5999
  const confirm = backCfg.confirmOnDirty ?? true;
@@ -5619,12 +6011,32 @@ class DynamicFormDialogHostComponent {
5619
6011
  ref
5620
6012
  .afterClosed()
5621
6013
  .pipe(filter((confirmed) => !!confirmed), takeUntilDestroyed(this.destroyRef))
5622
- .subscribe(() => this.dialogRef.close({ type: 'close' }));
6014
+ .subscribe(() => this.dialogRef?.close({ type: 'close' }));
5623
6015
  }
5624
6016
  else {
5625
- this.dialogRef.close({ type: 'close' });
6017
+ this.dialogRef?.close({ type: 'close' });
5626
6018
  }
5627
6019
  }
6020
+ async canLeaveSurfaceFrame() {
6021
+ if (this.terminalClose || !this.formComp?.form.dirty) {
6022
+ return true;
6023
+ }
6024
+ const backCfg = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
6025
+ if (backCfg.confirmOnDirty === false) {
6026
+ return true;
6027
+ }
6028
+ const ref = this.dialogService.open(ConfirmDialogComponent, {
6029
+ data: {
6030
+ title: this.texts.discardTitle,
6031
+ message: this.texts.discardMessage,
6032
+ confirmText: this.texts.discardConfirm,
6033
+ cancelText: this.texts.discardCancel,
6034
+ type: 'warning',
6035
+ },
6036
+ autoFocus: false,
6037
+ });
6038
+ return !!(await firstValueFrom(ref.afterClosed().pipe(take(1))));
6039
+ }
5628
6040
  toggleMaximize(initial = false) {
5629
6041
  const pane = this.resolveOverlayPane();
5630
6042
  if (!initial && !this.maximized && pane) {
@@ -5638,7 +6050,12 @@ class DynamicFormDialogHostComponent {
5638
6050
  const height = this.maximized
5639
6051
  ? `calc(100dvh - ${2 * (gap ?? 0)}px)`
5640
6052
  : this.initialSize.height;
5641
- this.dialogRef.updateSize(width, height);
6053
+ if (this.surfaceRef) {
6054
+ this.surfaceRef.updateSize?.(this.maximized ? 'full' : 'default');
6055
+ }
6056
+ else {
6057
+ this.dialogRef?.updateSize(width, height);
6058
+ }
5642
6059
  this.updateHostPosition();
5643
6060
  if (pane && pane.classList.contains('pfx-dialog-pane')) {
5644
6061
  if (this.presentation === 'drawer') {
@@ -5695,6 +6112,9 @@ class DynamicFormDialogHostComponent {
5695
6112
  startMaximized: this.modal.startMaximized,
5696
6113
  fullscreenBreakpoint: this.modal.fullscreenBreakpoint,
5697
6114
  });
6115
+ if (this.surfaceRef) {
6116
+ return;
6117
+ }
5698
6118
  let shouldMax = false;
5699
6119
  if (saved && typeof saved.maximized === 'boolean') {
5700
6120
  shouldMax = !!saved.maximized;
@@ -5709,16 +6129,19 @@ class DynamicFormDialogHostComponent {
5709
6129
  this.toggleMaximize(true);
5710
6130
  }
5711
6131
  else if (this.initialSize.width || this.initialSize.height) {
5712
- this.dialogRef.updateSize(this.initialSize.width, this.initialSize.height);
6132
+ this.dialogRef?.updateSize(this.initialSize.width, this.initialSize.height);
5713
6133
  this.updateHostPosition();
5714
6134
  }
5715
6135
  }
5716
6136
  updateHostPosition() {
6137
+ if (this.surfaceRef) {
6138
+ return;
6139
+ }
5717
6140
  if (this.presentation !== 'drawer') {
5718
- this.dialogRef.updatePosition();
6141
+ this.dialogRef?.updatePosition();
5719
6142
  return;
5720
6143
  }
5721
- this.dialogRef.updatePosition(this.maximized
6144
+ this.dialogRef?.updatePosition(this.maximized
5722
6145
  ? this.resolveMaximizedDrawerPosition()
5723
6146
  : this.initialPosition);
5724
6147
  }
@@ -5733,15 +6156,28 @@ class DynamicFormDialogHostComponent {
5733
6156
  : { top: offset };
5734
6157
  return { ...horizontal, ...vertical };
5735
6158
  }
5736
- 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 });
5737
- 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: [
6159
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, deps: [{ token: MatDialogRef, optional: true }, { token: MAT_DIALOG_DATA, optional: true }, { token: DialogService }, { token: i2$1.GenericCrudService }, { token: ASYNC_CONFIG_STORAGE }, { token: SURFACE_DRAWER_REF, optional: true }, { token: SURFACE_DRAWER_CONTENT_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component });
6160
+ 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\"", "class.praxis-surface-frame": "isSurfaceFrame" }, classAttribute: "praxis-dialog" }, providers: [
5738
6161
  GenericCrudService,
5739
6162
  providePraxisI18nConfig(PRAXIS_CRUD_RUNTIME_I18N_CONFIG),
5740
6163
  ], viewQueries: [{ propertyName: "formComp", first: true, predicate: PraxisDynamicForm, descendants: true }], ngImport: i0, template: `
6164
+ @if (!isSurfaceFrame) {
5741
6165
  <div mat-dialog-title class="dialog-header">
5742
- <h2 id="crudDialogTitle" class="dialog-title">
5743
- {{ dialogTitle }}
5744
- </h2>
6166
+ @if (dialogIcon; as icon) {
6167
+ <span class="dialog-title-icon" aria-hidden="true">
6168
+ <mat-icon [praxisIcon]="icon"></mat-icon>
6169
+ </span>
6170
+ }
6171
+ <span class="dialog-heading">
6172
+ <h2 id="crudDialogTitle" class="dialog-title">
6173
+ {{ dialogTitle }}
6174
+ </h2>
6175
+ @if (dialogSubtitle; as subtitle) {
6176
+ <span id="crudDialogDescription" class="dialog-subtitle">
6177
+ {{ subtitle }}
6178
+ </span>
6179
+ }
6180
+ </span>
5745
6181
  <span class="spacer"></span>
5746
6182
  @if (modal.canMaximize) {
5747
6183
  <button
@@ -5761,22 +6197,43 @@ class DynamicFormDialogHostComponent {
5761
6197
  cdkFocusInitial
5762
6198
  ></button>
5763
6199
  </div>
6200
+ }
5764
6201
 
5765
6202
  <mat-dialog-content
5766
6203
  class="dialog-content"
5767
- aria-labelledby="crudDialogTitle"
6204
+ [attr.aria-labelledby]="isSurfaceFrame ? null : 'crudDialogTitle'"
6205
+ [attr.aria-describedby]="
6206
+ !isSurfaceFrame && dialogSubtitle ? 'crudDialogDescription' : null
6207
+ "
5768
6208
  >
5769
- @if (resourceIdentity) {
5770
- <div class="crud-resource-identity" data-testid="crud-resource-identity">
5771
- <span class="crud-resource-identity__label" aria-hidden="true">
5772
- {{ texts.recordContextLabel }}
5773
- </span>
5774
- <praxis-resource-identity
5775
- [identity]="resourceIdentity"
5776
- density="compact"
5777
- [showKeyLabel]="true"
5778
- [ariaLabel]="texts.recordContextLabel"
5779
- ></praxis-resource-identity>
6209
+ @if (showContextIdentity || resourceIdentity) {
6210
+ <div class="crud-operation-context" data-testid="crud-operation-context">
6211
+ @if (showContextIdentity) {
6212
+ <div class="crud-resource-identity" data-testid="crud-context-identity">
6213
+ <span class="crud-resource-identity__label" aria-hidden="true">
6214
+ {{ texts.relatedContextLabel }}
6215
+ </span>
6216
+ <praxis-resource-identity
6217
+ [identity]="contextIdentity"
6218
+ density="compact"
6219
+ [showKeyLabel]="true"
6220
+ [ariaLabel]="texts.relatedContextLabel"
6221
+ ></praxis-resource-identity>
6222
+ </div>
6223
+ }
6224
+ @if (resourceIdentity) {
6225
+ <div class="crud-resource-identity" data-testid="crud-resource-identity">
6226
+ <span class="crud-resource-identity__label" aria-hidden="true">
6227
+ {{ resourceIdentityLabel }}
6228
+ </span>
6229
+ <praxis-resource-identity
6230
+ [identity]="resourceIdentity"
6231
+ density="compact"
6232
+ [showKeyLabel]="true"
6233
+ [ariaLabel]="resourceIdentityLabel"
6234
+ ></praxis-resource-identity>
6235
+ </div>
6236
+ }
5780
6237
  </div>
5781
6238
  }
5782
6239
  <praxis-dynamic-form
@@ -5794,19 +6251,22 @@ class DynamicFormDialogHostComponent {
5794
6251
  [configPersistenceStrategy]="formConfigPersistenceStrategy"
5795
6252
  [layoutPolicy]="layoutPolicy"
5796
6253
  [presentationModeGlobal]="mode === 'view' ? true : null"
6254
+ [fieldIconPolicy]="fieldIconPolicy"
5797
6255
  [backConfig]="backConfig"
5798
6256
  [actions]="formActions"
5799
6257
  (formSubmit)="onSave($event)"
5800
6258
  (formCancel)="onCancel()"
5801
6259
  ></praxis-dynamic-form>
5802
6260
  </mat-dialog-content>
5803
- `, 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 .crud-resource-identity{position:sticky;top:0;z-index:1;display:grid;gap:.25rem;margin-block-end:var(--dlg-pad);padding-block:.5rem .75rem;background:var(--pfx-form-surface, var(--md-sys-color-surface));border-bottom:1px solid var(--md-sys-color-outline-variant)}praxis-dynamic-form-dialog-host .crud-resource-identity__label{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-label-medium, 500 .75rem/1rem inherit)}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.praxis-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.praxis-icon-button:hover{color:var(--md-sys-color-primary)}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.pfx-drawer-maximized{margin:var(--pfx-drawer-edge-gap, 8px)!important}.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: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "component", type: PraxisResourceIdentityComponent, selector: "praxis-resource-identity", inputs: ["identity", "emptyTitle", "density", "showMetadataLabels", "showKeyLabel", "ariaLabel"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "initialValue", "editorialContext", "mode", "config", "actions", "schemaSource", "schemaUrl", "readUrl", "submitUrl", "submitMethod", "submitIdempotencyKey", "submitCorrelationId", "submitResourceVersion", "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 });
6261
+ `, 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);min-height:var(--dlg-header-h);padding:10px var(--dlg-pad);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 .dialog-heading{display:grid;min-width:0;gap:2px}praxis-dynamic-form-dialog-host .dialog-title-icon{display:inline-grid;flex:0 0 auto;width:36px;height:36px;place-items:center;border-radius:10px;color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary) 12%,transparent)}praxis-dynamic-form-dialog-host .dialog-title-icon mat-icon{width:22px;height:22px;font-size:22px}praxis-dynamic-form-dialog-host .dialog-subtitle{display:-webkit-box;overflow:hidden;color:var(--md-sys-color-on-surface-variant);font-size:.82rem;font-weight:400;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2}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 .crud-operation-context{position:sticky;top:0;z-index:1;display:grid;gap:.75rem;margin-block-end:var(--dlg-pad);padding-block:.5rem .75rem;background:var(--pfx-form-surface, var(--md-sys-color-surface));border-bottom:1px solid var(--md-sys-color-outline-variant)}praxis-dynamic-form-dialog-host .crud-resource-identity{display:grid;gap:.25rem;min-width:0}praxis-dynamic-form-dialog-host .crud-resource-identity__label{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-label-medium, 500 .75rem/1rem inherit)}praxis-dynamic-form-dialog-host.praxis-drawer{--dlg-header-h: 72px;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{display:flex;flex-direction:column;max-height:none;min-height:0;padding:clamp(12px,2.4vw,24px)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content>praxis-dynamic-form{display:block;flex:1 1 auto;min-height:0}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content>praxis-dynamic-form>.praxis-dynamic-form{min-height:100%}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content>praxis-dynamic-form>.praxis-dynamic-form>praxis-form-actions[data-actions-placement=afterSections]{margin-top:auto;padding-top:var(--pfx-actions-gap-top, var(--pfx-section-gap, 20px))}praxis-dynamic-form-dialog-host.praxis-surface-frame .dialog-content{max-height:none;min-height:0;padding:clamp(12px,2.4vw,24px)}praxis-dynamic-form-dialog-host .dialog-header button.praxis-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.praxis-icon-button:hover{color:var(--md-sys-color-primary)}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%;min-width:0;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.pfx-drawer-maximized{margin:var(--pfx-drawer-edge-gap, 8px)!important}.cdk-overlay-pane.pfx-drawer-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{min-width:0!important;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: MatIconModule }, { kind: "component", type: i6.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: PraxisIconButtonComponent, selector: "button[praxisIconButton]", inputs: ["praxisIconButton", "size", "appearance", "presentation", "pressed", "busy"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisResourceIdentityComponent, selector: "praxis-resource-identity", inputs: ["identity", "emptyTitle", "density", "showMetadataLabels", "showKeyLabel", "ariaLabel"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "initialValue", "editorialContext", "mode", "config", "actions", "schemaSource", "schemaUrl", "readUrl", "submitUrl", "submitMethod", "submitIdempotencyKey", "submitCorrelationId", "submitResourceVersion", "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 });
5804
6262
  }
5805
6263
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
5806
6264
  type: Component,
5807
6265
  args: [{ selector: 'praxis-dynamic-form-dialog-host', standalone: true, imports: [
5808
6266
  MatDialogModule,
6267
+ MatIconModule,
5809
6268
  PraxisIconButtonComponent,
6269
+ PraxisIconDirective,
5810
6270
  PraxisResourceIdentityComponent,
5811
6271
  PraxisDynamicForm
5812
6272
  ], encapsulation: ViewEncapsulation.None, providers: [
@@ -5817,11 +6277,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5817
6277
  '[attr.data-density]': 'modal.density || "default"',
5818
6278
  '[attr.data-presentation]': 'presentation',
5819
6279
  '[class.praxis-drawer]': 'presentation === "drawer"',
6280
+ '[class.praxis-surface-frame]': 'isSurfaceFrame',
5820
6281
  }, template: `
6282
+ @if (!isSurfaceFrame) {
5821
6283
  <div mat-dialog-title class="dialog-header">
5822
- <h2 id="crudDialogTitle" class="dialog-title">
5823
- {{ dialogTitle }}
5824
- </h2>
6284
+ @if (dialogIcon; as icon) {
6285
+ <span class="dialog-title-icon" aria-hidden="true">
6286
+ <mat-icon [praxisIcon]="icon"></mat-icon>
6287
+ </span>
6288
+ }
6289
+ <span class="dialog-heading">
6290
+ <h2 id="crudDialogTitle" class="dialog-title">
6291
+ {{ dialogTitle }}
6292
+ </h2>
6293
+ @if (dialogSubtitle; as subtitle) {
6294
+ <span id="crudDialogDescription" class="dialog-subtitle">
6295
+ {{ subtitle }}
6296
+ </span>
6297
+ }
6298
+ </span>
5825
6299
  <span class="spacer"></span>
5826
6300
  @if (modal.canMaximize) {
5827
6301
  <button
@@ -5841,22 +6315,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5841
6315
  cdkFocusInitial
5842
6316
  ></button>
5843
6317
  </div>
6318
+ }
5844
6319
 
5845
6320
  <mat-dialog-content
5846
6321
  class="dialog-content"
5847
- aria-labelledby="crudDialogTitle"
6322
+ [attr.aria-labelledby]="isSurfaceFrame ? null : 'crudDialogTitle'"
6323
+ [attr.aria-describedby]="
6324
+ !isSurfaceFrame && dialogSubtitle ? 'crudDialogDescription' : null
6325
+ "
5848
6326
  >
5849
- @if (resourceIdentity) {
5850
- <div class="crud-resource-identity" data-testid="crud-resource-identity">
5851
- <span class="crud-resource-identity__label" aria-hidden="true">
5852
- {{ texts.recordContextLabel }}
5853
- </span>
5854
- <praxis-resource-identity
5855
- [identity]="resourceIdentity"
5856
- density="compact"
5857
- [showKeyLabel]="true"
5858
- [ariaLabel]="texts.recordContextLabel"
5859
- ></praxis-resource-identity>
6327
+ @if (showContextIdentity || resourceIdentity) {
6328
+ <div class="crud-operation-context" data-testid="crud-operation-context">
6329
+ @if (showContextIdentity) {
6330
+ <div class="crud-resource-identity" data-testid="crud-context-identity">
6331
+ <span class="crud-resource-identity__label" aria-hidden="true">
6332
+ {{ texts.relatedContextLabel }}
6333
+ </span>
6334
+ <praxis-resource-identity
6335
+ [identity]="contextIdentity"
6336
+ density="compact"
6337
+ [showKeyLabel]="true"
6338
+ [ariaLabel]="texts.relatedContextLabel"
6339
+ ></praxis-resource-identity>
6340
+ </div>
6341
+ }
6342
+ @if (resourceIdentity) {
6343
+ <div class="crud-resource-identity" data-testid="crud-resource-identity">
6344
+ <span class="crud-resource-identity__label" aria-hidden="true">
6345
+ {{ resourceIdentityLabel }}
6346
+ </span>
6347
+ <praxis-resource-identity
6348
+ [identity]="resourceIdentity"
6349
+ density="compact"
6350
+ [showKeyLabel]="true"
6351
+ [ariaLabel]="resourceIdentityLabel"
6352
+ ></praxis-resource-identity>
6353
+ </div>
6354
+ }
5860
6355
  </div>
5861
6356
  }
5862
6357
  <praxis-dynamic-form
@@ -5874,22 +6369,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
5874
6369
  [configPersistenceStrategy]="formConfigPersistenceStrategy"
5875
6370
  [layoutPolicy]="layoutPolicy"
5876
6371
  [presentationModeGlobal]="mode === 'view' ? true : null"
6372
+ [fieldIconPolicy]="fieldIconPolicy"
5877
6373
  [backConfig]="backConfig"
5878
6374
  [actions]="formActions"
5879
6375
  (formSubmit)="onSave($event)"
5880
6376
  (formCancel)="onCancel()"
5881
6377
  ></praxis-dynamic-form>
5882
6378
  </mat-dialog-content>
5883
- `, 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 .crud-resource-identity{position:sticky;top:0;z-index:1;display:grid;gap:.25rem;margin-block-end:var(--dlg-pad);padding-block:.5rem .75rem;background:var(--pfx-form-surface, var(--md-sys-color-surface));border-bottom:1px solid var(--md-sys-color-outline-variant)}praxis-dynamic-form-dialog-host .crud-resource-identity__label{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-label-medium, 500 .75rem/1rem inherit)}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.praxis-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.praxis-icon-button:hover{color:var(--md-sys-color-primary)}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.pfx-drawer-maximized{margin:var(--pfx-drawer-edge-gap, 8px)!important}.cdk-overlay-pane.pfx-drawer-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{border-radius:0!important}\n"] }]
6379
+ `, 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);min-height:var(--dlg-header-h);padding:10px var(--dlg-pad);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 .dialog-heading{display:grid;min-width:0;gap:2px}praxis-dynamic-form-dialog-host .dialog-title-icon{display:inline-grid;flex:0 0 auto;width:36px;height:36px;place-items:center;border-radius:10px;color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary) 12%,transparent)}praxis-dynamic-form-dialog-host .dialog-title-icon mat-icon{width:22px;height:22px;font-size:22px}praxis-dynamic-form-dialog-host .dialog-subtitle{display:-webkit-box;overflow:hidden;color:var(--md-sys-color-on-surface-variant);font-size:.82rem;font-weight:400;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2}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 .crud-operation-context{position:sticky;top:0;z-index:1;display:grid;gap:.75rem;margin-block-end:var(--dlg-pad);padding-block:.5rem .75rem;background:var(--pfx-form-surface, var(--md-sys-color-surface));border-bottom:1px solid var(--md-sys-color-outline-variant)}praxis-dynamic-form-dialog-host .crud-resource-identity{display:grid;gap:.25rem;min-width:0}praxis-dynamic-form-dialog-host .crud-resource-identity__label{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-label-medium, 500 .75rem/1rem inherit)}praxis-dynamic-form-dialog-host.praxis-drawer{--dlg-header-h: 72px;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{display:flex;flex-direction:column;max-height:none;min-height:0;padding:clamp(12px,2.4vw,24px)}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content>praxis-dynamic-form{display:block;flex:1 1 auto;min-height:0}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content>praxis-dynamic-form>.praxis-dynamic-form{min-height:100%}praxis-dynamic-form-dialog-host.praxis-drawer .dialog-content>praxis-dynamic-form>.praxis-dynamic-form>praxis-form-actions[data-actions-placement=afterSections]{margin-top:auto;padding-top:var(--pfx-actions-gap-top, var(--pfx-section-gap, 20px))}praxis-dynamic-form-dialog-host.praxis-surface-frame .dialog-content{max-height:none;min-height:0;padding:clamp(12px,2.4vw,24px)}praxis-dynamic-form-dialog-host .dialog-header button.praxis-icon-button{color:var(--md-sys-color-on-surface-variant)}praxis-dynamic-form-dialog-host .dialog-header button.praxis-icon-button:hover{color:var(--md-sys-color-primary)}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%;min-width:0;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.pfx-drawer-maximized{margin:var(--pfx-drawer-edge-gap, 8px)!important}.cdk-overlay-pane.pfx-drawer-pane .mat-mdc-dialog-surface,.cdk-overlay-pane.pfx-drawer-pane .mdc-dialog__surface{min-width:0!important;border-radius:0!important}\n"] }]
5884
6380
  }], ctorParameters: () => [{ type: undefined, decorators: [{
6381
+ type: Optional
6382
+ }, {
5885
6383
  type: Inject,
5886
6384
  args: [MatDialogRef]
5887
6385
  }] }, { type: undefined, decorators: [{
6386
+ type: Optional
6387
+ }, {
5888
6388
  type: Inject,
5889
6389
  args: [MAT_DIALOG_DATA]
5890
6390
  }] }, { type: DialogService }, { type: i2$1.GenericCrudService }, { type: undefined, decorators: [{
5891
6391
  type: Inject,
5892
6392
  args: [ASYNC_CONFIG_STORAGE]
6393
+ }] }, { type: undefined, decorators: [{
6394
+ type: Optional
6395
+ }, {
6396
+ type: Inject,
6397
+ args: [SURFACE_DRAWER_REF]
6398
+ }] }, { type: undefined, decorators: [{
6399
+ type: Optional
6400
+ }, {
6401
+ type: Inject,
6402
+ args: [SURFACE_DRAWER_CONTENT_DATA]
5893
6403
  }] }], propDecorators: { formComp: [{
5894
6404
  type: ViewChild,
5895
6405
  args: [PraxisDynamicForm]
@@ -6119,6 +6629,12 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
6119
6629
  type: 'string',
6120
6630
  description: 'Identificador opcional para múltiplas instâncias na mesma rota.',
6121
6631
  },
6632
+ {
6633
+ name: 'tableConfigPersistenceStrategy',
6634
+ type: "'local-first' | 'input-first' | 'volatile'",
6635
+ default: 'local-first',
6636
+ description: 'Política da tabela interna. Use input-first para contratos governados autoritativos e volatile para previews que não devem ler nem gravar preferências locais.',
6637
+ },
6122
6638
  {
6123
6639
  name: 'context',
6124
6640
  type: 'Record<string, unknown>',
@@ -6198,6 +6714,11 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
6198
6714
  type: '{ row: unknown; index: number }',
6199
6715
  description: 'Encaminha o clique de linha da tabela interna para composição master-detail e seleção local.',
6200
6716
  },
6717
+ {
6718
+ name: 'rowAction',
6719
+ type: 'CrudActionRuntimeEvent',
6720
+ description: 'Encaminha a ação contextual da linha com row, action e resourceIdentity para composição governada.',
6721
+ },
6201
6722
  {
6202
6723
  name: 'selectionChange',
6203
6724
  type: 'unknown',
@@ -6694,6 +7215,7 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
6694
7215
  { name: 'metadata', type: 'CrudMetadata | string', description: 'Canonical CRUD metadata or serialized metadata document.' },
6695
7216
  { name: 'crudId', type: 'string', description: 'Stable CRUD instance id used for table/form identity and persistence.' },
6696
7217
  { name: 'componentInstanceId', type: 'string', description: 'Optional stable host instance id for multiple CRUD widgets on the same route.' },
7218
+ { name: 'tableConfigPersistenceStrategy', type: "'local-first' | 'input-first' | 'volatile'", description: 'Persistence precedence forwarded to the internal Praxis Table. Governed previews should use input-first or volatile explicitly.' },
6697
7219
  { name: 'context', type: 'Record<string, unknown>', description: 'Opaque host context used for authoring seeds and launcher inputs.' },
6698
7220
  { name: 'enableCustomization', type: 'boolean', description: 'Explicit host opt-in for CRUD authoring surfaces.' },
6699
7221
  { name: 'authoringCapability', type: 'string | null', description: 'Public runtime capability required before CRUD and delegated table authoring can open.' },