@praxisui/crud 0.0.1

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.
@@ -0,0 +1,1025 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, InjectionToken, inject, EventEmitter, ViewChild, Output, Input, Component, DestroyRef, Inject, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
+ import { PraxisTable } from '@praxisui/table';
4
+ import { Router, RouterLink } from '@angular/router';
5
+ import * as i1 from '@angular/material/dialog';
6
+ import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
7
+ export { MAT_DIALOG_DATA as DIALOG_DATA } from '@angular/material/dialog';
8
+ import * as i2 from '@praxisui/core';
9
+ import { CONFIG_STORAGE, GlobalConfigService, fillUndefined, EmptyStateCardComponent, PraxisIconDirective, GenericCrudService, ComponentMetadataRegistry } from '@praxisui/core';
10
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
11
+ import { MatSnackBar } from '@angular/material/snack-bar';
12
+ import * as i1$1 from '@angular/common';
13
+ import { CommonModule } from '@angular/common';
14
+ import * as i4 from '@angular/material/button';
15
+ import { MatButtonModule } from '@angular/material/button';
16
+ import * as i5 from '@angular/material/icon';
17
+ import { MatIconModule } from '@angular/material/icon';
18
+ import { PraxisDynamicForm } from '@praxisui/dynamic-form';
19
+ import { ConfirmDialogComponent } from '@praxisui/dynamic-fields';
20
+ import { filter } from 'rxjs/operators';
21
+
22
+ class DialogService {
23
+ matDialog;
24
+ zone;
25
+ constructor(matDialog, zone) {
26
+ this.matDialog = matDialog;
27
+ this.zone = zone;
28
+ }
29
+ open(component, config) {
30
+ return this.zone.run(() => this.matDialog.open(component, config));
31
+ }
32
+ async openAsync(loader, config) {
33
+ const component = await loader();
34
+ return this.zone.run(() => this.matDialog.open(component, config));
35
+ }
36
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DialogService, deps: [{ token: i1.MatDialog }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
37
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DialogService, providedIn: 'root' });
38
+ }
39
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DialogService, decorators: [{
40
+ type: Injectable,
41
+ args: [{ providedIn: 'root' }]
42
+ }], ctorParameters: () => [{ type: i1.MatDialog }, { type: i0.NgZone }] });
43
+
44
+ const CRUD_DRAWER_ADAPTER = new InjectionToken('CRUD_DRAWER_ADAPTER');
45
+
46
+ class CrudLauncherService {
47
+ router = inject(Router);
48
+ dialog = inject(DialogService);
49
+ storage = inject(CONFIG_STORAGE);
50
+ global = inject(GlobalConfigService);
51
+ drawerAdapter = (() => {
52
+ try {
53
+ return inject(CRUD_DRAWER_ADAPTER);
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ })();
59
+ async launch(action, row, metadata) {
60
+ // Carregar overrides de CRUD (se houver) e mesclar em uma cópia local
61
+ const merged = this.mergeCrudOverrides(metadata, action);
62
+ const mode = this.resolveOpenMode(merged.action, merged.metadata);
63
+ console.debug('[CRUD:Launcher] mode=', mode, 'action=', action);
64
+ if (mode === 'route') {
65
+ if (!merged.action.route) {
66
+ throw new Error(`Route not provided for action ${merged.action.action}`);
67
+ }
68
+ const url = this.buildRoute(merged.action, row, merged.metadata);
69
+ await this.router.navigateByUrl(url);
70
+ return { mode };
71
+ }
72
+ if (mode === 'drawer' && this.drawerAdapter) {
73
+ const inputs = this.mapInputs(merged.action, row);
74
+ const idField = merged.metadata.resource?.idField ?? 'id';
75
+ if (row && inputs[idField] === undefined && row[idField] !== undefined) {
76
+ inputs[idField] = row[idField];
77
+ }
78
+ await Promise.resolve(this.drawerAdapter.open({ action: merged.action, metadata: merged.metadata, inputs }));
79
+ return { mode };
80
+ }
81
+ if (!merged.action.formId) {
82
+ throw new Error(`formId not provided for action ${merged.action.action}`);
83
+ }
84
+ const inputs = this.mapInputs(merged.action, row);
85
+ const idField = merged.metadata.resource?.idField ?? 'id';
86
+ if (row && inputs[idField] === undefined && row[idField] !== undefined) {
87
+ inputs[idField] = row[idField];
88
+ }
89
+ const modalCfg = { ...(merged.metadata.defaults?.modal || {}) };
90
+ console.debug('[CRUD:Launcher] opening dialog with:', {
91
+ action: merged.action.action,
92
+ formId: merged.action.formId,
93
+ inputs,
94
+ modalCfg,
95
+ resourcePath: merged.metadata.resource?.path ?? merged.metadata.table?.resourcePath,
96
+ });
97
+ const panelClasses = ['pfx-dialog-pane', 'pfx-dialog-frosted'];
98
+ if (modalCfg.panelClass) {
99
+ panelClasses.push(modalCfg.panelClass);
100
+ }
101
+ // Backdrop style presets
102
+ const backdropClasses = [];
103
+ const style = modalCfg.backdropStyle;
104
+ if (!style || style === 'blur') {
105
+ backdropClasses.push('pfx-blur-backdrop');
106
+ }
107
+ else if (style === 'dim') {
108
+ backdropClasses.push('cdk-overlay-dark-backdrop');
109
+ }
110
+ else if (style === 'transparent') {
111
+ backdropClasses.push('pfx-transparent-backdrop');
112
+ }
113
+ if (modalCfg.backdropClass) {
114
+ backdropClasses.push(modalCfg.backdropClass);
115
+ }
116
+ const ref = await this.dialog.openAsync(() => Promise.resolve().then(function () { return dynamicFormDialogHost_component; }).then((m) => m.DynamicFormDialogHostComponent), {
117
+ ...modalCfg,
118
+ panelClass: panelClasses,
119
+ backdropClass: backdropClasses,
120
+ autoFocus: modalCfg.autoFocus ?? true,
121
+ restoreFocus: modalCfg.restoreFocus ?? true,
122
+ minWidth: '360px',
123
+ maxWidth: '95vw',
124
+ ariaLabelledBy: 'crudDialogTitle',
125
+ data: { action: merged.action, row, metadata: merged.metadata, inputs },
126
+ });
127
+ return { mode, ref };
128
+ }
129
+ resolveOpenMode(action, metadata) {
130
+ // Local precedence: action > metadata.defaults
131
+ const local = action.openMode ?? metadata.defaults?.openMode;
132
+ if (local)
133
+ return local;
134
+ // Global fallback (action-specific, then general), then hard fallback 'route'
135
+ try {
136
+ const globalCrud = this.global.getCrud();
137
+ const actionName = action.action;
138
+ const globalMode = (actionName && globalCrud?.actionDefaults?.[actionName]?.openMode) ?? globalCrud?.defaults?.openMode;
139
+ let resolved = globalMode ?? 'route';
140
+ // Safety: if modal/drawer but no formId, degrade to route to avoid runtime error
141
+ if ((resolved === 'modal' || resolved === 'drawer') && !action.formId) {
142
+ resolved = 'route';
143
+ }
144
+ return resolved;
145
+ }
146
+ catch {
147
+ return 'route';
148
+ }
149
+ }
150
+ buildRoute(action, row, metadata) {
151
+ let route = action.route;
152
+ const query = {};
153
+ action.params?.forEach((p) => {
154
+ const value = row?.[p.from];
155
+ if (p.to === 'routeParam') {
156
+ if (value === undefined || value === null) {
157
+ throw new Error(`Missing value for route param ${p.name}`);
158
+ }
159
+ const re = new RegExp(`:${p.name}\\b`, 'g');
160
+ route = route.replace(re, encodeURIComponent(String(value)));
161
+ }
162
+ else if (p.to === 'query' && value !== undefined && value !== null) {
163
+ query[p.name] = String(value);
164
+ }
165
+ });
166
+ const missing = route.match(/:[a-zA-Z0-9_-]+/g);
167
+ if (missing) {
168
+ throw new Error(`Missing route parameters for action "${action.action}": ${missing
169
+ .map((m) => m.slice(1))
170
+ .join(', ')}`);
171
+ }
172
+ // Ensure resource is present for routed forms so the formId is deterministic
173
+ try {
174
+ const resourcePath = metadata.resource?.path || metadata.table?.resourcePath;
175
+ if (resourcePath && query['resource'] === undefined) {
176
+ query['resource'] = resourcePath;
177
+ }
178
+ }
179
+ catch { }
180
+ // ReturnTo param for graceful back navigation on routed forms
181
+ const back = action.back || metadata.defaults?.back;
182
+ const returnTo = back?.returnTo || this.router.url;
183
+ if (returnTo) {
184
+ query['returnTo'] = returnTo;
185
+ }
186
+ const queryString = new URLSearchParams(query).toString();
187
+ return queryString ? `${route}?${queryString}` : route;
188
+ }
189
+ mapInputs(action, row) {
190
+ const inputs = {};
191
+ action.params?.forEach((p) => {
192
+ if (p.to === 'input') {
193
+ inputs[p.name] = row?.[p.from];
194
+ }
195
+ });
196
+ return inputs;
197
+ }
198
+ mergeCrudOverrides(metadata, action) {
199
+ try {
200
+ const tableId = metadata.resource?.path || metadata.table?.resourcePath || 'default';
201
+ const key = `crud-overrides:${tableId}`;
202
+ const saved = this.storage.loadConfig(key) || {};
203
+ const overDefaults = saved.defaults || {};
204
+ const overActions = saved.actions || {};
205
+ // Merge defaults (shallow for defaults, shallow for modal/back)
206
+ let mergedDefaults = {
207
+ ...(metadata.defaults || {}),
208
+ ...('openMode' in overDefaults ? { openMode: overDefaults.openMode } : {}),
209
+ modal: { ...(metadata.defaults?.modal || {}), ...(overDefaults.modal || {}) },
210
+ back: { ...(metadata.defaults?.back || {}), ...(overDefaults.back || {}) },
211
+ header: { ...(metadata.defaults?.header || {}), ...(overDefaults.header || {}) },
212
+ };
213
+ // Fill missing defaults from GlobalConfig (as last fallback)
214
+ try {
215
+ const globalCrud = this.global.getCrud();
216
+ if (globalCrud?.defaults) {
217
+ mergedDefaults = fillUndefined(mergedDefaults, globalCrud.defaults);
218
+ }
219
+ }
220
+ catch { }
221
+ // Merge metadata copy
222
+ const mergedMetadata = {
223
+ ...metadata,
224
+ defaults: mergedDefaults,
225
+ };
226
+ // Action-level override
227
+ const actOv = overActions[action.action] || {};
228
+ const mergedAction = {
229
+ ...action,
230
+ ...('openMode' in actOv ? { openMode: actOv.openMode } : {}),
231
+ ...('formId' in actOv ? { formId: actOv.formId } : {}),
232
+ ...('route' in actOv ? { route: actOv.route } : {}),
233
+ };
234
+ return { metadata: mergedMetadata, action: mergedAction };
235
+ }
236
+ catch {
237
+ return { metadata, action };
238
+ }
239
+ }
240
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudLauncherService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
241
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudLauncherService, providedIn: 'root' });
242
+ }
243
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudLauncherService, decorators: [{
244
+ type: Injectable,
245
+ args: [{ providedIn: 'root' }]
246
+ }] });
247
+
248
+ function assertCrudMetadata(meta) {
249
+ meta.actions?.forEach((action) => {
250
+ const mode = action.openMode ?? meta.defaults?.openMode ?? 'route';
251
+ if (mode === 'route' && !action.route) {
252
+ throw new Error(`Route not provided for action ${action.action}`);
253
+ }
254
+ if ((mode === 'modal' || mode === 'drawer') && !action.formId) {
255
+ throw new Error(`formId not provided for action ${action.action}`);
256
+ }
257
+ action.params?.forEach((p) => {
258
+ if (!['routeParam', 'query', 'input'].includes(p.to)) {
259
+ throw new Error(`Invalid param mapping target: ${p.to}`);
260
+ }
261
+ });
262
+ });
263
+ }
264
+
265
+ class PraxisCrudComponent {
266
+ /** Habilita visual de debug para alinhamento/layouot */
267
+ debugLayout = false;
268
+ /** JSON inline ou chave/URL resolvida pelo MetadataResolver */
269
+ metadata;
270
+ context;
271
+ /** Encaminha o modo de edição de layout para a tabela interna */
272
+ editModeEnabled = false;
273
+ /** CTA: usado pelo Builder para abrir configuração de metadados quando vazio */
274
+ configureRequested = new EventEmitter();
275
+ afterOpen = new EventEmitter();
276
+ afterClose = new EventEmitter();
277
+ afterSave = new EventEmitter();
278
+ afterDelete = new EventEmitter();
279
+ error = new EventEmitter();
280
+ resolvedMetadata;
281
+ /** Configuração efetiva da tabela com melhorias automáticas (ex.: botão Adicionar). */
282
+ effectiveTableConfig;
283
+ launcher = inject(CrudLauncherService);
284
+ table;
285
+ storage = inject(CONFIG_STORAGE);
286
+ snack = inject(MatSnackBar);
287
+ /**
288
+ * Stable CRUD context passed to PraxisTable.
289
+ * Previously this was created via a getter, producing a new object each CD tick
290
+ * and causing excessive re-renders. Now we compute it only when metadata changes.
291
+ */
292
+ tableCrudContext;
293
+ onResetPreferences() {
294
+ try {
295
+ const id = this.resolvedMetadata?.resource?.path || 'default';
296
+ const key = `crud-overrides:${id}`;
297
+ this.storage.clearConfig(key);
298
+ this.snack.open('Overrides de CRUD redefinidos', undefined, { duration: 2000 });
299
+ }
300
+ catch { }
301
+ }
302
+ ngOnChanges(changes) {
303
+ if (changes['metadata']) {
304
+ try {
305
+ this.resolvedMetadata =
306
+ typeof this.metadata === 'string'
307
+ ? JSON.parse(this.metadata)
308
+ : this.metadata;
309
+ assertCrudMetadata(this.resolvedMetadata);
310
+ this.effectiveTableConfig = this.buildEffectiveTableConfig(this.resolvedMetadata);
311
+ // Build a stable table context when metadata changes
312
+ this.tableCrudContext = this.buildTableCrudContext(this.resolvedMetadata);
313
+ }
314
+ catch (err) {
315
+ this.error.emit(err);
316
+ }
317
+ }
318
+ }
319
+ async onAction(action, row) {
320
+ try {
321
+ document.activeElement?.blur();
322
+ let actionMeta = this.resolvedMetadata.actions?.find((a) => a.action === action);
323
+ // Fallback: if metadata.actions is missing or doesn't include the action,
324
+ // synthesize from table CRUD context or let overrides drive behavior.
325
+ if (!actionMeta) {
326
+ const ctxAction = this.tableCrudContext?.actions?.find((a) => a.action === action);
327
+ if (ctxAction) {
328
+ actionMeta = {
329
+ action: ctxAction.action,
330
+ openMode: ctxAction.openMode,
331
+ formId: ctxAction.formId,
332
+ route: ctxAction.route,
333
+ };
334
+ }
335
+ else {
336
+ // Minimal stub – CrudLauncherService will merge overrides/defaults
337
+ actionMeta = { action };
338
+ }
339
+ }
340
+ const effectiveAction = (actionMeta || { action });
341
+ const { mode, ref } = await this.launcher.launch(effectiveAction, row, this.resolvedMetadata);
342
+ this.afterOpen.emit({ mode, action: effectiveAction.action });
343
+ if (ref) {
344
+ const idField = this.getIdField();
345
+ ref
346
+ .afterClosed()
347
+ .pipe(takeUntilDestroyed())
348
+ .subscribe((result) => {
349
+ this.afterClose.emit();
350
+ if (result?.type === 'save') {
351
+ const data = result.data;
352
+ const id = data?.[idField];
353
+ this.afterSave.emit({ id, data: result.data });
354
+ this.refreshTable();
355
+ }
356
+ if (result?.type === 'delete') {
357
+ const data = result.data;
358
+ const id = data?.[idField];
359
+ this.afterDelete.emit({ id });
360
+ this.refreshTable();
361
+ }
362
+ });
363
+ }
364
+ }
365
+ catch (err) {
366
+ this.error.emit(err);
367
+ }
368
+ }
369
+ refreshTable() {
370
+ this.table.refetch();
371
+ }
372
+ getIdField() {
373
+ return this.resolvedMetadata?.resource?.idField || 'id';
374
+ }
375
+ onConfigureRequested() {
376
+ this.configureRequested.emit();
377
+ }
378
+ /**
379
+ * Constrói uma configuração de tabela efetiva a partir dos metadados,
380
+ * adicionando automaticamente a ação de "Adicionar" na toolbar quando aplicável.
381
+ * - Evita duplicidade se a toolbar já tiver ação equivalente.
382
+ * - Garante visibilidade da toolbar quando a ação é injetada.
383
+ */
384
+ buildEffectiveTableConfig(meta) {
385
+ const base = meta.table;
386
+ if (!base)
387
+ return undefined;
388
+ // Clonar para evitar mutações
389
+ const cfg = JSON.parse(JSON.stringify(base));
390
+ const ensureToolbar = () => {
391
+ if (!cfg.toolbar) {
392
+ cfg.toolbar = { visible: true, position: 'top' };
393
+ }
394
+ return cfg.toolbar;
395
+ };
396
+ // Detectar se já existe ação de adicionar na toolbar
397
+ const hasToolbarAdd = (cfg.toolbar?.actions || []).some((a) => this.isAddLike(a));
398
+ // Procurar mapeamento de ação "adicionar/criar" nos metadados do CRUD
399
+ const addAction = (meta.actions || []).find((a) => this.isAddLike(a));
400
+ // Se não houver mapeamento "add/create", não vamos injetar nada
401
+ if (!addAction) {
402
+ return hasToolbarAdd ? cfg : undefined;
403
+ }
404
+ // Se já existe na toolbar, apenas garantir visibilidade se necessário
405
+ if (hasToolbarAdd) {
406
+ const tb = ensureToolbar();
407
+ tb.visible = true;
408
+ return cfg;
409
+ }
410
+ // Injetar ação na toolbar
411
+ const tb = ensureToolbar();
412
+ tb.visible = true;
413
+ if (!tb.actions)
414
+ tb.actions = [];
415
+ const injected = {
416
+ id: addAction.id || 'add',
417
+ label: addAction.label || 'Adicionar',
418
+ icon: addAction.icon || 'add',
419
+ type: 'button',
420
+ color: 'primary',
421
+ position: 'end',
422
+ action: addAction.action || 'add',
423
+ };
424
+ tb.actions.push(injected);
425
+ return cfg;
426
+ }
427
+ /** Heurística leve para identificar ações do tipo "adicionar/criar" */
428
+ isAddLike(a) {
429
+ if (!a)
430
+ return false;
431
+ const normalize = (s) => String(s || '').trim().toLowerCase();
432
+ const id = normalize(a.action || a.id || a.code || a.key || a.name || a.type);
433
+ const icon = normalize(a.icon);
434
+ const label = normalize(a.label);
435
+ const addIds = new Set(['add', 'create', 'novo', 'new', 'incluir', 'inserir']);
436
+ if (addIds.has(id))
437
+ return true;
438
+ if (icon === 'add' || icon === 'add_circle' || icon === 'add_box')
439
+ return true;
440
+ if (label === 'adicionar' || label === 'novo' || label === 'criar' || label === 'incluir')
441
+ return true;
442
+ return false;
443
+ }
444
+ /** Builds a stable CRUD context object for PraxisTable based on metadata. */
445
+ buildTableCrudContext(meta) {
446
+ if (!meta)
447
+ return undefined;
448
+ // Provide a minimal default action set when metadata.actions is empty,
449
+ // so the editor can expose per-action overrides entirely via UI.
450
+ const baseActions = meta.actions && meta.actions.length
451
+ ? meta.actions
452
+ : [{ action: 'create' }, { action: 'view' }, { action: 'edit' }];
453
+ return {
454
+ tableId: meta.resource?.path || meta.table?.resourcePath || 'default',
455
+ resourcePath: meta.resource?.path || meta.table?.resourcePath,
456
+ defaults: meta.defaults,
457
+ actions: baseActions.map((a) => ({
458
+ action: a.action,
459
+ label: a.label,
460
+ formId: a.formId,
461
+ route: a.route,
462
+ openMode: a.openMode,
463
+ })),
464
+ idField: meta.resource?.idField || 'id',
465
+ };
466
+ }
467
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisCrudComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
468
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: PraxisCrudComponent, isStandalone: true, selector: "praxis-crud", inputs: { debugLayout: "debugLayout", metadata: "metadata", context: "context", editModeEnabled: "editModeEnabled" }, outputs: { configureRequested: "configureRequested", afterOpen: "afterOpen", afterClose: "afterClose", afterSave: "afterSave", afterDelete: "afterDelete", error: "error" }, viewQueries: [{ propertyName: "table", first: true, predicate: PraxisTable, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
469
+ @if ($any(resolvedMetadata)?.resource?.path) {
470
+ <praxis-table
471
+ [config]="effectiveTableConfig || resolvedMetadata.table"
472
+ [resourcePath]="resolvedMetadata.resource?.path"
473
+ [tableId]="resolvedMetadata.resource?.path || 'default'"
474
+ [crudContext]="tableCrudContext"
475
+ [editModeEnabled]="editModeEnabled"
476
+ (rowAction)="onAction($event.action, $event.row)"
477
+ (toolbarAction)="onAction($event.action)"
478
+ (reset)="onResetPreferences()"
479
+ [debugLayout]="debugLayout"
480
+ ></praxis-table>
481
+ } @else {
482
+ @if (editModeEnabled) {
483
+ <praxis-empty-state-card
484
+ icon="table_rows"
485
+ [title]="'Conecte o CRUD a um recurso'"
486
+ [description]="'Informe os metadados (resourcePath / schema) para habilitar a tabela e ações.'"
487
+ [primaryAction]="{ label: 'Configurar metadados', icon: 'bolt', action: onConfigureRequested.bind(this) }"
488
+ />
489
+ }
490
+ }
491
+ `, isInline: true, dependencies: [{ kind: "component", type: PraxisTable, selector: "praxis-table", inputs: ["config", "resourcePath", "filterCriteria", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "showToolbar", "toolbarV2", "autoDelete", "editModeEnabled", "dense", "tableId", "debugLayout", "crudContext", "idField"], outputs: ["rowClick", "rowAction", "toolbarAction", "bulkAction", "rowDoubleClick", "schemaStatusChange", "beforeDelete", "afterDelete", "deleteError", "beforeBulkDelete", "afterBulkDelete", "bulkDeleteError"] }, { kind: "component", type: EmptyStateCardComponent, selector: "praxis-empty-state-card", inputs: ["icon", "title", "description", "primaryAction", "secondaryActions", "inline"] }] });
492
+ }
493
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisCrudComponent, decorators: [{
494
+ type: Component,
495
+ args: [{
496
+ selector: 'praxis-crud',
497
+ standalone: true,
498
+ imports: [PraxisTable, EmptyStateCardComponent],
499
+ template: `
500
+ @if ($any(resolvedMetadata)?.resource?.path) {
501
+ <praxis-table
502
+ [config]="effectiveTableConfig || resolvedMetadata.table"
503
+ [resourcePath]="resolvedMetadata.resource?.path"
504
+ [tableId]="resolvedMetadata.resource?.path || 'default'"
505
+ [crudContext]="tableCrudContext"
506
+ [editModeEnabled]="editModeEnabled"
507
+ (rowAction)="onAction($event.action, $event.row)"
508
+ (toolbarAction)="onAction($event.action)"
509
+ (reset)="onResetPreferences()"
510
+ [debugLayout]="debugLayout"
511
+ ></praxis-table>
512
+ } @else {
513
+ @if (editModeEnabled) {
514
+ <praxis-empty-state-card
515
+ icon="table_rows"
516
+ [title]="'Conecte o CRUD a um recurso'"
517
+ [description]="'Informe os metadados (resourcePath / schema) para habilitar a tabela e ações.'"
518
+ [primaryAction]="{ label: 'Configurar metadados', icon: 'bolt', action: onConfigureRequested.bind(this) }"
519
+ />
520
+ }
521
+ }
522
+ `,
523
+ }]
524
+ }], propDecorators: { debugLayout: [{
525
+ type: Input
526
+ }], metadata: [{
527
+ type: Input,
528
+ args: [{ required: true }]
529
+ }], context: [{
530
+ type: Input
531
+ }], editModeEnabled: [{
532
+ type: Input
533
+ }], configureRequested: [{
534
+ type: Output
535
+ }], afterOpen: [{
536
+ type: Output
537
+ }], afterClose: [{
538
+ type: Output
539
+ }], afterSave: [{
540
+ type: Output
541
+ }], afterDelete: [{
542
+ type: Output
543
+ }], error: [{
544
+ type: Output
545
+ }], table: [{
546
+ type: ViewChild,
547
+ args: [PraxisTable]
548
+ }] } });
549
+
550
+ class DynamicFormDialogHostComponent {
551
+ dialogRef;
552
+ data;
553
+ dialogService;
554
+ crud;
555
+ configStorage;
556
+ formComp;
557
+ modal = {};
558
+ maximized = false;
559
+ initialSize = {};
560
+ rememberState = false;
561
+ stateKey;
562
+ destroyRef = inject(DestroyRef);
563
+ resourcePath;
564
+ resourceId;
565
+ mode = 'create';
566
+ backConfig;
567
+ idField = 'id';
568
+ texts = {
569
+ title: 'Formulário',
570
+ close: 'Fechar',
571
+ closeLabel: 'Fechar diálogo',
572
+ maximizeLabel: 'Maximizar',
573
+ restoreLabel: 'Restaurar',
574
+ discardTitle: 'Descartar alterações?',
575
+ discardMessage: 'Você tem alterações não salvas. Deseja fechar assim mesmo?',
576
+ discardConfirm: 'Descartar',
577
+ discardCancel: 'Cancelar',
578
+ };
579
+ constructor(dialogRef, data, dialogService, crud, configStorage) {
580
+ this.dialogRef = dialogRef;
581
+ this.data = data;
582
+ this.dialogService = dialogService;
583
+ this.crud = crud;
584
+ this.configStorage = configStorage;
585
+ this.dialogRef.disableClose = true;
586
+ // i18n
587
+ this.texts = {
588
+ ...this.texts,
589
+ ...(this.data.metadata?.i18n?.crudDialog || {}),
590
+ };
591
+ // defaults do modal (inclui canMaximize: true)
592
+ this.modal = {
593
+ canMaximize: true,
594
+ ...(this.data.metadata?.defaults?.modal || {}),
595
+ };
596
+ this.rememberState = !!this.modal.rememberLastState;
597
+ this.stateKey = this.data.action?.formId
598
+ ? `crud-dialog-state:${this.data.action.formId}`
599
+ : undefined;
600
+ // derivar path/id/mode
601
+ this.resourcePath =
602
+ this.data.metadata?.resource?.path ??
603
+ this.data.metadata?.table?.resourcePath;
604
+ if (this.resourcePath) {
605
+ this.crud.configure(this.resourcePath, this.data.metadata?.resource?.endpointKey);
606
+ }
607
+ this.idField = this.data.metadata?.resource?.idField ?? 'id';
608
+ this.resourceId = this.data.inputs?.[this.idField];
609
+ const act = this.data.action?.action;
610
+ this.mode = act === 'edit' ? 'edit' : act === 'view' ? 'view' : 'create';
611
+ // Back config: defaults from metadata/action, overridden by saved per-form config
612
+ const defaults = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
613
+ const saved = this.data.action?.formId
614
+ ? this.configStorage.loadConfig(`crud-back:${this.data.action.formId}`)
615
+ : null;
616
+ this.backConfig = { ...defaults, ...(saved || {}) };
617
+ console.debug('[CRUD:Host] constructed', {
618
+ action: this.data?.action,
619
+ resourcePath: this.resourcePath,
620
+ resourceId: this.resourceId,
621
+ mode: this.mode,
622
+ modal: this.modal,
623
+ backConfig: this.backConfig,
624
+ });
625
+ // Esc
626
+ if (!this.modal.disableCloseOnEsc) {
627
+ this.dialogRef
628
+ .keydownEvents()
629
+ .pipe(filter((e) => e.key === 'Escape'), takeUntilDestroyed(this.destroyRef))
630
+ .subscribe(() => this.onCancel());
631
+ }
632
+ // Backdrop
633
+ if (!this.modal.disableCloseOnBackdrop) {
634
+ this.dialogRef
635
+ .backdropClick()
636
+ .pipe(takeUntilDestroyed(this.destroyRef))
637
+ .subscribe(() => this.onCancel());
638
+ }
639
+ // Salvar estado ao fechar, se aplicável
640
+ this.dialogRef
641
+ .afterClosed()
642
+ .pipe(takeUntilDestroyed(this.destroyRef))
643
+ .subscribe(() => this.saveState());
644
+ }
645
+ ngOnInit() {
646
+ // Carregar estado salvo (se habilitado)
647
+ const saved = this.rememberState && this.stateKey
648
+ ? this.configStorage.loadConfig(this.stateKey)
649
+ : null;
650
+ this.initialSize = {
651
+ width: saved?.width ?? this.modal.width,
652
+ height: saved?.height ?? this.modal.height,
653
+ };
654
+ console.debug('[CRUD:Host] ngOnInit', {
655
+ initialSize: this.initialSize,
656
+ startMaximized: this.modal.startMaximized,
657
+ fullscreenBreakpoint: this.modal.fullscreenBreakpoint,
658
+ });
659
+ let shouldMax = false;
660
+ if (saved && typeof saved.maximized === 'boolean') {
661
+ shouldMax = !!saved.maximized;
662
+ }
663
+ else {
664
+ shouldMax =
665
+ this.modal.startMaximized ||
666
+ (this.modal.fullscreenBreakpoint &&
667
+ window.innerWidth <= this.modal.fullscreenBreakpoint);
668
+ }
669
+ if (shouldMax) {
670
+ this.toggleMaximize(true);
671
+ }
672
+ else if (this.initialSize.width || this.initialSize.height) {
673
+ this.dialogRef.updateSize(this.initialSize.width, this.initialSize.height);
674
+ this.dialogRef.updatePosition();
675
+ }
676
+ }
677
+ onSave(result) {
678
+ if (this.modal.closeOnSave === false) {
679
+ // Não fechar: manter aberto e opcionalmente salvar estado atual
680
+ this.saveState();
681
+ return;
682
+ }
683
+ this.dialogRef.close({ type: 'save', data: result });
684
+ }
685
+ onCancel() {
686
+ const dirty = this.formComp?.form.dirty;
687
+ const backCfg = (this.data.action?.back || this.data.metadata?.defaults?.back) || {};
688
+ const confirm = backCfg.confirmOnDirty ?? true;
689
+ if (dirty && confirm) {
690
+ const ref = this.dialogService.open(ConfirmDialogComponent, {
691
+ data: {
692
+ title: this.texts.discardTitle,
693
+ message: this.texts.discardMessage,
694
+ confirmText: this.texts.discardConfirm,
695
+ cancelText: this.texts.discardCancel,
696
+ type: 'warning',
697
+ },
698
+ });
699
+ ref
700
+ .afterClosed()
701
+ .pipe(filter((confirmed) => !!confirmed), takeUntilDestroyed(this.destroyRef))
702
+ .subscribe(() => this.dialogRef.close());
703
+ }
704
+ else {
705
+ this.dialogRef.close();
706
+ }
707
+ }
708
+ toggleMaximize(initial = false) {
709
+ this.maximized = initial ? true : !this.maximized;
710
+ const gap = this.maximized ? this.modal.edgeGap ?? 8 : undefined;
711
+ const width = this.maximized
712
+ ? `calc(100vw - ${2 * (gap ?? 0)}px)`
713
+ : this.initialSize.width;
714
+ const height = this.maximized
715
+ ? `calc(100dvh - ${2 * (gap ?? 0)}px)`
716
+ : this.initialSize.height;
717
+ this.dialogRef.updateSize(width, height);
718
+ this.dialogRef.updatePosition();
719
+ const pane = this.dialogRef
720
+ ?._containerInstance?._elementRef?.nativeElement?.parentElement;
721
+ if (pane && pane.classList.contains('pfx-dialog-pane')) {
722
+ pane.style.margin = this.maximized ? `${gap}px` : '';
723
+ }
724
+ this.saveState();
725
+ }
726
+ saveState() {
727
+ if (!this.rememberState || !this.stateKey)
728
+ return;
729
+ const pane = this.dialogRef
730
+ ?._containerInstance?._elementRef?.nativeElement?.parentElement;
731
+ const style = pane ? getComputedStyle(pane) : null;
732
+ const currentWidth = style?.width || this.initialSize.width;
733
+ const currentHeight = style?.height || this.initialSize.height;
734
+ this.configStorage.saveConfig(this.stateKey, {
735
+ maximized: this.maximized,
736
+ width: currentWidth,
737
+ height: currentHeight,
738
+ });
739
+ }
740
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DynamicFormDialogHostComponent, deps: [{ token: MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: DialogService }, { token: i2.GenericCrudService }, { token: CONFIG_STORAGE }], target: i0.ɵɵFactoryTarget.Component });
741
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: DynamicFormDialogHostComponent, isStandalone: true, selector: "praxis-dynamic-form-dialog-host", host: { properties: { "attr.data-density": "modal.density || \"default\"" }, classAttribute: "praxis-dialog" }, providers: [GenericCrudService], viewQueries: [{ propertyName: "formComp", first: true, predicate: PraxisDynamicForm, descendants: true }], ngImport: i0, template: `
742
+ <div class="dialog-header">
743
+ <h2 id="crudDialogTitle" class="dialog-title">
744
+ {{ data.action?.label || texts.title }}
745
+ </h2>
746
+ <span class="spacer"></span>
747
+ @if (modal.canMaximize) {
748
+ <button
749
+ mat-icon-button
750
+ type="button"
751
+ (click)="toggleMaximize()"
752
+ [attr.aria-label]="
753
+ maximized ? texts.restoreLabel : texts.maximizeLabel
754
+ "
755
+ >
756
+ <mat-icon>{{
757
+ maximized ? 'close_fullscreen' : 'open_in_full'
758
+ }}</mat-icon>
759
+ </button>
760
+ }
761
+ <button
762
+ mat-icon-button
763
+ type="button"
764
+ (click)="onCancel()"
765
+ [attr.aria-label]="texts.closeLabel"
766
+ cdkFocusInitial
767
+ >
768
+ <mat-icon [praxisIcon]="'close'"></mat-icon>
769
+ </button>
770
+ </div>
771
+
772
+ <mat-dialog-content
773
+ class="dialog-content"
774
+ aria-labelledby="crudDialogTitle"
775
+ >
776
+ <praxis-dynamic-form
777
+ [formId]="data.action?.formId"
778
+ [resourcePath]="resourcePath"
779
+ [resourceId]="resourceId"
780
+ [mode]="mode"
781
+ [backConfig]="backConfig"
782
+ (formSubmit)="onSave($event)"
783
+ (formCancel)="onCancel()"
784
+ ></praxis-dynamic-form>
785
+ </mat-dialog-content>
786
+ `, isInline: true, styles: [":host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}:host([data-density=\"compact\"]){--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}.dialog-header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--dlg-pad);padding:0 var(--dlg-pad);height:var(--dlg-header-h);background:var(--md-sys-color-surface-container-high);border-bottom:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface)}.dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}.spacer{flex:1}.dialog-content{overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}.dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}.dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary),transparent 90%)}.dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisDynamicForm, selector: "praxis-dynamic-form", inputs: ["resourcePath", "resourceId", "mode", "config", "schemaSource", "editModeEnabled", "formId", "layout", "backConfig", "hooks", "removeEmptyContainersOnSave", "reactiveValidation", "reactiveValidationDebounceMs", "notifyIfOutdated", "snoozeMs", "autoOpenSettingsOnOutdated", "readonlyModeGlobal", "disabledModeGlobal", "presentationModeGlobal", "visibleGlobal", "customEndpoints"], outputs: ["formSubmit", "formCancel", "formReset", "configChange", "formReady", "valueChange", "syncCompleted", "initializationError", "editModeEnabledChange", "customAction", "actionConfirmation", "schemaStatusChange"] }] });
787
+ }
788
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DynamicFormDialogHostComponent, decorators: [{
789
+ type: Component,
790
+ args: [{ selector: 'praxis-dynamic-form-dialog-host', standalone: true, imports: [
791
+ CommonModule,
792
+ MatDialogModule,
793
+ MatButtonModule,
794
+ MatIconModule,
795
+ PraxisIconDirective,
796
+ PraxisDynamicForm,
797
+ ], providers: [GenericCrudService], host: {
798
+ class: 'praxis-dialog',
799
+ '[attr.data-density]': 'modal.density || "default"',
800
+ }, template: `
801
+ <div class="dialog-header">
802
+ <h2 id="crudDialogTitle" class="dialog-title">
803
+ {{ data.action?.label || texts.title }}
804
+ </h2>
805
+ <span class="spacer"></span>
806
+ @if (modal.canMaximize) {
807
+ <button
808
+ mat-icon-button
809
+ type="button"
810
+ (click)="toggleMaximize()"
811
+ [attr.aria-label]="
812
+ maximized ? texts.restoreLabel : texts.maximizeLabel
813
+ "
814
+ >
815
+ <mat-icon>{{
816
+ maximized ? 'close_fullscreen' : 'open_in_full'
817
+ }}</mat-icon>
818
+ </button>
819
+ }
820
+ <button
821
+ mat-icon-button
822
+ type="button"
823
+ (click)="onCancel()"
824
+ [attr.aria-label]="texts.closeLabel"
825
+ cdkFocusInitial
826
+ >
827
+ <mat-icon [praxisIcon]="'close'"></mat-icon>
828
+ </button>
829
+ </div>
830
+
831
+ <mat-dialog-content
832
+ class="dialog-content"
833
+ aria-labelledby="crudDialogTitle"
834
+ >
835
+ <praxis-dynamic-form
836
+ [formId]="data.action?.formId"
837
+ [resourcePath]="resourcePath"
838
+ [resourceId]="resourceId"
839
+ [mode]="mode"
840
+ [backConfig]="backConfig"
841
+ (formSubmit)="onSave($event)"
842
+ (formCancel)="onCancel()"
843
+ ></praxis-dynamic-form>
844
+ </mat-dialog-content>
845
+ `, styles: [":host{--dlg-header-h: 56px;--dlg-footer-h: 56px;--dlg-pad: 16px;display:flex;flex-direction:column;height:100%;overflow:hidden}:host([data-density=\"compact\"]){--dlg-header-h: 44px;--dlg-footer-h: 44px;--dlg-pad: 12px}.dialog-header{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:var(--dlg-pad);padding:0 var(--dlg-pad);height:var(--dlg-header-h);background:var(--md-sys-color-surface-container-high);border-bottom:1px solid var(--md-sys-color-outline-variant);color:var(--md-sys-color-on-surface)}.dialog-title{margin:0;font:inherit;font-weight:600;color:var(--md-sys-color-on-surface)}.spacer{flex:1}.dialog-content{overflow:auto;padding:var(--dlg-pad);max-height:calc(100svh - var(--dlg-header-h) - 32px)}.dialog-header button.mat-icon-button{color:var(--md-sys-color-on-surface-variant)}.dialog-header button.mat-icon-button:hover{color:var(--md-sys-color-primary);background:color-mix(in srgb,var(--md-sys-color-primary),transparent 90%)}.dialog-footer{position:sticky;bottom:0;z-index:1;padding:var(--dlg-pad)}\n"] }]
846
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
847
+ type: Inject,
848
+ args: [MatDialogRef]
849
+ }] }, { type: undefined, decorators: [{
850
+ type: Inject,
851
+ args: [MAT_DIALOG_DATA]
852
+ }] }, { type: DialogService }, { type: i2.GenericCrudService }, { type: undefined, decorators: [{
853
+ type: Inject,
854
+ args: [CONFIG_STORAGE]
855
+ }] }], propDecorators: { formComp: [{
856
+ type: ViewChild,
857
+ args: [PraxisDynamicForm]
858
+ }] } });
859
+
860
+ var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
861
+ __proto__: null,
862
+ DynamicFormDialogHostComponent: DynamicFormDialogHostComponent
863
+ });
864
+
865
+ /** Metadata for PraxisCrudComponent */
866
+ const PRAXIS_CRUD_COMPONENT_METADATA = {
867
+ id: 'praxis-crud',
868
+ selector: 'praxis-crud',
869
+ component: PraxisCrudComponent,
870
+ friendlyName: 'Praxis CRUD',
871
+ description: 'Tabela com operações de CRUD via metadados.',
872
+ icon: 'table_chart',
873
+ inputs: [
874
+ {
875
+ name: 'metadata',
876
+ type: 'CrudMetadata | string',
877
+ description: 'Metadados de configuração do CRUD',
878
+ },
879
+ {
880
+ name: 'context',
881
+ type: 'Record<string, unknown>',
882
+ description: 'Contexto adicional para resoluções',
883
+ },
884
+ {
885
+ name: 'editModeEnabled',
886
+ type: 'boolean',
887
+ default: false,
888
+ description: 'Habilita modo de edição do layout',
889
+ },
890
+ ],
891
+ outputs: [
892
+ {
893
+ name: 'afterOpen',
894
+ type: '{ mode: FormOpenMode; action: string }',
895
+ description: 'Emitido após abrir diálogo',
896
+ },
897
+ {
898
+ name: 'afterClose',
899
+ type: 'void',
900
+ description: 'Emitido após fechar diálogo',
901
+ },
902
+ {
903
+ name: 'afterSave',
904
+ type: '{ id: string | number; data: unknown }',
905
+ description: 'Emitido ao salvar',
906
+ },
907
+ {
908
+ name: 'afterDelete',
909
+ type: '{ id: string | number }',
910
+ description: 'Emitido ao deletar',
911
+ },
912
+ { name: 'error', type: 'unknown', description: 'Emitido em erros' },
913
+ {
914
+ name: 'configureRequested',
915
+ type: 'void',
916
+ description: 'Emitido quando CTA de configuração é acionado em modo edição',
917
+ },
918
+ ],
919
+ tags: ['widget', 'crud', 'configurable', 'hasWizard', 'stable'],
920
+ lib: '@praxisui/crud',
921
+ };
922
+ /** Provider para auto-registrar metadados do componente CRUD. */
923
+ function providePraxisCrudMetadata() {
924
+ return {
925
+ provide: ENVIRONMENT_INITIALIZER,
926
+ multi: true,
927
+ useFactory: (registry) => () => {
928
+ registry.register(PRAXIS_CRUD_COMPONENT_METADATA);
929
+ },
930
+ deps: [ComponentMetadataRegistry],
931
+ };
932
+ }
933
+
934
+ class CrudPageHeaderComponent {
935
+ title = '';
936
+ backLabel;
937
+ showBack = true;
938
+ variant = 'ghost';
939
+ sticky = true;
940
+ divider = true;
941
+ returnTo;
942
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudPageHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
943
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: CrudPageHeaderComponent, isStandalone: true, selector: "praxis-crud-page-header", inputs: { title: "title", backLabel: "backLabel", showBack: "showBack", variant: "variant", sticky: "sticky", divider: "divider", returnTo: "returnTo" }, ngImport: i0, template: `
944
+ <header
945
+ class="crud-header"
946
+ [class.sticky]="sticky"
947
+ [class.with-divider]="divider"
948
+ >
949
+ <div class="left">
950
+ <a
951
+ *ngIf="showBack && returnTo"
952
+ class="back-btn"
953
+ [class.ghost]="variant === 'ghost'"
954
+ [class.tonal]="variant === 'tonal'"
955
+ [class.outlined]="variant === 'outlined'"
956
+ [routerLink]="returnTo"
957
+ [attr.aria-label]="backLabel || 'Voltar'"
958
+ mat-button
959
+ >
960
+ <mat-icon [praxisIcon]="'arrow_back'"></mat-icon>
961
+ <span class="label" [class.hide-on-narrow]="true">{{ backLabel || 'Voltar' }}</span>
962
+ </a>
963
+ <h2 class="title" [attr.title]="title">{{ title }}</h2>
964
+ </div>
965
+ <div class="right">
966
+ <ng-content></ng-content>
967
+ </div>
968
+ </header>
969
+ `, isInline: true, styles: [".crud-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0;background:var(--md-sys-color-surface, transparent)}.crud-header.sticky{position:sticky;top:0;z-index:10;-webkit-backdrop-filter:saturate(110%);backdrop-filter:saturate(110%)}.crud-header.with-divider{border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.08))}.left{display:flex;align-items:center;gap:8px;min-width:0}.right{display:flex;align-items:center;gap:8px}.title{margin:0;font-weight:600;color:var(--md-sys-color-on-surface, currentColor);font:var(--mdc-typography-title-large, 600 20px/28px system-ui);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.back-btn{display:inline-flex;align-items:center;gap:6px;text-decoration:none}.back-btn .mat-mdc-button{padding-left:0}.back-btn mat-icon{color:var(--md-sys-color-on-surface-variant, currentColor)}.back-btn .label{color:var(--md-sys-color-on-surface-variant, currentColor)}.back-btn.ghost{border-radius:20px;padding:4px 8px}.back-btn.ghost:hover{background:color-mix(in srgb,var(--md-sys-color-primary) 8%,transparent)}.back-btn.tonal{border-radius:20px;padding:4px 10px;background:var(--md-sys-color-surface-container, rgba(0,0,0,.02));box-shadow:inset 0 0 0 1px var(--md-sys-color-outline-variant, rgba(0,0,0,.06))}.back-btn.tonal:hover{background:color-mix(in srgb,var(--md-sys-color-primary) 10%,var(--md-sys-color-surface-container))}.back-btn.outlined{border-radius:20px;padding:4px 10px;border:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.12));background:var(--md-sys-color-surface, transparent)}.back-btn.outlined:hover{border-color:color-mix(in srgb,var(--md-sys-color-primary) 40%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary) 6%,transparent)}@media (max-width: 599px){.label.hide-on-narrow{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }] });
970
+ }
971
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: CrudPageHeaderComponent, decorators: [{
972
+ type: Component,
973
+ args: [{ selector: 'praxis-crud-page-header', standalone: true, imports: [CommonModule, RouterLink, MatButtonModule, MatIconModule, PraxisIconDirective], template: `
974
+ <header
975
+ class="crud-header"
976
+ [class.sticky]="sticky"
977
+ [class.with-divider]="divider"
978
+ >
979
+ <div class="left">
980
+ <a
981
+ *ngIf="showBack && returnTo"
982
+ class="back-btn"
983
+ [class.ghost]="variant === 'ghost'"
984
+ [class.tonal]="variant === 'tonal'"
985
+ [class.outlined]="variant === 'outlined'"
986
+ [routerLink]="returnTo"
987
+ [attr.aria-label]="backLabel || 'Voltar'"
988
+ mat-button
989
+ >
990
+ <mat-icon [praxisIcon]="'arrow_back'"></mat-icon>
991
+ <span class="label" [class.hide-on-narrow]="true">{{ backLabel || 'Voltar' }}</span>
992
+ </a>
993
+ <h2 class="title" [attr.title]="title">{{ title }}</h2>
994
+ </div>
995
+ <div class="right">
996
+ <ng-content></ng-content>
997
+ </div>
998
+ </header>
999
+ `, styles: [".crud-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0;background:var(--md-sys-color-surface, transparent)}.crud-header.sticky{position:sticky;top:0;z-index:10;-webkit-backdrop-filter:saturate(110%);backdrop-filter:saturate(110%)}.crud-header.with-divider{border-bottom:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.08))}.left{display:flex;align-items:center;gap:8px;min-width:0}.right{display:flex;align-items:center;gap:8px}.title{margin:0;font-weight:600;color:var(--md-sys-color-on-surface, currentColor);font:var(--mdc-typography-title-large, 600 20px/28px system-ui);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.back-btn{display:inline-flex;align-items:center;gap:6px;text-decoration:none}.back-btn .mat-mdc-button{padding-left:0}.back-btn mat-icon{color:var(--md-sys-color-on-surface-variant, currentColor)}.back-btn .label{color:var(--md-sys-color-on-surface-variant, currentColor)}.back-btn.ghost{border-radius:20px;padding:4px 8px}.back-btn.ghost:hover{background:color-mix(in srgb,var(--md-sys-color-primary) 8%,transparent)}.back-btn.tonal{border-radius:20px;padding:4px 10px;background:var(--md-sys-color-surface-container, rgba(0,0,0,.02));box-shadow:inset 0 0 0 1px var(--md-sys-color-outline-variant, rgba(0,0,0,.06))}.back-btn.tonal:hover{background:color-mix(in srgb,var(--md-sys-color-primary) 10%,var(--md-sys-color-surface-container))}.back-btn.outlined{border-radius:20px;padding:4px 10px;border:1px solid var(--md-sys-color-outline-variant, rgba(0,0,0,.12));background:var(--md-sys-color-surface, transparent)}.back-btn.outlined:hover{border-color:color-mix(in srgb,var(--md-sys-color-primary) 40%,var(--md-sys-color-outline-variant));background:color-mix(in srgb,var(--md-sys-color-primary) 6%,transparent)}@media (max-width: 599px){.label.hide-on-narrow{display:none}}\n"] }]
1000
+ }], propDecorators: { title: [{
1001
+ type: Input
1002
+ }], backLabel: [{
1003
+ type: Input
1004
+ }], showBack: [{
1005
+ type: Input
1006
+ }], variant: [{
1007
+ type: Input
1008
+ }], sticky: [{
1009
+ type: Input
1010
+ }], divider: [{
1011
+ type: Input
1012
+ }], returnTo: [{
1013
+ type: Input
1014
+ }] } });
1015
+
1016
+ /*
1017
+ * Public API Surface of praxis-crud
1018
+ */
1019
+
1020
+ /**
1021
+ * Generated bundle index. Do not edit.
1022
+ */
1023
+
1024
+ export { CRUD_DRAWER_ADAPTER, CrudLauncherService, CrudPageHeaderComponent, DialogService, DynamicFormDialogHostComponent, PRAXIS_CRUD_COMPONENT_METADATA, PraxisCrudComponent, assertCrudMetadata, providePraxisCrudMetadata };
1025
+ //# sourceMappingURL=praxisui-crud.mjs.map