@praxisui/crud 9.0.4-rc.35 → 9.0.4-rc.37

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,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-08-05T17:32:01.038Z",
3
+ "generatedAt": "2026-08-09T11:18:35.694Z",
4
4
  "packageName": "@praxisui/crud",
5
- "packageVersion": "9.0.4-rc.35",
5
+ "packageVersion": "9.0.4-rc.37",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 2,
@@ -97,6 +97,35 @@ 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);
@@ -115,6 +144,15 @@ class CrudLauncherService {
115
144
  }
116
145
  })();
117
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();
118
156
  const prefetchedDialogHost = this.isLikelyOverlayAction(action, metadata)
119
157
  ? this.loadDialogHost()
120
158
  : undefined;
@@ -156,6 +194,8 @@ class CrudLauncherService {
156
194
  resourceIdentity,
157
195
  contextIdentity,
158
196
  };
197
+ const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
198
+ const restoreFocus = resolveCrudRestoreFocus(modalCfg.restoreFocus, actionFocusOrigin);
159
199
  if (mode === 'drawer' && runtime?.surfaceRuntime?.push) {
160
200
  const frameId = this.buildSurfaceFrameId(actionForLaunch);
161
201
  const frameRef = await runtime.surfaceRuntime.push({
@@ -163,6 +203,9 @@ class CrudLauncherService {
163
203
  title: this.resolveSurfaceFrameTitle(actionForLaunch, merged.metadata),
164
204
  titleIcon: stringOrUndefined$1(actionForLaunch['icon']),
165
205
  subtitle: this.resolveSurfaceFrameSubtitle(actionForLaunch, merged.metadata),
206
+ ...(actionFocusOrigin
207
+ ? { returnFocusTo: actionFocusOrigin }
208
+ : {}),
166
209
  content: {
167
210
  component: await (prefetchedDialogHost ?? this.loadDialogHost()),
168
211
  inputs: { data: dialogData },
@@ -197,12 +240,12 @@ class CrudLauncherService {
197
240
  inputs,
198
241
  resourceIdentity,
199
242
  contextIdentity,
243
+ restoreFocus,
200
244
  onClose: drawerCallbacks?.onClose,
201
245
  onResult: drawerCallbacks?.onResult,
202
246
  }));
203
247
  return { mode };
204
248
  }
205
- const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
206
249
  debugCrudLauncher('[CRUD:Launcher] opening dialog with:', {
207
250
  action: merged.action.action,
208
251
  formId: actionForLaunch.formId,
@@ -233,7 +276,7 @@ class CrudLauncherService {
233
276
  panelClass: panelClasses,
234
277
  backdropClass: mergedBackdropClasses,
235
278
  autoFocus: modalCfg.autoFocus ?? true,
236
- restoreFocus: modalCfg.restoreFocus ?? true,
279
+ restoreFocus,
237
280
  minWidth: drawerMode
238
281
  ? (modalCfg.minWidth ?? DEFAULT_CRUD_DRAWER_MODAL_CONFIG.minWidth)
239
282
  : '360px',
@@ -986,7 +1029,7 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
986
1029
  'crud.emptyState.primaryAction': 'Configurar metadados',
987
1030
  'crud.table.emptyState.initial.title': 'Sem registros em {label}',
988
1031
  'crud.table.emptyState.initial.titleFallback': 'Nenhum registro disponível.',
989
- 'crud.table.emptyState.initial.descriptionWithAction': 'Use a ação principal para adicionar o primeiro registro quando houver informações para cadastrar.',
1032
+ 'crud.table.emptyState.initial.descriptionWithToolbarAction': 'Use "{action}" na barra da tabela para adicionar o primeiro registro quando houver informações para cadastrar.',
990
1033
  'crud.table.emptyState.filtered.title': 'Nenhum resultado encontrado.',
991
1034
  'crud.table.emptyState.filtered.description': 'Revise os filtros ou ajuste o termo de busca.',
992
1035
  'crud.preferences.resetSuccess': 'Overrides de CRUD redefinidos',
@@ -1009,7 +1052,7 @@ const PRAXIS_CRUD_RUNTIME_I18N_CONFIG = {
1009
1052
  'crud.emptyState.primaryAction': 'Configure metadata',
1010
1053
  'crud.table.emptyState.initial.title': 'No records in {label}',
1011
1054
  'crud.table.emptyState.initial.titleFallback': 'No records available.',
1012
- 'crud.table.emptyState.initial.descriptionWithAction': 'Use the primary action to add the first record when there is information to register.',
1055
+ 'crud.table.emptyState.initial.descriptionWithToolbarAction': 'Use "{action}" in the table toolbar to add the first record when there is information to register.',
1013
1056
  'crud.table.emptyState.filtered.title': 'No results found.',
1014
1057
  'crud.table.emptyState.filtered.description': 'Review the filters or adjust the search term.',
1015
1058
  'crud.preferences.resetSuccess': 'CRUD overrides reset',
@@ -4006,6 +4049,7 @@ class PraxisCrudComponent {
4006
4049
  links: this.resolveCrudRuntimeLinks(effectiveAction.action, contextualRow),
4007
4050
  resourceIdentity,
4008
4051
  contextIdentity,
4052
+ focusOrigin: runtimeEvent?.focusOrigin ?? null,
4009
4053
  surfaceRuntime: this.resolveSurfaceRuntime(),
4010
4054
  });
4011
4055
  this.afterOpen.emit({ mode, action: effectiveAction.action });
@@ -4467,6 +4511,17 @@ class PraxisCrudComponent {
4467
4511
  const first = Array.isArray(event?.selectedRows) ? event.selectedRows[0] : null;
4468
4512
  return this.isRecord(first) ? first : null;
4469
4513
  }
4514
+ /**
4515
+ * Keeps the DOM trigger exclusively in transient runtime context. Discovery
4516
+ * payloads remain JSON-safe and can still be authored, inspected and
4517
+ * persisted independently from the browser that opened the surface.
4518
+ */
4519
+ resolveRuntimeFocusOrigin(event) {
4520
+ const focusOrigin = event?.focusOrigin;
4521
+ return focusOrigin && typeof focusOrigin.focus === 'function'
4522
+ ? focusOrigin
4523
+ : undefined;
4524
+ }
4470
4525
  extractDiscoveryActionConfig(event) {
4471
4526
  const config = event?.actionConfig;
4472
4527
  return this.isRecord(config) && typeof config['resourceKey'] === 'string'
@@ -4516,6 +4571,7 @@ class PraxisCrudComponent {
4516
4571
  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 });
4517
4572
  return true;
4518
4573
  }
4574
+ const focusOrigin = this.resolveRuntimeFocusOrigin(runtimeEvent);
4519
4575
  let openPromise;
4520
4576
  try {
4521
4577
  const surfaceContext = {
@@ -4534,6 +4590,7 @@ class PraxisCrudComponent {
4534
4590
  action: normalizedAction,
4535
4591
  resourcePath,
4536
4592
  },
4593
+ ...(focusOrigin ? { focusOrigin } : {}),
4537
4594
  },
4538
4595
  };
4539
4596
  const handledInline = await this.surfaceOutlets.tryActivate(payload, surfaceContext);
@@ -4610,6 +4667,7 @@ class PraxisCrudComponent {
4610
4667
  }
4611
4668
  return false;
4612
4669
  }
4670
+ const focusOrigin = this.resolveRuntimeFocusOrigin(runtimeEvent);
4613
4671
  let openPromise;
4614
4672
  try {
4615
4673
  openPromise = Promise.resolve(this.surfaceService.open(payload, {
@@ -4628,6 +4686,7 @@ class PraxisCrudComponent {
4628
4686
  action: normalizedAction,
4629
4687
  resourcePath,
4630
4688
  },
4689
+ ...(focusOrigin ? { focusOrigin } : {}),
4631
4690
  },
4632
4691
  }));
4633
4692
  }
@@ -5340,9 +5399,7 @@ class PraxisCrudComponent {
5340
5399
  const title = resourceLabel
5341
5400
  ? this.txWithParams('crud.table.emptyState.initial.title', 'Sem registros em {label}', { label: resourceLabel })
5342
5401
  : this.tx('crud.table.emptyState.initial.titleFallback', 'Nenhum registro disponível.');
5343
- const actionId = String(createAction.action || createAction.id || 'create').trim() || 'create';
5344
5402
  const actionLabel = String(createAction.label || this.getCrudActionLabel('create')).trim();
5345
- const actionIcon = String(createAction.icon || 'add').trim();
5346
5403
  config.behavior = {
5347
5404
  ...behavior,
5348
5405
  emptyState: {
@@ -5350,15 +5407,10 @@ class PraxisCrudComponent {
5350
5407
  contexts: {
5351
5408
  initial: {
5352
5409
  title,
5353
- description: this.tx('crud.table.emptyState.initial.descriptionWithAction', 'Use a ação principal para adicionar o primeiro registro quando houver informações para cadastrar.'),
5354
- actions: [
5355
- {
5356
- label: actionLabel,
5357
- action: actionId,
5358
- icon: actionIcon,
5359
- primary: true,
5360
- },
5361
- ],
5410
+ 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 }),
5411
+ // The create action is always materialized in the visible collection toolbar.
5412
+ // Keep the generated empty state instructional so it does not duplicate that CTA.
5413
+ actions: [],
5362
5414
  },
5363
5415
  filtered: {
5364
5416
  title: this.tx('crud.table.emptyState.filtered.title', 'Nenhum resultado encontrado.'),
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@praxisui/crud",
3
- "version": "9.0.4-rc.35",
3
+ "version": "9.0.4-rc.37",
4
4
  "description": "CRUD building blocks for Praxis UI: integrates dynamic forms and tables with unified configuration and services.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
7
7
  "@angular/core": "^21.0.0",
8
- "@praxisui/dynamic-form": "^9.0.4-rc.35",
9
- "@praxisui/table": "^9.0.4-rc.35",
10
- "@praxisui/core": "^9.0.4-rc.35",
11
- "@praxisui/dynamic-fields": "^9.0.4-rc.35",
12
- "@praxisui/settings-panel": "^9.0.4-rc.35",
8
+ "@praxisui/dynamic-form": "^9.0.4-rc.37",
9
+ "@praxisui/table": "^9.0.4-rc.37",
10
+ "@praxisui/core": "^9.0.4-rc.37",
11
+ "@praxisui/dynamic-fields": "^9.0.4-rc.37",
12
+ "@praxisui/settings-panel": "^9.0.4-rc.37",
13
13
  "@angular/cdk": "^21.0.0",
14
14
  "@angular/forms": "^21.0.0",
15
15
  "@angular/material": "^21.0.0",
16
16
  "@angular/router": "^21.0.0",
17
- "@praxisui/ai": "^9.0.4-rc.35",
17
+ "@praxisui/ai": "^9.0.4-rc.37",
18
18
  "rxjs": "~7.8.0"
19
19
  },
20
20
  "dependencies": {
@@ -149,6 +149,8 @@ type CrudActionRuntimeEvent = {
149
149
  selectedRows?: Record<string, unknown>[];
150
150
  rows?: Record<string, unknown>[];
151
151
  resourceIdentity?: MaterializedResourceIdentity | null;
152
+ /** Ephemeral focus origin provided by a runtime surface, never persisted. */
153
+ focusOrigin?: HTMLElement;
152
154
  };
153
155
  type CrudContextAction = {
154
156
  action: string;
@@ -285,6 +287,12 @@ declare class PraxisCrudComponent implements OnChanges {
285
287
  private extractSelectedRowFromEvent;
286
288
  private extractSelectedRowFromBulkAction;
287
289
  private extractSelectedRowFromToolbarAction;
290
+ /**
291
+ * Keeps the DOM trigger exclusively in transient runtime context. Discovery
292
+ * payloads remain JSON-safe and can still be authored, inspected and
293
+ * persisted independently from the browser that opened the surface.
294
+ */
295
+ private resolveRuntimeFocusOrigin;
288
296
  private extractDiscoveryActionConfig;
289
297
  private resolveSelectedRowForAction;
290
298
  private tryOpenDiscoveredCrudSurface;
@@ -356,6 +364,8 @@ interface CrudLaunchRuntime {
356
364
  contextIdentity?: MaterializedResourceIdentity | null;
357
365
  /** Active runtime drawer session. It is never persisted in CRUD metadata. */
358
366
  surfaceRuntime?: SurfaceDrawerRef | null;
367
+ /** Stable runtime trigger for restoring focus after a form overlay closes. */
368
+ focusOrigin?: HTMLElement | null;
359
369
  }
360
370
  declare class CrudLauncherService {
361
371
  private readonly router;