@praxisui/files-upload 1.0.0-beta.61 → 1.0.0-beta.64

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,44 +1,70 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, inject, signal, Inject, Component, InjectionToken, Optional, EventEmitter, ViewChild, Output, Input, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import { ActivatedRoute } from '@angular/router';
4
- import * as i7 from '@angular/common';
4
+ import * as i9 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
6
6
  import * as i1$1 from '@angular/forms';
7
7
  import { ReactiveFormsModule, FormsModule, FormControl } from '@angular/forms';
8
- import * as i9 from '@angular/material/button';
8
+ import * as i11 from '@angular/material/button';
9
9
  import { MatButtonModule } from '@angular/material/button';
10
10
  import * as i10 from '@angular/material/icon';
11
11
  import { MatIconModule } from '@angular/material/icon';
12
- import * as i11$1 from '@angular/material/progress-bar';
12
+ import * as i13 from '@angular/material/progress-bar';
13
13
  import { MatProgressBarModule } from '@angular/material/progress-bar';
14
- import * as i3$1 from '@angular/material/menu';
14
+ import * as i14 from '@angular/material/menu';
15
15
  import { MatMenuModule } from '@angular/material/menu';
16
16
  import * as i1 from '@angular/common/http';
17
- import { HttpHeaders, HttpEventType, HttpErrorResponse } from '@angular/common/http';
18
- import { ComponentKeyService, ASYNC_CONFIG_STORAGE, PraxisIconDirective, ComponentMetadataRegistry } from '@praxisui/core';
17
+ import { HttpHeaders, HttpEventType, HttpResponse, HttpErrorResponse } from '@angular/common/http';
18
+ import * as i1$2 from '@praxisui/core';
19
+ import { providePraxisI18n, ComponentKeyService, ASYNC_CONFIG_STORAGE, PraxisIconDirective, PraxisI18nService, ComponentMetadataRegistry } from '@praxisui/core';
19
20
  import * as i4 from '@angular/material/tabs';
20
21
  import { MatTabsModule } from '@angular/material/tabs';
21
22
  import * as i5 from '@angular/material/form-field';
22
23
  import { MatFormFieldModule } from '@angular/material/form-field';
23
24
  import * as i6 from '@angular/material/input';
24
25
  import { MatInputModule } from '@angular/material/input';
25
- import * as i7$1 from '@angular/material/checkbox';
26
+ import * as i7 from '@angular/material/checkbox';
26
27
  import { MatCheckboxModule } from '@angular/material/checkbox';
27
28
  import * as i8 from '@angular/material/select';
28
29
  import { MatSelectModule } from '@angular/material/select';
29
- import * as i11 from '@angular/material/tooltip';
30
+ import * as i11$1 from '@angular/material/tooltip';
30
31
  import { MatTooltipModule } from '@angular/material/tooltip';
31
32
  import * as i2 from '@angular/material/snack-bar';
32
33
  import { MatSnackBarModule } from '@angular/material/snack-bar';
33
34
  import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
34
- import { of, throwError, BehaviorSubject, map as map$1, startWith, retry, debounceTime } from 'rxjs';
35
- import * as i1$2 from '@praxisui/settings-panel';
35
+ import { of, throwError, BehaviorSubject, map as map$1, startWith, retry, debounceTime, from } from 'rxjs';
36
+ import * as i1$3 from '@praxisui/settings-panel';
36
37
  import { SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
37
- import { map, catchError, take } from 'rxjs/operators';
38
+ import { map, catchError, take, concatMap, toArray, tap, filter, defaultIfEmpty } from 'rxjs/operators';
38
39
  import { TemplatePortal } from '@angular/cdk/portal';
39
40
  import * as i3 from '@angular/cdk/overlay';
41
+ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
40
42
  import { SimpleBaseInputComponent } from '@praxisui/dynamic-fields';
41
43
 
44
+ function isStructuredErrorItem(value) {
45
+ return !!value && typeof value === 'object' && 'code' in value && 'message' in value;
46
+ }
47
+ function isErrorEnvelope(value) {
48
+ return !!value && typeof value === 'object' && 'status' in value;
49
+ }
50
+ /** Resolves the primary structured error item from the backend error payload. */
51
+ function getPrimaryErrorItem(error) {
52
+ if (!error) {
53
+ return undefined;
54
+ }
55
+ if (isStructuredErrorItem(error) && !isErrorEnvelope(error)) {
56
+ return error;
57
+ }
58
+ if (!isErrorEnvelope(error)) {
59
+ return undefined;
60
+ }
61
+ const structured = error.errors?.[0];
62
+ if (structured) {
63
+ return structured;
64
+ }
65
+ return undefined;
66
+ }
67
+
42
68
  var ErrorCode;
43
69
  (function (ErrorCode) {
44
70
  ErrorCode["INVALID_FILE_TYPE"] = "INVALID_FILE_TYPE";
@@ -433,7 +459,7 @@ class PraxisFilesUploadConfigEditor {
433
459
  dense: [false],
434
460
  accept: [''],
435
461
  showMetadataForm: [false],
436
- // NOVO: grupos específicos
462
+ // Grupos específicos de UI
437
463
  dropzone: this.fb.group({
438
464
  expandOnDragProximity: [true],
439
465
  proximityPx: [64],
@@ -454,18 +480,18 @@ class PraxisFilesUploadConfigEditor {
454
480
  maxFileSizeBytes: [null],
455
481
  maxFilesPerBulk: [null],
456
482
  maxBulkSizeBytes: [null],
483
+ }),
484
+ options: this.fb.group({
457
485
  defaultConflictPolicy: ['RENAME'],
458
- failFast: [false],
459
486
  strictValidation: [true],
460
487
  maxUploadSizeMb: [50],
461
- }),
462
- options: this.fb.group({
463
488
  allowedExtensions: [''],
464
489
  acceptMimeTypes: [''],
465
490
  targetDirectory: [''],
466
491
  enableVirusScanning: [false],
467
492
  }),
468
493
  bulk: this.fb.group({
494
+ failFast: [false],
469
495
  parallelUploads: [1],
470
496
  retryCount: [0],
471
497
  retryBackoffMs: [0],
@@ -483,7 +509,7 @@ class PraxisFilesUploadConfigEditor {
483
509
  headers: this.fb.group({
484
510
  tenantHeader: ['X-Tenant-Id'],
485
511
  userHeader: ['X-User-Id'],
486
- // novos campos: valores para consulta do servidor
512
+ // Valores usados nas consultas de configuração do servidor
487
513
  tenantValue: [''],
488
514
  userValue: [''],
489
515
  }),
@@ -537,7 +563,7 @@ class PraxisFilesUploadConfigEditor {
537
563
  if (patch.ui?.accept) {
538
564
  patch.ui = { ...patch.ui, accept: patch.ui.accept.join(',') };
539
565
  }
540
- // NOVO: normalizar lista de detalhes para CSV
566
+ // Normalizar lista de detalhes para CSV
541
567
  if (patch.ui?.list?.detailsFields) {
542
568
  patch.ui = {
543
569
  ...patch.ui,
@@ -570,22 +596,18 @@ class PraxisFilesUploadConfigEditor {
570
596
  applyServerConfig(cfg) {
571
597
  if (!cfg)
572
598
  return;
573
- // Options mapeadas para "Validações" e "Opções"
599
+ // Configuração efetiva do backend separada em validações locais, opções
600
+ // de backend e execução de lote.
574
601
  const options = cfg.options ?? {};
575
602
  const bulk = cfg.bulk ?? {};
576
- // limits
603
+ // Validações locais alimentadas pelo backend
577
604
  this.limitsGroup.patchValue({
578
- defaultConflictPolicy: options.nameConflictPolicy ?? null,
579
- strictValidation: options.strictValidation ?? null,
580
- maxUploadSizeMb: options.maxUploadSizeMb ?? null,
581
- // failFast padrão do servidor pode influenciar a UI de validação em lote
582
- failFast: bulk.failFastModeDefault ?? false,
583
- // preencher máx. arquivos por lote a partir do backend
605
+ // Preencher máx. arquivos por lote a partir do backend
584
606
  maxFilesPerBulk: typeof bulk.maxFilesPerBatch === 'number'
585
607
  ? bulk.maxFilesPerBatch
586
608
  : null,
587
609
  }, { emitEvent: false });
588
- // options (normalizar arrays em string CSV)
610
+ // Opções de backend (normalizar arrays em string CSV)
589
611
  const allowed = Array.isArray(options.allowedExtensions)
590
612
  ? options.allowedExtensions.join(',')
591
613
  : '';
@@ -593,13 +615,17 @@ class PraxisFilesUploadConfigEditor {
593
615
  ? options.acceptMimeTypes.join(',')
594
616
  : '';
595
617
  this.optionsGroup.patchValue({
618
+ defaultConflictPolicy: options.nameConflictPolicy ?? null,
619
+ strictValidation: options.strictValidation ?? null,
620
+ maxUploadSizeMb: options.maxUploadSizeMb ?? null,
596
621
  allowedExtensions: allowed,
597
622
  acceptMimeTypes: mimes,
598
623
  targetDirectory: options.targetDirectory ?? '',
599
624
  enableVirusScanning: !!options.enableVirusScanning,
600
625
  }, { emitEvent: false });
601
- // bulk (UI)
626
+ // Execução de lote
602
627
  this.bulkGroup.patchValue({
628
+ failFast: bulk.failFastModeDefault ?? false,
603
629
  parallelUploads: typeof bulk.maxConcurrentUploads === 'number'
604
630
  ? bulk.maxConcurrentUploads
605
631
  : 1,
@@ -725,10 +751,165 @@ class PraxisFilesUploadConfigEditor {
725
751
  refetchServerConfig() {
726
752
  this.state.refetch();
727
753
  }
754
+ get summaryStrategyTitle() {
755
+ const strategy = this.form.get('strategy')?.value;
756
+ if (strategy === 'presign')
757
+ return 'Upload com URL pré-assinada';
758
+ if (strategy === 'auto')
759
+ return 'Seleção automática da estratégia';
760
+ return 'Upload direto ao backend';
761
+ }
762
+ get summaryStrategyDetail() {
763
+ const strategy = this.form.get('strategy')?.value;
764
+ if (strategy === 'presign') {
765
+ return 'Depende de endpoint de presign e costuma reduzir carga direta no servidor principal.';
766
+ }
767
+ if (strategy === 'auto') {
768
+ return 'Tenta presign primeiro e volta ao fluxo direto quando necessário.';
769
+ }
770
+ return 'Fluxo mais simples de integrar e diagnosticar em ambientes controlados.';
771
+ }
772
+ get summaryExperienceTitle() {
773
+ const ui = this.uiGroup.value;
774
+ if (ui?.manualUpload)
775
+ return 'Revisão manual antes do envio';
776
+ if (ui?.showDropzone === false)
777
+ return 'Seleção orientada por campo compacto';
778
+ return 'Envio imediato com dropzone visível';
779
+ }
780
+ get summaryExperienceDetail() {
781
+ const ui = this.uiGroup.value;
782
+ const tokens = [
783
+ ui?.showProgress ? 'progresso visível' : 'sem barra de progresso',
784
+ ui?.showConflictPolicySelector
785
+ ? 'escolha de conflito disponível'
786
+ : 'política de conflito escondida',
787
+ ui?.showMetadataForm ? 'metadados editáveis na UI' : 'sem formulário extra',
788
+ ];
789
+ return tokens.join(' • ');
790
+ }
791
+ get summaryLimitsTitle() {
792
+ const maxFile = this.optionsGroup.get('maxUploadSizeMb')?.value;
793
+ const maxBulk = this.limitsGroup.get('maxFilesPerBulk')?.value;
794
+ return `${maxFile || '—'} MB por arquivo • ${maxBulk || '—'} itens por lote`;
795
+ }
796
+ get summaryLimitsDetail() {
797
+ const parallel = this.bulkGroup.get('parallelUploads')?.value;
798
+ const failFast = this.bulkGroup.get('failFast')?.value;
799
+ return `${parallel || 1} upload(s) paralelos • ${failFast ? 'fail-fast ativo' : 'falhas parciais permitidas'}`;
800
+ }
801
+ get summaryServerTitle() {
802
+ const conflict = this.optionsGroup.get('defaultConflictPolicy')?.value || 'RENAME';
803
+ return `Conflito padrão: ${conflict}`;
804
+ }
805
+ get summaryServerDetail() {
806
+ const strict = this.optionsGroup.get('strictValidation')?.value;
807
+ const virus = this.optionsGroup.get('enableVirusScanning')?.value;
808
+ return `${strict ? 'validação rigorosa ligada' : 'validação rígida desativada'} • ${virus ? 'antivírus forçado' : 'antivírus opcional/inativo'}`;
809
+ }
810
+ get activeRisks() {
811
+ const risks = [];
812
+ if (this.optionsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE') {
813
+ risks.push({
814
+ title: 'Sobrescrita habilitada',
815
+ detail: 'Arquivos existentes podem ser substituídos silenciosamente se o backend aceitar a operação.',
816
+ });
817
+ }
818
+ if (this.optionsGroup.get('enableVirusScanning')?.value === true) {
819
+ risks.push({
820
+ title: 'Antivírus pode elevar latência',
821
+ detail: 'Ativar varredura forçada melhora segurança, mas pode impactar throughput e tempo de resposta.',
822
+ });
823
+ }
824
+ if (this.bulkGroup.get('failFast')?.value === true) {
825
+ risks.push({
826
+ title: 'Lote para no primeiro erro',
827
+ detail: 'Usuários podem precisar reenviar itens válidos quando o primeiro erro interromper o restante do lote.',
828
+ });
829
+ }
830
+ if ((this.rateLimitGroup.get('maxAutoRetry')?.value ?? 0) >= 5) {
831
+ risks.push({
832
+ title: 'Retry automático agressivo',
833
+ detail: 'Muitas tentativas automáticas podem aumentar tempo de espera percebido e ruído operacional.',
834
+ });
835
+ }
836
+ if (this.uiGroup.get('manualUpload')?.value === true) {
837
+ risks.push({
838
+ title: 'Fluxo exige ação manual',
839
+ detail: 'Adequado para revisão, mas aumenta passos no envio e pode reduzir taxa de conclusão em cenários simples.',
840
+ });
841
+ }
842
+ return risks;
843
+ }
728
844
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisFilesUploadConfigEditor, deps: [{ token: i1$1.FormBuilder }, { token: SETTINGS_PANEL_DATA }, { token: i2.MatSnackBar }, { token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component });
729
845
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisFilesUploadConfigEditor, isStandalone: true, selector: "praxis-files-upload-config-editor", ngImport: i0, template: `
846
+ <div class="editor-layout">
847
+ <aside class="summary-panel" aria-label="Resumo executivo da configuração">
848
+ <h3>Resumo executivo</h3>
849
+ <p class="summary-intro">
850
+ Revise aqui o impacto operacional antes de percorrer todos os grupos
851
+ de configuração.
852
+ </p>
853
+
854
+ <div class="summary-grid">
855
+ <section class="summary-card">
856
+ <span class="eyebrow">Estratégia</span>
857
+ <strong>{{ summaryStrategyTitle }}</strong>
858
+ <p>{{ summaryStrategyDetail }}</p>
859
+ </section>
860
+
861
+ <section class="summary-card">
862
+ <span class="eyebrow">Experiência</span>
863
+ <strong>{{ summaryExperienceTitle }}</strong>
864
+ <p>{{ summaryExperienceDetail }}</p>
865
+ </section>
866
+
867
+ <section class="summary-card">
868
+ <span class="eyebrow">Limites</span>
869
+ <strong>{{ summaryLimitsTitle }}</strong>
870
+ <p>{{ summaryLimitsDetail }}</p>
871
+ </section>
872
+
873
+ <section class="summary-card">
874
+ <span class="eyebrow">Servidor</span>
875
+ <strong>{{ summaryServerTitle }}</strong>
876
+ <p>{{ summaryServerDetail }}</p>
877
+ </section>
878
+ </div>
879
+
880
+ <section class="risk-panel" [class.safe]="activeRisks.length === 0">
881
+ <h4>
882
+ <mat-icon aria-hidden="true">{{
883
+ activeRisks.length === 0 ? 'verified' : 'warning'
884
+ }}</mat-icon>
885
+ Riscos e atenção
886
+ </h4>
887
+ <div *ngIf="activeRisks.length > 0; else noRiskState" class="risk-list">
888
+ <article class="risk-item" *ngFor="let risk of activeRisks">
889
+ <strong>{{ risk.title }}</strong>
890
+ <p>{{ risk.detail }}</p>
891
+ </article>
892
+ </div>
893
+ <ng-template #noRiskState>
894
+ <p class="safe-copy">
895
+ Nenhum risco operacional evidente na configuração atual.
896
+ </p>
897
+ </ng-template>
898
+ </section>
899
+ </aside>
900
+
901
+ <div class="editor-main">
730
902
  <mat-tab-group>
731
903
  <mat-tab label="Comportamento">
904
+ <div class="tab-panel">
905
+ <section class="tab-intro">
906
+ <h3>Estratégia e cadência do envio</h3>
907
+ <p>
908
+ Defina aqui como o usuário inicia o upload e como o lote se
909
+ comporta em termos de paralelismo e retentativa.
910
+ </p>
911
+ </section>
912
+ <section class="config-section">
732
913
  <form [formGroup]="form">
733
914
  <mat-form-field appearance="fill">
734
915
  <mat-label>Estratégia de envio <span class="opt-tag">opcional</span></mat-label>
@@ -751,11 +932,18 @@ class PraxisFilesUploadConfigEditor {
751
932
  </button>
752
933
  </mat-form-field>
753
934
  </form>
935
+ </section>
936
+ <section class="config-section">
754
937
  <form [formGroup]="bulkGroup">
755
938
  <h4 class="section-subtitle">
756
939
  <mat-icon aria-hidden="true">build</mat-icon>
757
940
  Opções configuráveis — Lote
758
941
  </h4>
942
+ <p class="section-note">
943
+ Use paralelismo e retries com moderação. Em ambientes corporativos,
944
+ esses controles afetam throughput, experiência percebida e carga no
945
+ backend.
946
+ </p>
759
947
  <mat-form-field appearance="fill">
760
948
  <mat-label>Uploads paralelos <span class="opt-tag">opcional</span></mat-label>
761
949
  <input matInput type="number" formControlName="parallelUploads" />
@@ -799,13 +987,28 @@ class PraxisFilesUploadConfigEditor {
799
987
  </button>
800
988
  </mat-form-field>
801
989
  </form>
990
+ </section>
991
+ </div>
802
992
  </mat-tab>
803
993
  <mat-tab label="Interface">
994
+ <div class="tab-panel">
995
+ <section class="tab-intro">
996
+ <h3>Experiência visível para o usuário</h3>
997
+ <p>
998
+ Configure densidade, dropzone, metadados e detalhes da lista.
999
+ Priorize clareza operacional antes de habilitar recursos avançados.
1000
+ </p>
1001
+ </section>
1002
+ <section class="config-section">
804
1003
  <form [formGroup]="uiGroup">
805
1004
  <h4 class="section-subtitle">
806
1005
  <mat-icon aria-hidden="true">edit</mat-icon>
807
1006
  Opções configuráveis — Interface
808
1007
  </h4>
1008
+ <p class="section-note">
1009
+ Esta seção controla a ergonomia do componente. Mudanças aqui afetam
1010
+ descoberta, esforço de uso e taxa de conclusão do envio.
1011
+ </p>
809
1012
  <mat-checkbox formControlName="showDropzone"
810
1013
  >Exibir área de soltar</mat-checkbox
811
1014
  >
@@ -919,13 +1122,28 @@ class PraxisFilesUploadConfigEditor {
919
1122
  </mat-form-field>
920
1123
  </fieldset>
921
1124
  </form>
1125
+ </section>
1126
+ </div>
922
1127
  </mat-tab>
923
1128
  <mat-tab label="Validações">
1129
+ <div class="tab-panel">
1130
+ <section class="tab-intro">
1131
+ <h3>Regras de aceite e comportamento operacional</h3>
1132
+ <p>
1133
+ Separe mentalmente o que é validação de UX local do que é regra
1134
+ efetiva de backend. Revise com atenção as opções destrutivas.
1135
+ </p>
1136
+ </section>
1137
+ <section class="config-section">
924
1138
  <form [formGroup]="limitsGroup">
925
1139
  <h4 class="section-subtitle">
926
1140
  <mat-icon aria-hidden="true">build</mat-icon>
927
1141
  Opções configuráveis — Validações
928
1142
  </h4>
1143
+ <p class="section-note">
1144
+ Limites locais melhoram feedback imediato, mas não substituem a
1145
+ política efetiva do servidor.
1146
+ </p>
929
1147
  <mat-form-field appearance="fill">
930
1148
  <mat-label>Tamanho máximo do arquivo (bytes) <span class="opt-tag">opcional</span></mat-label>
931
1149
  <input matInput type="number" formControlName="maxFileSizeBytes" />
@@ -948,6 +1166,19 @@ class PraxisFilesUploadConfigEditor {
948
1166
  <mat-label>Tamanho máximo do lote (bytes) <span class="opt-tag">opcional</span></mat-label>
949
1167
  <input matInput type="number" formControlName="maxBulkSizeBytes" />
950
1168
  </mat-form-field>
1169
+ </form>
1170
+ </section>
1171
+ <section class="config-section">
1172
+ <form [formGroup]="optionsGroup">
1173
+ <h4 class="section-subtitle">
1174
+ <mat-icon aria-hidden="true">tune</mat-icon>
1175
+ Opções configuráveis — Backend
1176
+ </h4>
1177
+ <p class="section-note">
1178
+ Estas opções têm impacto direto no contrato com o servidor e em
1179
+ governança operacional. Trate alterações aqui como decisão de
1180
+ política, não apenas de interface.
1181
+ </p>
951
1182
  <mat-form-field appearance="fill">
952
1183
  <mat-label>Política de conflito (padrão) <span class="opt-tag">opcional</span></mat-label>
953
1184
  <mat-select formControlName="defaultConflictPolicy">
@@ -967,14 +1198,11 @@ class PraxisFilesUploadConfigEditor {
967
1198
  >
968
1199
  <mat-icon>help_outline</mat-icon>
969
1200
  </button>
970
- <div class="warn" *ngIf="limitsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE'">
1201
+ <div class="warn" *ngIf="optionsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE'">
971
1202
  <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
972
1203
  Atenção: OVERWRITE pode sobrescrever arquivos existentes.
973
1204
  </div>
974
1205
  </mat-form-field>
975
- <mat-checkbox formControlName="failFast"
976
- >Parar no primeiro erro (fail-fast)</mat-checkbox
977
- >
978
1206
  <mat-checkbox formControlName="strictValidation"
979
1207
  >Validação rigorosa (backend)</mat-checkbox
980
1208
  >
@@ -992,12 +1220,6 @@ class PraxisFilesUploadConfigEditor {
992
1220
  <mat-icon>help_outline</mat-icon>
993
1221
  </button>
994
1222
  </mat-form-field>
995
- </form>
996
- <form [formGroup]="optionsGroup">
997
- <h4 class="section-subtitle">
998
- <mat-icon aria-hidden="true">tune</mat-icon>
999
- Opções configuráveis — Avançado
1000
- </h4>
1001
1223
  <mat-form-field appearance="fill">
1002
1224
  <mat-label>Extensões permitidas <span class="opt-tag">opcional</span></mat-label>
1003
1225
  <input
@@ -1050,6 +1272,8 @@ class PraxisFilesUploadConfigEditor {
1050
1272
  Pode impactar desempenho e latência de upload.
1051
1273
  </div>
1052
1274
  </form>
1275
+ </section>
1276
+ <section class="config-section">
1053
1277
  <form [formGroup]="quotasGroup">
1054
1278
  <h4 class="section-subtitle">
1055
1279
  <mat-icon aria-hidden="true">edit</mat-icon>
@@ -1062,11 +1286,17 @@ class PraxisFilesUploadConfigEditor {
1062
1286
  >Bloquear ao exceder cota</mat-checkbox
1063
1287
  >
1064
1288
  </form>
1289
+ </section>
1290
+ <section class="config-section">
1065
1291
  <form [formGroup]="rateLimitGroup">
1066
1292
  <h4 class="section-subtitle">
1067
1293
  <mat-icon aria-hidden="true">edit</mat-icon>
1068
1294
  Opções configuráveis — Rate Limit (UI)
1069
1295
  </h4>
1296
+ <p class="section-note">
1297
+ Prefira poucos retries automáticos. Mais tentativas aliviam atrito
1298
+ imediato, mas podem mascarar gargalos reais do ambiente.
1299
+ </p>
1070
1300
  <mat-checkbox formControlName="showBannerOn429"
1071
1301
  >Exibir banner quando atingir o limite</mat-checkbox
1072
1302
  >
@@ -1082,13 +1312,40 @@ class PraxisFilesUploadConfigEditor {
1082
1312
  <input matInput type="number" formControlName="baseBackoffMs" />
1083
1313
  </mat-form-field>
1084
1314
  </form>
1315
+ </section>
1316
+ <section class="config-section">
1317
+ <form [formGroup]="bulkGroup">
1318
+ <h4 class="section-subtitle">
1319
+ <mat-icon aria-hidden="true">bolt</mat-icon>
1320
+ Opções configuráveis — Execução do lote
1321
+ </h4>
1322
+ <mat-checkbox formControlName="failFast"
1323
+ >Parar no primeiro erro (fail-fast)</mat-checkbox
1324
+ >
1325
+ </form>
1326
+ </section>
1327
+ </div>
1085
1328
  </mat-tab>
1086
1329
  <mat-tab label="Mensagens">
1330
+ <div class="tab-panel">
1331
+ <section class="tab-intro">
1332
+ <h3>Mensagens orientadas à operação</h3>
1333
+ <p>
1334
+ Ajuste o texto exibido ao usuário final. Priorize clareza,
1335
+ consistência e linguagem acionável.
1336
+ </p>
1337
+ </section>
1338
+ <section class="config-section">
1087
1339
  <form [formGroup]="messagesGroup">
1088
1340
  <h4 class="section-subtitle">
1089
1341
  <mat-icon aria-hidden="true">edit</mat-icon>
1090
1342
  Opções configuráveis — Mensagens (UI)
1091
1343
  </h4>
1344
+ <p class="section-note">
1345
+ Mensagens curtas e objetivas funcionam melhor em contextos de alta
1346
+ frequência. Use este grupo para adequar a voz da interface ao seu
1347
+ ambiente.
1348
+ </p>
1092
1349
  <mat-form-field appearance="fill">
1093
1350
  <mat-label>Sucesso (individual) <span class="opt-tag">opcional</span></mat-label>
1094
1351
  <input
@@ -1115,8 +1372,19 @@ class PraxisFilesUploadConfigEditor {
1115
1372
  </ng-container>
1116
1373
  </div>
1117
1374
  </form>
1375
+ </section>
1376
+ </div>
1118
1377
  </mat-tab>
1119
1378
  <mat-tab label="Cabeçalhos">
1379
+ <div class="tab-panel">
1380
+ <section class="tab-intro">
1381
+ <h3>Contexto de tenant e usuário</h3>
1382
+ <p>
1383
+ Use cabeçalhos para consultar configuração efetiva do servidor no
1384
+ mesmo contexto que o componente usará em produção.
1385
+ </p>
1386
+ </section>
1387
+ <section class="config-section">
1120
1388
  <form [formGroup]="headersGroup">
1121
1389
  <h4 class="section-subtitle">
1122
1390
  <mat-icon aria-hidden="true">edit</mat-icon>
@@ -1151,8 +1419,19 @@ class PraxisFilesUploadConfigEditor {
1151
1419
  <input matInput formControlName="userValue" placeholder="ex.: 42" />
1152
1420
  </mat-form-field>
1153
1421
  </form>
1422
+ </section>
1423
+ </div>
1154
1424
  </mat-tab>
1155
1425
  <mat-tab label="Servidor">
1426
+ <div class="tab-panel">
1427
+ <section class="tab-intro">
1428
+ <h3>Contrato efetivo retornado pelo backend</h3>
1429
+ <p>
1430
+ Esta aba é a fonte de verdade do ambiente ativo. Compare o resumo
1431
+ do servidor com o que foi configurado nos formulários anteriores.
1432
+ </p>
1433
+ </section>
1434
+ <section class="config-section">
1156
1435
  <div class="server-tab">
1157
1436
  <div class="toolbar">
1158
1437
  <h4 class="section-subtitle ro">
@@ -1244,17 +1523,36 @@ class PraxisFilesUploadConfigEditor {
1244
1523
  </ng-template>
1245
1524
  </ng-template>
1246
1525
  </div>
1526
+ </section>
1527
+ </div>
1247
1528
  </mat-tab>
1248
1529
  <mat-tab label="JSON">
1530
+ <div class="tab-panel">
1531
+ <section class="tab-intro">
1532
+ <h3>Modo avançado</h3>
1533
+ <p>
1534
+ Edite o payload bruto apenas quando precisar de controle fino.
1535
+ Alterações aqui exigem mais rigor de revisão e validação.
1536
+ </p>
1537
+ </section>
1538
+ <div class="advanced-note">
1539
+ O JSON é uma visão de baixo nível da configuração. Prefira as abas
1540
+ guiadas sempre que possível para reduzir erro humano e manter a
1541
+ configuração auditável.
1542
+ </div>
1249
1543
  <textarea
1544
+ class="json-textarea"
1250
1545
  rows="10"
1251
1546
  [ngModel]="form.value | json"
1252
1547
  (ngModelChange)="onJsonChange($event)"
1253
1548
  ></textarea>
1254
1549
  <div class="error" *ngIf="jsonError">{{ jsonError }}</div>
1550
+ </div>
1255
1551
  </mat-tab>
1256
1552
  </mat-tab-group>
1257
- `, isInline: true, styles: [".help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i7.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i7.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i4.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i4.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i7$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i10.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i7.JsonPipe, name: "json" }] });
1553
+ </div>
1554
+ </div>
1555
+ `, isInline: true, styles: [".editor-layout{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);gap:16px;align-items:start}.summary-panel{position:sticky;top:0;display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--pfx-surface-border, #d8d8d8);border-radius:16px;background:linear-gradient(180deg,#fafbfc,#fff)}.summary-panel h3{margin:0;font-size:1rem;font-weight:600}.summary-panel .summary-intro{margin:0;color:#000000a6;line-height:1.4;font-size:.88rem}.summary-grid{display:grid;gap:10px}.summary-card{padding:12px;border-radius:12px;background:#fff;border:1px solid #e7e9ee}.summary-card .eyebrow{display:block;margin-bottom:4px;font-size:.72rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#5f6b7a}.summary-card strong{display:block;margin-bottom:4px;font-size:.95rem}.summary-card p{margin:0;font-size:.85rem;line-height:1.45;color:#000000b8}.risk-panel{padding:12px;border-radius:12px;background:#fff8e8;border:1px solid #f0d29a}.risk-panel.safe{background:#f4fbf6;border-color:#b8ddc1}.risk-panel h4{display:flex;align-items:center;gap:6px;margin:0 0 8px;font-size:.92rem}.risk-list{display:grid;gap:8px}.risk-item{padding:10px;border-radius:10px;background:#ffffffb8}.risk-item strong{display:block;margin-bottom:2px;font-size:.84rem}.risk-item p,.risk-panel .safe-copy{margin:0;font-size:.82rem;line-height:1.4;color:#000000b8}.editor-main{min-width:0}.tab-panel{display:grid;gap:16px;padding-top:8px}.tab-intro{padding:16px 18px;border:1px solid #e5e8ef;border-radius:16px;background:linear-gradient(180deg,#fff,#f7f9fc)}.tab-intro h3{margin:0 0 6px;font-size:1rem;font-weight:600}.tab-intro p{margin:0;font-size:.9rem;line-height:1.5;color:#000000b8}.config-section{padding:18px;border:1px solid #e5e8ef;border-radius:16px;background:#fff}.config-section+.config-section{margin-top:0}.section-note{margin:0 0 16px;color:#000000b3;font-size:.88rem;line-height:1.45}.advanced-note{padding:14px 16px;border-radius:14px;border:1px solid #f0d29a;background:#fff8e8;color:#000000c7;font-size:.88rem;line-height:1.45}.json-textarea{width:100%;min-height:280px;padding:14px 16px;border-radius:14px;border:1px solid #d7dce5;background:#0f172a;color:#e5eefc;font:500 .84rem/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;resize:vertical}.server-tab{display:grid;gap:16px}.toolbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.summary{display:grid;gap:8px;padding-left:18px}.summary li{line-height:1.45}details{margin-top:12px;padding:14px 16px;border:1px solid #e5e8ef;border-radius:14px;background:#fbfcfe}details pre{white-space:pre-wrap;word-break:break-word}@media(max-width:1100px){.editor-layout{grid-template-columns:1fr}.summary-panel{position:static}}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i9.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i9.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i4.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i4.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i7.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i11.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i10.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i9.JsonPipe, name: "json" }] });
1258
1556
  }
1259
1557
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisFilesUploadConfigEditor, decorators: [{
1260
1558
  type: Component,
@@ -1272,8 +1570,73 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1272
1570
  MatSnackBarModule,
1273
1571
  FormsModule,
1274
1572
  ], template: `
1573
+ <div class="editor-layout">
1574
+ <aside class="summary-panel" aria-label="Resumo executivo da configuração">
1575
+ <h3>Resumo executivo</h3>
1576
+ <p class="summary-intro">
1577
+ Revise aqui o impacto operacional antes de percorrer todos os grupos
1578
+ de configuração.
1579
+ </p>
1580
+
1581
+ <div class="summary-grid">
1582
+ <section class="summary-card">
1583
+ <span class="eyebrow">Estratégia</span>
1584
+ <strong>{{ summaryStrategyTitle }}</strong>
1585
+ <p>{{ summaryStrategyDetail }}</p>
1586
+ </section>
1587
+
1588
+ <section class="summary-card">
1589
+ <span class="eyebrow">Experiência</span>
1590
+ <strong>{{ summaryExperienceTitle }}</strong>
1591
+ <p>{{ summaryExperienceDetail }}</p>
1592
+ </section>
1593
+
1594
+ <section class="summary-card">
1595
+ <span class="eyebrow">Limites</span>
1596
+ <strong>{{ summaryLimitsTitle }}</strong>
1597
+ <p>{{ summaryLimitsDetail }}</p>
1598
+ </section>
1599
+
1600
+ <section class="summary-card">
1601
+ <span class="eyebrow">Servidor</span>
1602
+ <strong>{{ summaryServerTitle }}</strong>
1603
+ <p>{{ summaryServerDetail }}</p>
1604
+ </section>
1605
+ </div>
1606
+
1607
+ <section class="risk-panel" [class.safe]="activeRisks.length === 0">
1608
+ <h4>
1609
+ <mat-icon aria-hidden="true">{{
1610
+ activeRisks.length === 0 ? 'verified' : 'warning'
1611
+ }}</mat-icon>
1612
+ Riscos e atenção
1613
+ </h4>
1614
+ <div *ngIf="activeRisks.length > 0; else noRiskState" class="risk-list">
1615
+ <article class="risk-item" *ngFor="let risk of activeRisks">
1616
+ <strong>{{ risk.title }}</strong>
1617
+ <p>{{ risk.detail }}</p>
1618
+ </article>
1619
+ </div>
1620
+ <ng-template #noRiskState>
1621
+ <p class="safe-copy">
1622
+ Nenhum risco operacional evidente na configuração atual.
1623
+ </p>
1624
+ </ng-template>
1625
+ </section>
1626
+ </aside>
1627
+
1628
+ <div class="editor-main">
1275
1629
  <mat-tab-group>
1276
1630
  <mat-tab label="Comportamento">
1631
+ <div class="tab-panel">
1632
+ <section class="tab-intro">
1633
+ <h3>Estratégia e cadência do envio</h3>
1634
+ <p>
1635
+ Defina aqui como o usuário inicia o upload e como o lote se
1636
+ comporta em termos de paralelismo e retentativa.
1637
+ </p>
1638
+ </section>
1639
+ <section class="config-section">
1277
1640
  <form [formGroup]="form">
1278
1641
  <mat-form-field appearance="fill">
1279
1642
  <mat-label>Estratégia de envio <span class="opt-tag">opcional</span></mat-label>
@@ -1296,11 +1659,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1296
1659
  </button>
1297
1660
  </mat-form-field>
1298
1661
  </form>
1662
+ </section>
1663
+ <section class="config-section">
1299
1664
  <form [formGroup]="bulkGroup">
1300
1665
  <h4 class="section-subtitle">
1301
1666
  <mat-icon aria-hidden="true">build</mat-icon>
1302
1667
  Opções configuráveis — Lote
1303
1668
  </h4>
1669
+ <p class="section-note">
1670
+ Use paralelismo e retries com moderação. Em ambientes corporativos,
1671
+ esses controles afetam throughput, experiência percebida e carga no
1672
+ backend.
1673
+ </p>
1304
1674
  <mat-form-field appearance="fill">
1305
1675
  <mat-label>Uploads paralelos <span class="opt-tag">opcional</span></mat-label>
1306
1676
  <input matInput type="number" formControlName="parallelUploads" />
@@ -1344,13 +1714,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1344
1714
  </button>
1345
1715
  </mat-form-field>
1346
1716
  </form>
1717
+ </section>
1718
+ </div>
1347
1719
  </mat-tab>
1348
1720
  <mat-tab label="Interface">
1721
+ <div class="tab-panel">
1722
+ <section class="tab-intro">
1723
+ <h3>Experiência visível para o usuário</h3>
1724
+ <p>
1725
+ Configure densidade, dropzone, metadados e detalhes da lista.
1726
+ Priorize clareza operacional antes de habilitar recursos avançados.
1727
+ </p>
1728
+ </section>
1729
+ <section class="config-section">
1349
1730
  <form [formGroup]="uiGroup">
1350
1731
  <h4 class="section-subtitle">
1351
1732
  <mat-icon aria-hidden="true">edit</mat-icon>
1352
1733
  Opções configuráveis — Interface
1353
1734
  </h4>
1735
+ <p class="section-note">
1736
+ Esta seção controla a ergonomia do componente. Mudanças aqui afetam
1737
+ descoberta, esforço de uso e taxa de conclusão do envio.
1738
+ </p>
1354
1739
  <mat-checkbox formControlName="showDropzone"
1355
1740
  >Exibir área de soltar</mat-checkbox
1356
1741
  >
@@ -1464,13 +1849,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1464
1849
  </mat-form-field>
1465
1850
  </fieldset>
1466
1851
  </form>
1852
+ </section>
1853
+ </div>
1467
1854
  </mat-tab>
1468
1855
  <mat-tab label="Validações">
1856
+ <div class="tab-panel">
1857
+ <section class="tab-intro">
1858
+ <h3>Regras de aceite e comportamento operacional</h3>
1859
+ <p>
1860
+ Separe mentalmente o que é validação de UX local do que é regra
1861
+ efetiva de backend. Revise com atenção as opções destrutivas.
1862
+ </p>
1863
+ </section>
1864
+ <section class="config-section">
1469
1865
  <form [formGroup]="limitsGroup">
1470
1866
  <h4 class="section-subtitle">
1471
1867
  <mat-icon aria-hidden="true">build</mat-icon>
1472
1868
  Opções configuráveis — Validações
1473
1869
  </h4>
1870
+ <p class="section-note">
1871
+ Limites locais melhoram feedback imediato, mas não substituem a
1872
+ política efetiva do servidor.
1873
+ </p>
1474
1874
  <mat-form-field appearance="fill">
1475
1875
  <mat-label>Tamanho máximo do arquivo (bytes) <span class="opt-tag">opcional</span></mat-label>
1476
1876
  <input matInput type="number" formControlName="maxFileSizeBytes" />
@@ -1493,6 +1893,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1493
1893
  <mat-label>Tamanho máximo do lote (bytes) <span class="opt-tag">opcional</span></mat-label>
1494
1894
  <input matInput type="number" formControlName="maxBulkSizeBytes" />
1495
1895
  </mat-form-field>
1896
+ </form>
1897
+ </section>
1898
+ <section class="config-section">
1899
+ <form [formGroup]="optionsGroup">
1900
+ <h4 class="section-subtitle">
1901
+ <mat-icon aria-hidden="true">tune</mat-icon>
1902
+ Opções configuráveis — Backend
1903
+ </h4>
1904
+ <p class="section-note">
1905
+ Estas opções têm impacto direto no contrato com o servidor e em
1906
+ governança operacional. Trate alterações aqui como decisão de
1907
+ política, não apenas de interface.
1908
+ </p>
1496
1909
  <mat-form-field appearance="fill">
1497
1910
  <mat-label>Política de conflito (padrão) <span class="opt-tag">opcional</span></mat-label>
1498
1911
  <mat-select formControlName="defaultConflictPolicy">
@@ -1512,14 +1925,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1512
1925
  >
1513
1926
  <mat-icon>help_outline</mat-icon>
1514
1927
  </button>
1515
- <div class="warn" *ngIf="limitsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE'">
1928
+ <div class="warn" *ngIf="optionsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE'">
1516
1929
  <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
1517
1930
  Atenção: OVERWRITE pode sobrescrever arquivos existentes.
1518
1931
  </div>
1519
1932
  </mat-form-field>
1520
- <mat-checkbox formControlName="failFast"
1521
- >Parar no primeiro erro (fail-fast)</mat-checkbox
1522
- >
1523
1933
  <mat-checkbox formControlName="strictValidation"
1524
1934
  >Validação rigorosa (backend)</mat-checkbox
1525
1935
  >
@@ -1537,12 +1947,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1537
1947
  <mat-icon>help_outline</mat-icon>
1538
1948
  </button>
1539
1949
  </mat-form-field>
1540
- </form>
1541
- <form [formGroup]="optionsGroup">
1542
- <h4 class="section-subtitle">
1543
- <mat-icon aria-hidden="true">tune</mat-icon>
1544
- Opções configuráveis — Avançado
1545
- </h4>
1546
1950
  <mat-form-field appearance="fill">
1547
1951
  <mat-label>Extensões permitidas <span class="opt-tag">opcional</span></mat-label>
1548
1952
  <input
@@ -1595,6 +1999,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1595
1999
  Pode impactar desempenho e latência de upload.
1596
2000
  </div>
1597
2001
  </form>
2002
+ </section>
2003
+ <section class="config-section">
1598
2004
  <form [formGroup]="quotasGroup">
1599
2005
  <h4 class="section-subtitle">
1600
2006
  <mat-icon aria-hidden="true">edit</mat-icon>
@@ -1607,11 +2013,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1607
2013
  >Bloquear ao exceder cota</mat-checkbox
1608
2014
  >
1609
2015
  </form>
2016
+ </section>
2017
+ <section class="config-section">
1610
2018
  <form [formGroup]="rateLimitGroup">
1611
2019
  <h4 class="section-subtitle">
1612
2020
  <mat-icon aria-hidden="true">edit</mat-icon>
1613
2021
  Opções configuráveis — Rate Limit (UI)
1614
2022
  </h4>
2023
+ <p class="section-note">
2024
+ Prefira poucos retries automáticos. Mais tentativas aliviam atrito
2025
+ imediato, mas podem mascarar gargalos reais do ambiente.
2026
+ </p>
1615
2027
  <mat-checkbox formControlName="showBannerOn429"
1616
2028
  >Exibir banner quando atingir o limite</mat-checkbox
1617
2029
  >
@@ -1627,13 +2039,40 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1627
2039
  <input matInput type="number" formControlName="baseBackoffMs" />
1628
2040
  </mat-form-field>
1629
2041
  </form>
2042
+ </section>
2043
+ <section class="config-section">
2044
+ <form [formGroup]="bulkGroup">
2045
+ <h4 class="section-subtitle">
2046
+ <mat-icon aria-hidden="true">bolt</mat-icon>
2047
+ Opções configuráveis — Execução do lote
2048
+ </h4>
2049
+ <mat-checkbox formControlName="failFast"
2050
+ >Parar no primeiro erro (fail-fast)</mat-checkbox
2051
+ >
2052
+ </form>
2053
+ </section>
2054
+ </div>
1630
2055
  </mat-tab>
1631
2056
  <mat-tab label="Mensagens">
2057
+ <div class="tab-panel">
2058
+ <section class="tab-intro">
2059
+ <h3>Mensagens orientadas à operação</h3>
2060
+ <p>
2061
+ Ajuste o texto exibido ao usuário final. Priorize clareza,
2062
+ consistência e linguagem acionável.
2063
+ </p>
2064
+ </section>
2065
+ <section class="config-section">
1632
2066
  <form [formGroup]="messagesGroup">
1633
2067
  <h4 class="section-subtitle">
1634
2068
  <mat-icon aria-hidden="true">edit</mat-icon>
1635
2069
  Opções configuráveis — Mensagens (UI)
1636
2070
  </h4>
2071
+ <p class="section-note">
2072
+ Mensagens curtas e objetivas funcionam melhor em contextos de alta
2073
+ frequência. Use este grupo para adequar a voz da interface ao seu
2074
+ ambiente.
2075
+ </p>
1637
2076
  <mat-form-field appearance="fill">
1638
2077
  <mat-label>Sucesso (individual) <span class="opt-tag">opcional</span></mat-label>
1639
2078
  <input
@@ -1660,8 +2099,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1660
2099
  </ng-container>
1661
2100
  </div>
1662
2101
  </form>
2102
+ </section>
2103
+ </div>
1663
2104
  </mat-tab>
1664
2105
  <mat-tab label="Cabeçalhos">
2106
+ <div class="tab-panel">
2107
+ <section class="tab-intro">
2108
+ <h3>Contexto de tenant e usuário</h3>
2109
+ <p>
2110
+ Use cabeçalhos para consultar configuração efetiva do servidor no
2111
+ mesmo contexto que o componente usará em produção.
2112
+ </p>
2113
+ </section>
2114
+ <section class="config-section">
1665
2115
  <form [formGroup]="headersGroup">
1666
2116
  <h4 class="section-subtitle">
1667
2117
  <mat-icon aria-hidden="true">edit</mat-icon>
@@ -1696,8 +2146,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1696
2146
  <input matInput formControlName="userValue" placeholder="ex.: 42" />
1697
2147
  </mat-form-field>
1698
2148
  </form>
2149
+ </section>
2150
+ </div>
1699
2151
  </mat-tab>
1700
2152
  <mat-tab label="Servidor">
2153
+ <div class="tab-panel">
2154
+ <section class="tab-intro">
2155
+ <h3>Contrato efetivo retornado pelo backend</h3>
2156
+ <p>
2157
+ Esta aba é a fonte de verdade do ambiente ativo. Compare o resumo
2158
+ do servidor com o que foi configurado nos formulários anteriores.
2159
+ </p>
2160
+ </section>
2161
+ <section class="config-section">
1701
2162
  <div class="server-tab">
1702
2163
  <div class="toolbar">
1703
2164
  <h4 class="section-subtitle ro">
@@ -1789,22 +2250,272 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1789
2250
  </ng-template>
1790
2251
  </ng-template>
1791
2252
  </div>
2253
+ </section>
2254
+ </div>
1792
2255
  </mat-tab>
1793
2256
  <mat-tab label="JSON">
2257
+ <div class="tab-panel">
2258
+ <section class="tab-intro">
2259
+ <h3>Modo avançado</h3>
2260
+ <p>
2261
+ Edite o payload bruto apenas quando precisar de controle fino.
2262
+ Alterações aqui exigem mais rigor de revisão e validação.
2263
+ </p>
2264
+ </section>
2265
+ <div class="advanced-note">
2266
+ O JSON é uma visão de baixo nível da configuração. Prefira as abas
2267
+ guiadas sempre que possível para reduzir erro humano e manter a
2268
+ configuração auditável.
2269
+ </div>
1794
2270
  <textarea
2271
+ class="json-textarea"
1795
2272
  rows="10"
1796
2273
  [ngModel]="form.value | json"
1797
2274
  (ngModelChange)="onJsonChange($event)"
1798
2275
  ></textarea>
1799
2276
  <div class="error" *ngIf="jsonError">{{ jsonError }}</div>
2277
+ </div>
1800
2278
  </mat-tab>
1801
2279
  </mat-tab-group>
1802
- `, styles: [".help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"] }]
2280
+ </div>
2281
+ </div>
2282
+ `, styles: [".editor-layout{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);gap:16px;align-items:start}.summary-panel{position:sticky;top:0;display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--pfx-surface-border, #d8d8d8);border-radius:16px;background:linear-gradient(180deg,#fafbfc,#fff)}.summary-panel h3{margin:0;font-size:1rem;font-weight:600}.summary-panel .summary-intro{margin:0;color:#000000a6;line-height:1.4;font-size:.88rem}.summary-grid{display:grid;gap:10px}.summary-card{padding:12px;border-radius:12px;background:#fff;border:1px solid #e7e9ee}.summary-card .eyebrow{display:block;margin-bottom:4px;font-size:.72rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#5f6b7a}.summary-card strong{display:block;margin-bottom:4px;font-size:.95rem}.summary-card p{margin:0;font-size:.85rem;line-height:1.45;color:#000000b8}.risk-panel{padding:12px;border-radius:12px;background:#fff8e8;border:1px solid #f0d29a}.risk-panel.safe{background:#f4fbf6;border-color:#b8ddc1}.risk-panel h4{display:flex;align-items:center;gap:6px;margin:0 0 8px;font-size:.92rem}.risk-list{display:grid;gap:8px}.risk-item{padding:10px;border-radius:10px;background:#ffffffb8}.risk-item strong{display:block;margin-bottom:2px;font-size:.84rem}.risk-item p,.risk-panel .safe-copy{margin:0;font-size:.82rem;line-height:1.4;color:#000000b8}.editor-main{min-width:0}.tab-panel{display:grid;gap:16px;padding-top:8px}.tab-intro{padding:16px 18px;border:1px solid #e5e8ef;border-radius:16px;background:linear-gradient(180deg,#fff,#f7f9fc)}.tab-intro h3{margin:0 0 6px;font-size:1rem;font-weight:600}.tab-intro p{margin:0;font-size:.9rem;line-height:1.5;color:#000000b8}.config-section{padding:18px;border:1px solid #e5e8ef;border-radius:16px;background:#fff}.config-section+.config-section{margin-top:0}.section-note{margin:0 0 16px;color:#000000b3;font-size:.88rem;line-height:1.45}.advanced-note{padding:14px 16px;border-radius:14px;border:1px solid #f0d29a;background:#fff8e8;color:#000000c7;font-size:.88rem;line-height:1.45}.json-textarea{width:100%;min-height:280px;padding:14px 16px;border-radius:14px;border:1px solid #d7dce5;background:#0f172a;color:#e5eefc;font:500 .84rem/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;resize:vertical}.server-tab{display:grid;gap:16px}.toolbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.summary{display:grid;gap:8px;padding-left:18px}.summary li{line-height:1.45}details{margin-top:12px;padding:14px 16px;border:1px solid #e5e8ef;border-radius:14px;background:#fbfcfe}details pre{white-space:pre-wrap;word-break:break-word}@media(max-width:1100px){.editor-layout{grid-template-columns:1fr}.summary-panel{position:static}}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"] }]
1803
2283
  }], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: undefined, decorators: [{
1804
2284
  type: Inject,
1805
2285
  args: [SETTINGS_PANEL_DATA]
1806
2286
  }] }, { type: i2.MatSnackBar }, { type: i0.DestroyRef }] });
1807
2287
 
2288
+ const FILES_UPLOAD_EN_US = {
2289
+ 'praxis.filesUpload.settingsAriaLabel': 'Open settings',
2290
+ 'praxis.filesUpload.dropzoneLabel': 'Drag files or',
2291
+ 'praxis.filesUpload.dropzoneButton': 'select',
2292
+ 'praxis.filesUpload.conflictPolicyLabel': 'Conflict policy',
2293
+ 'praxis.filesUpload.metadataLabel': 'Metadata (JSON)',
2294
+ 'praxis.filesUpload.progressAriaLabel': 'Upload progress',
2295
+ 'praxis.filesUpload.rateLimitBanner': 'Rate limit exceeded. Try again at',
2296
+ 'praxis.filesUpload.settingsTitle': 'Files Upload Configuration',
2297
+ 'praxis.filesUpload.statusSuccess': 'Uploaded',
2298
+ 'praxis.filesUpload.statusError': 'Error',
2299
+ 'praxis.filesUpload.invalidMetadata': 'Invalid metadata.',
2300
+ 'praxis.filesUpload.genericUploadError': 'File upload error.',
2301
+ 'praxis.filesUpload.acceptError': 'File type not allowed.',
2302
+ 'praxis.filesUpload.maxFileSizeError': 'File exceeds the maximum size.',
2303
+ 'praxis.filesUpload.maxFilesPerBulkError': 'Too many files selected.',
2304
+ 'praxis.filesUpload.maxBulkSizeError': 'Total files size exceeded.',
2305
+ 'praxis.filesUpload.serviceUnavailable': 'Upload service unavailable.',
2306
+ 'praxis.filesUpload.baseUrlMissing': 'Base URL not configured. Provide [baseUrl] to enable uploads.',
2307
+ 'praxis.filesUpload.selectFiles': 'Select file(s)',
2308
+ 'praxis.filesUpload.remove': 'Remove',
2309
+ 'praxis.filesUpload.moreActions': 'More actions',
2310
+ 'praxis.filesUpload.policySummaryAction': 'Information about upload policies',
2311
+ 'praxis.filesUpload.retry': 'Retry',
2312
+ 'praxis.filesUpload.download': 'Download',
2313
+ 'praxis.filesUpload.copyLink': 'Copy link',
2314
+ 'praxis.filesUpload.details': 'Details',
2315
+ 'praxis.filesUpload.detailsMetadata': 'Metadata',
2316
+ 'praxis.filesUpload.dropzoneHint': 'Drag and drop files here or click to select',
2317
+ 'praxis.filesUpload.dropzoneProximityHint': 'Drop here to upload',
2318
+ 'praxis.filesUpload.placeholder': 'Select or drop files…',
2319
+ 'praxis.filesUpload.partialUploadError': 'Partial upload completed with failures in part of the batch.',
2320
+ 'praxis.filesUpload.presignMetadataMissing': 'Presigned upload completed without file metadata.',
2321
+ 'praxis.filesUpload.fieldChipAria': 'Additional files selected',
2322
+ 'praxis.filesUpload.fieldPendingCountSuffix': 'file(s) selected',
2323
+ 'praxis.filesUpload.removePendingFile': 'Remove selected file',
2324
+ 'praxis.filesUpload.fieldPolicyTypes': 'Types',
2325
+ 'praxis.filesUpload.fieldPolicyMaxPerFile': 'Max/file',
2326
+ 'praxis.filesUpload.fieldPolicyMaxItems': 'Max/items',
2327
+ 'praxis.filesUpload.fieldPolicyNotConfigured': 'Not configured',
2328
+ 'praxis.filesUpload.sizeUnitBytes': 'Bytes',
2329
+ 'praxis.filesUpload.sizeUnitKB': 'KB',
2330
+ 'praxis.filesUpload.sizeUnitMB': 'MB',
2331
+ 'praxis.filesUpload.sizeUnitGB': 'GB',
2332
+ 'praxis.filesUpload.sizeUnitTB': 'TB',
2333
+ 'praxis.filesUpload.errors.INVALID_FILE_TYPE': 'Invalid file type.',
2334
+ 'praxis.filesUpload.errors.FILE_TOO_LARGE': 'File is too large.',
2335
+ 'praxis.filesUpload.errors.NOT_FOUND': 'File not found.',
2336
+ 'praxis.filesUpload.errors.UNAUTHORIZED': 'Unauthorized request.',
2337
+ 'praxis.filesUpload.errors.RATE_LIMIT_EXCEEDED': 'Request limit exceeded.',
2338
+ 'praxis.filesUpload.errors.INTERNAL_ERROR': 'Internal server error.',
2339
+ 'praxis.filesUpload.errors.QUOTA_EXCEEDED': 'Quota exceeded.',
2340
+ 'praxis.filesUpload.errors.SEC_VIRUS_DETECTED': 'Virus detected in file.',
2341
+ 'praxis.filesUpload.errors.SEC_MALICIOUS_CONTENT': 'Malicious content detected.',
2342
+ 'praxis.filesUpload.errors.SEC_DANGEROUS_TYPE': 'Dangerous file type.',
2343
+ 'praxis.filesUpload.errors.FMT_MAGIC_MISMATCH': 'File content does not match the declared format.',
2344
+ 'praxis.filesUpload.errors.FMT_CORRUPTED': 'Corrupted file.',
2345
+ 'praxis.filesUpload.errors.FMT_UNSUPPORTED': 'Unsupported file format.',
2346
+ 'praxis.filesUpload.errors.SYS_STORAGE_ERROR': 'Storage error.',
2347
+ 'praxis.filesUpload.errors.SYS_SERVICE_DOWN': 'Service unavailable.',
2348
+ 'praxis.filesUpload.errors.SYS_RATE_LIMIT': 'Request limit exceeded.',
2349
+ 'praxis.filesUpload.errors.ARQUIVO_MUITO_GRANDE': 'File is too large.',
2350
+ 'praxis.filesUpload.errors.TIPO_ARQUIVO_INVALIDO': 'Invalid file type.',
2351
+ 'praxis.filesUpload.errors.TIPO_MIDIA_NAO_SUPORTADO': 'Unsupported media type.',
2352
+ 'praxis.filesUpload.errors.CAMPO_OBRIGATORIO_AUSENTE': 'Required field is missing.',
2353
+ 'praxis.filesUpload.errors.OPCOES_JSON_INVALIDAS': 'Invalid JSON options.',
2354
+ 'praxis.filesUpload.errors.ARGUMENTO_INVALIDO': 'Invalid argument.',
2355
+ 'praxis.filesUpload.errors.NAO_AUTORIZADO': 'Authentication required.',
2356
+ 'praxis.filesUpload.errors.ERRO_INTERNO': 'Internal server error.',
2357
+ 'praxis.filesUpload.errors.LIMITE_TAXA_EXCEDIDO': 'Rate limit exceeded.',
2358
+ 'praxis.filesUpload.errors.COTA_EXCEDIDA': 'Quota exceeded.',
2359
+ 'praxis.filesUpload.errors.ARQUIVO_JA_EXISTE': 'File already exists.',
2360
+ 'praxis.filesUpload.errors.INVALID_JSON_OPTIONS': 'Invalid JSON options.',
2361
+ 'praxis.filesUpload.errors.EMPTY_FILENAME': 'File name is required.',
2362
+ 'praxis.filesUpload.errors.FILE_EXISTS': 'File already exists.',
2363
+ 'praxis.filesUpload.errors.PATH_TRAVERSAL': 'Path traversal detected.',
2364
+ 'praxis.filesUpload.errors.INSUFFICIENT_STORAGE': 'Insufficient storage.',
2365
+ 'praxis.filesUpload.errors.UPLOAD_TIMEOUT': 'Upload timed out.',
2366
+ 'praxis.filesUpload.errors.BULK_UPLOAD_TIMEOUT': 'Bulk upload timed out.',
2367
+ 'praxis.filesUpload.errors.BULK_UPLOAD_CANCELLED': 'Bulk upload cancelled.',
2368
+ 'praxis.filesUpload.errors.USER_CANCELLED': 'Upload cancelled by user.',
2369
+ 'praxis.filesUpload.errors.UNKNOWN_ERROR': 'Unknown error.',
2370
+ 'praxis.filesUpload.fieldPendingSummary': 'Files ready to upload',
2371
+ 'praxis.filesUpload.fieldSelectedSummary': 'Selected file',
2372
+ 'praxis.filesUpload.fieldSelectedPlural': 'files attached',
2373
+ 'praxis.filesUpload.fieldEmptySummary': 'No file selected',
2374
+ 'praxis.filesUpload.fieldErrorSummary': 'Fix the error to continue',
2375
+ 'praxis.filesUpload.fieldActionUpload': 'Upload selected files',
2376
+ 'praxis.filesUpload.fieldActionUploadShort': 'Upload',
2377
+ 'praxis.filesUpload.fieldActionCancel': 'Cancel current selection',
2378
+ 'praxis.filesUpload.fieldActionCancelShort': 'Cancel',
2379
+ 'praxis.filesUpload.fieldActionSelect': 'Select file(s)',
2380
+ 'praxis.filesUpload.fieldActionClear': 'Clear',
2381
+ 'praxis.filesUpload.fieldInfoAction': 'Information',
2382
+ 'praxis.filesUpload.fieldMoreActions': 'More actions',
2383
+ 'praxis.filesUpload.fieldPendingAriaSuffix': 'files ready to upload',
2384
+ 'praxis.filesUpload.selectedOverlayAriaLabel': 'Selected files',
2385
+ 'praxis.filesUpload.selectedOverlayTitle': 'Selected',
2386
+ 'praxis.filesUpload.bulkSummaryTotal': 'Total',
2387
+ 'praxis.filesUpload.bulkSummarySuccess': 'Success',
2388
+ 'praxis.filesUpload.bulkSummaryFailed': 'Failed',
2389
+ 'praxis.filesUpload.resultMetaIdLabel': 'ID',
2390
+ 'praxis.filesUpload.resultMetaTypeLabel': 'Type',
2391
+ 'praxis.filesUpload.resultMetaSizeLabel': 'Size',
2392
+ 'praxis.filesUpload.resultMetaUploadedAtLabel': 'Uploaded at',
2393
+ 'praxis.filesUpload.policySummaryAny': 'any',
2394
+ 'praxis.filesUpload.policySummaryNotConfigured': '—',
2395
+ 'praxis.filesUpload.policySummaryTypesLabel': 'Types',
2396
+ 'praxis.filesUpload.policySummaryMaxPerFileLabel': 'Max. per file',
2397
+ 'praxis.filesUpload.policySummaryQuantityLabel': 'Qty',
2398
+ 'praxis.filesUpload.showAll': 'Show all',
2399
+ 'praxis.filesUpload.showLess': 'Show less',
2400
+ };
2401
+
2402
+ const FILES_UPLOAD_PT_BR = {
2403
+ 'praxis.filesUpload.settingsAriaLabel': 'Abrir configurações',
2404
+ 'praxis.filesUpload.dropzoneLabel': 'Arraste arquivos ou',
2405
+ 'praxis.filesUpload.dropzoneButton': 'selecionar',
2406
+ 'praxis.filesUpload.conflictPolicyLabel': 'Política de conflito',
2407
+ 'praxis.filesUpload.metadataLabel': 'Metadados (JSON)',
2408
+ 'praxis.filesUpload.progressAriaLabel': 'Progresso do upload',
2409
+ 'praxis.filesUpload.rateLimitBanner': 'Limite de requisições excedido. Tente novamente às',
2410
+ 'praxis.filesUpload.settingsTitle': 'Configuração de Upload de Arquivos',
2411
+ 'praxis.filesUpload.statusSuccess': 'Enviado',
2412
+ 'praxis.filesUpload.statusError': 'Erro',
2413
+ 'praxis.filesUpload.invalidMetadata': 'Metadados inválidos.',
2414
+ 'praxis.filesUpload.genericUploadError': 'Erro no envio de arquivo.',
2415
+ 'praxis.filesUpload.acceptError': 'Tipo de arquivo não permitido.',
2416
+ 'praxis.filesUpload.maxFileSizeError': 'Arquivo excede o tamanho máximo.',
2417
+ 'praxis.filesUpload.maxFilesPerBulkError': 'Quantidade de arquivos excedida.',
2418
+ 'praxis.filesUpload.maxBulkSizeError': 'Tamanho total dos arquivos excedido.',
2419
+ 'praxis.filesUpload.serviceUnavailable': 'Serviço de upload indisponível.',
2420
+ 'praxis.filesUpload.baseUrlMissing': 'Base URL não configurada. Informe [baseUrl] para habilitar o envio.',
2421
+ 'praxis.filesUpload.selectFiles': 'Selecionar arquivo(s)',
2422
+ 'praxis.filesUpload.remove': 'Remover',
2423
+ 'praxis.filesUpload.moreActions': 'Mais ações',
2424
+ 'praxis.filesUpload.policySummaryAction': 'Informações sobre políticas de upload',
2425
+ 'praxis.filesUpload.retry': 'Reenviar',
2426
+ 'praxis.filesUpload.download': 'Baixar',
2427
+ 'praxis.filesUpload.copyLink': 'Copiar link',
2428
+ 'praxis.filesUpload.details': 'Detalhes',
2429
+ 'praxis.filesUpload.detailsMetadata': 'Metadados',
2430
+ 'praxis.filesUpload.dropzoneHint': 'Arraste e solte arquivos aqui ou clique para selecionar',
2431
+ 'praxis.filesUpload.dropzoneProximityHint': 'Solte aqui para enviar',
2432
+ 'praxis.filesUpload.placeholder': 'Selecione ou solte arquivos…',
2433
+ 'praxis.filesUpload.partialUploadError': 'Upload parcial concluído com falhas em parte do lote.',
2434
+ 'praxis.filesUpload.presignMetadataMissing': 'Upload assinado concluído sem metadados do arquivo.',
2435
+ 'praxis.filesUpload.fieldChipAria': 'Arquivos adicionais selecionados',
2436
+ 'praxis.filesUpload.fieldPendingCountSuffix': 'arquivo(s) selecionado(s)',
2437
+ 'praxis.filesUpload.removePendingFile': 'Remover arquivo selecionado',
2438
+ 'praxis.filesUpload.fieldPolicyTypes': 'Tipos',
2439
+ 'praxis.filesUpload.fieldPolicyMaxPerFile': 'Máx/arquivo',
2440
+ 'praxis.filesUpload.fieldPolicyMaxItems': 'Máx/itens',
2441
+ 'praxis.filesUpload.fieldPolicyNotConfigured': 'Não configurado',
2442
+ 'praxis.filesUpload.sizeUnitBytes': 'Bytes',
2443
+ 'praxis.filesUpload.sizeUnitKB': 'KB',
2444
+ 'praxis.filesUpload.sizeUnitMB': 'MB',
2445
+ 'praxis.filesUpload.sizeUnitGB': 'GB',
2446
+ 'praxis.filesUpload.sizeUnitTB': 'TB',
2447
+ 'praxis.filesUpload.errors.INVALID_FILE_TYPE': 'Tipo de arquivo inválido.',
2448
+ 'praxis.filesUpload.errors.FILE_TOO_LARGE': 'Arquivo muito grande.',
2449
+ 'praxis.filesUpload.errors.NOT_FOUND': 'Arquivo não encontrado.',
2450
+ 'praxis.filesUpload.errors.UNAUTHORIZED': 'Requisição não autorizada.',
2451
+ 'praxis.filesUpload.errors.RATE_LIMIT_EXCEEDED': 'Limite de requisições excedido.',
2452
+ 'praxis.filesUpload.errors.INTERNAL_ERROR': 'Erro interno do servidor.',
2453
+ 'praxis.filesUpload.errors.QUOTA_EXCEEDED': 'Cota excedida.',
2454
+ 'praxis.filesUpload.errors.SEC_VIRUS_DETECTED': 'Vírus detectado no arquivo.',
2455
+ 'praxis.filesUpload.errors.SEC_MALICIOUS_CONTENT': 'Conteúdo malicioso detectado.',
2456
+ 'praxis.filesUpload.errors.SEC_DANGEROUS_TYPE': 'Tipo de arquivo perigoso.',
2457
+ 'praxis.filesUpload.errors.FMT_MAGIC_MISMATCH': 'Conteúdo do arquivo incompatível.',
2458
+ 'praxis.filesUpload.errors.FMT_CORRUPTED': 'Arquivo corrompido.',
2459
+ 'praxis.filesUpload.errors.FMT_UNSUPPORTED': 'Formato de arquivo não suportado.',
2460
+ 'praxis.filesUpload.errors.SYS_STORAGE_ERROR': 'Erro de armazenamento.',
2461
+ 'praxis.filesUpload.errors.SYS_SERVICE_DOWN': 'Serviço indisponível.',
2462
+ 'praxis.filesUpload.errors.SYS_RATE_LIMIT': 'Limite de requisições excedido.',
2463
+ // Aliases PT-BR do backend e códigos adicionais
2464
+ 'praxis.filesUpload.errors.ARQUIVO_MUITO_GRANDE': 'Arquivo muito grande.',
2465
+ 'praxis.filesUpload.errors.TIPO_ARQUIVO_INVALIDO': 'Tipo de arquivo inválido.',
2466
+ 'praxis.filesUpload.errors.TIPO_MIDIA_NAO_SUPORTADO': 'Tipo de mídia não suportado.',
2467
+ 'praxis.filesUpload.errors.CAMPO_OBRIGATORIO_AUSENTE': 'Campo obrigatório ausente.',
2468
+ 'praxis.filesUpload.errors.OPCOES_JSON_INVALIDAS': 'JSON de opções inválido.',
2469
+ 'praxis.filesUpload.errors.ARGUMENTO_INVALIDO': 'Argumento inválido.',
2470
+ 'praxis.filesUpload.errors.NAO_AUTORIZADO': 'Autenticação necessária.',
2471
+ 'praxis.filesUpload.errors.ERRO_INTERNO': 'Erro interno do servidor.',
2472
+ 'praxis.filesUpload.errors.LIMITE_TAXA_EXCEDIDO': 'Limite de taxa excedido.',
2473
+ 'praxis.filesUpload.errors.COTA_EXCEDIDA': 'Cota excedida.',
2474
+ 'praxis.filesUpload.errors.ARQUIVO_JA_EXISTE': 'Arquivo já existe.',
2475
+ // Outros possíveis códigos mapeados no catálogo
2476
+ 'praxis.filesUpload.errors.INVALID_JSON_OPTIONS': 'JSON de opções inválido.',
2477
+ 'praxis.filesUpload.errors.EMPTY_FILENAME': 'Nome do arquivo obrigatório.',
2478
+ 'praxis.filesUpload.errors.FILE_EXISTS': 'Arquivo já existe.',
2479
+ 'praxis.filesUpload.errors.PATH_TRAVERSAL': 'Path traversal detectado.',
2480
+ 'praxis.filesUpload.errors.INSUFFICIENT_STORAGE': 'Sem espaço em disco.',
2481
+ 'praxis.filesUpload.errors.UPLOAD_TIMEOUT': 'Tempo esgotado no upload.',
2482
+ 'praxis.filesUpload.errors.BULK_UPLOAD_TIMEOUT': 'Tempo esgotado no upload em lote.',
2483
+ 'praxis.filesUpload.errors.BULK_UPLOAD_CANCELLED': 'Upload em lote cancelado.',
2484
+ 'praxis.filesUpload.errors.USER_CANCELLED': 'Upload cancelado pelo usuário.',
2485
+ 'praxis.filesUpload.errors.UNKNOWN_ERROR': 'Erro desconhecido.',
2486
+ 'praxis.filesUpload.fieldPendingSummary': 'Arquivos prontos para envio',
2487
+ 'praxis.filesUpload.fieldSelectedSummary': 'Arquivo selecionado',
2488
+ 'praxis.filesUpload.fieldSelectedPlural': 'arquivos vinculados',
2489
+ 'praxis.filesUpload.fieldEmptySummary': 'Nenhum arquivo selecionado',
2490
+ 'praxis.filesUpload.fieldErrorSummary': 'Corrija o erro para continuar',
2491
+ 'praxis.filesUpload.fieldActionUpload': 'Enviar arquivos selecionados',
2492
+ 'praxis.filesUpload.fieldActionUploadShort': 'Enviar',
2493
+ 'praxis.filesUpload.fieldActionCancel': 'Cancelar seleção atual',
2494
+ 'praxis.filesUpload.fieldActionCancelShort': 'Cancelar',
2495
+ 'praxis.filesUpload.fieldActionSelect': 'Selecionar arquivo(s)',
2496
+ 'praxis.filesUpload.fieldActionClear': 'Limpar',
2497
+ 'praxis.filesUpload.fieldInfoAction': 'Informações',
2498
+ 'praxis.filesUpload.fieldMoreActions': 'Mais ações',
2499
+ 'praxis.filesUpload.fieldPendingAriaSuffix': 'arquivos prontos para envio',
2500
+ 'praxis.filesUpload.selectedOverlayAriaLabel': 'Arquivos selecionados',
2501
+ 'praxis.filesUpload.selectedOverlayTitle': 'Selecionados',
2502
+ 'praxis.filesUpload.bulkSummaryTotal': 'Total',
2503
+ 'praxis.filesUpload.bulkSummarySuccess': 'Sucesso',
2504
+ 'praxis.filesUpload.bulkSummaryFailed': 'Falhas',
2505
+ 'praxis.filesUpload.resultMetaIdLabel': 'ID',
2506
+ 'praxis.filesUpload.resultMetaTypeLabel': 'Tipo',
2507
+ 'praxis.filesUpload.resultMetaSizeLabel': 'Tamanho',
2508
+ 'praxis.filesUpload.resultMetaUploadedAtLabel': 'Enviado em',
2509
+ 'praxis.filesUpload.policySummaryAny': 'qualquer',
2510
+ 'praxis.filesUpload.policySummaryNotConfigured': '—',
2511
+ 'praxis.filesUpload.policySummaryTypesLabel': 'Tipos',
2512
+ 'praxis.filesUpload.policySummaryMaxPerFileLabel': 'Máx. por arquivo',
2513
+ 'praxis.filesUpload.policySummaryQuantityLabel': 'Qtde',
2514
+ 'praxis.filesUpload.showAll': 'Ver todos',
2515
+ 'praxis.filesUpload.showLess': 'Ver menos',
2516
+ };
2517
+
2518
+ /** @deprecated Transitional compatibility token. Prefer PraxisI18nService. */
1808
2519
  const FILES_UPLOAD_TEXTS = new InjectionToken('FILES_UPLOAD_TEXTS', {
1809
2520
  providedIn: 'root',
1810
2521
  factory: () => ({
@@ -1817,97 +2528,62 @@ const FILES_UPLOAD_TEXTS = new InjectionToken('FILES_UPLOAD_TEXTS', {
1817
2528
  rateLimitBanner: 'Limite de requisições excedido. Tente novamente às',
1818
2529
  }),
1819
2530
  });
1820
-
2531
+ /** @deprecated Transitional compatibility token. Prefer PraxisI18nService. */
1821
2532
  const TRANSLATE_LIKE = new InjectionToken('TRANSLATE_LIKE');
1822
- const FILES_UPLOAD_ERROR_MESSAGES = new InjectionToken('FILES_UPLOAD_ERROR_MESSAGES');
1823
- const DEFAULT_MESSAGES = {
1824
- INVALID_FILE_TYPE: 'Tipo de arquivo inválido.',
1825
- FILE_TOO_LARGE: 'Arquivo muito grande.',
1826
- NOT_FOUND: 'Arquivo não encontrado.',
1827
- UNAUTHORIZED: 'Requisição não autorizada.',
1828
- RATE_LIMIT_EXCEEDED: 'Limite de requisições excedido.',
1829
- INTERNAL_ERROR: 'Erro interno do servidor.',
1830
- QUOTA_EXCEEDED: 'Cota excedida.',
1831
- SEC_VIRUS_DETECTED: 'Vírus detectado no arquivo.',
1832
- SEC_MALICIOUS_CONTENT: 'Conteúdo malicioso detectado.',
1833
- SEC_DANGEROUS_TYPE: 'Tipo de arquivo perigoso.',
1834
- FMT_MAGIC_MISMATCH: 'Conteúdo do arquivo incompatível.',
1835
- FMT_CORRUPTED: 'Arquivo corrompido.',
1836
- FMT_UNSUPPORTED: 'Formato de arquivo não suportado.',
1837
- SYS_STORAGE_ERROR: 'Erro de armazenamento.',
1838
- SYS_SERVICE_DOWN: 'Serviço indisponível.',
1839
- SYS_RATE_LIMIT: 'Limite de requisições excedido.',
1840
- // aliases em português vindos do backend
1841
- ARQUIVO_MUITO_GRANDE: 'Arquivo muito grande.',
1842
- TIPO_ARQUIVO_INVALIDO: 'Tipo de arquivo inválido.',
1843
- TIPO_MIDIA_NAO_SUPORTADO: 'Tipo de mídia não suportado.',
1844
- CAMPO_OBRIGATORIO_AUSENTE: 'Campo obrigatório ausente.',
1845
- OPCOES_JSON_INVALIDAS: 'JSON de opções inválido.',
1846
- ARGUMENTO_INVALIDO: 'Argumento inválido.',
1847
- NAO_AUTORIZADO: 'Autenticação necessária.',
1848
- ERRO_INTERNO: 'Erro interno do servidor.',
1849
- LIMITE_TAXA_EXCEDIDO: 'Limite de taxa excedido.',
1850
- COTA_EXCEDIDA: 'Cota excedida.',
1851
- ARQUIVO_JA_EXISTE: 'Arquivo já existe.',
1852
- };
1853
- class ErrorMapperService {
1854
- customMessages;
1855
- translate;
1856
- constructor(customMessages, translate) {
1857
- this.customMessages = customMessages;
1858
- this.translate = translate;
1859
- }
1860
- map(error, headers) {
1861
- const code = String(error.code || 'UNKNOWN_ERROR');
1862
- const key = `praxis.filesUpload.errors.${code}`;
1863
- const translated = this.translate?.instant(key);
1864
- const message = (translated && translated !== key ? translated : undefined) ??
1865
- this.customMessages?.[code] ??
1866
- DEFAULT_MESSAGES[code] ??
1867
- error.message ??
1868
- 'Erro desconhecido.';
1869
- // metadados do catálogo
1870
- const meta = getErrorMeta(code);
1871
- const mapped = {
1872
- message,
1873
- title: meta.title,
1874
- severity: meta.severity,
1875
- category: meta.category,
1876
- phase: meta.phase,
1877
- userAction: meta.userAction,
1878
- };
1879
- const isRateLimit = code === 'LIMITE_TAXA_EXCEDIDO' ||
1880
- code === 'RATE_LIMIT_EXCEEDED' ||
1881
- code === 'SYS_RATE_LIMIT';
1882
- if (isRateLimit) {
1883
- const limit = Number(headers?.get('X-RateLimit-Limit'));
1884
- const remaining = Number(headers?.get('X-RateLimit-Remaining'));
1885
- const resetEpochSeconds = Number(headers?.get('X-RateLimit-Reset'));
1886
- if (!Number.isNaN(limit) &&
1887
- !Number.isNaN(remaining) &&
1888
- !Number.isNaN(resetEpochSeconds)) {
1889
- mapped.rateLimit = { limit, remaining, resetEpochSeconds };
1890
- }
2533
+ function createPraxisFilesUploadI18nConfig(options = {}) {
2534
+ const dictionaries = {
2535
+ 'pt-BR': {
2536
+ ...FILES_UPLOAD_PT_BR,
2537
+ ...(options.dictionaries?.['pt-BR'] ?? {}),
2538
+ },
2539
+ 'en-US': {
2540
+ ...FILES_UPLOAD_EN_US,
2541
+ ...(options.dictionaries?.['en-US'] ?? {}),
2542
+ },
2543
+ };
2544
+ for (const [locale, dictionary] of Object.entries(options.dictionaries ?? {})) {
2545
+ if (locale === 'pt-BR' || locale === 'en-US') {
2546
+ continue;
1891
2547
  }
1892
- return mapped;
2548
+ dictionaries[locale] = {
2549
+ ...(dictionaries[locale] ?? {}),
2550
+ ...dictionary,
2551
+ };
1893
2552
  }
1894
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ErrorMapperService, deps: [{ token: FILES_UPLOAD_ERROR_MESSAGES, optional: true }, { token: TRANSLATE_LIKE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
1895
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ErrorMapperService, providedIn: 'root' });
1896
- }
1897
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ErrorMapperService, decorators: [{
1898
- type: Injectable,
1899
- args: [{ providedIn: 'root' }]
1900
- }], ctorParameters: () => [{ type: undefined, decorators: [{
1901
- type: Optional
1902
- }, {
1903
- type: Inject,
1904
- args: [FILES_UPLOAD_ERROR_MESSAGES]
1905
- }] }, { type: undefined, decorators: [{
1906
- type: Optional
1907
- }, {
1908
- type: Inject,
1909
- args: [TRANSLATE_LIKE]
1910
- }] }] });
2553
+ return {
2554
+ locale: options.locale,
2555
+ fallbackLocale: options.fallbackLocale ?? 'pt-BR',
2556
+ dictionaries,
2557
+ };
2558
+ }
2559
+ function providePraxisFilesUploadI18n(options = {}) {
2560
+ return providePraxisI18n(createPraxisFilesUploadI18nConfig(options));
2561
+ }
2562
+ function resolvePraxisFilesUploadText(i18n, key, fallback, legacyTexts, legacyTranslate) {
2563
+ const namespacedKey = key.startsWith('praxis.filesUpload.')
2564
+ ? key
2565
+ : `praxis.filesUpload.${key}`;
2566
+ const coreTranslation = i18n.t(namespacedKey, undefined, '');
2567
+ if (coreTranslation) {
2568
+ return coreTranslation;
2569
+ }
2570
+ const legacyText = legacyTexts?.[key];
2571
+ if (legacyText) {
2572
+ return legacyText;
2573
+ }
2574
+ const translatedByLegacyService = legacyTranslate?.t?.(namespacedKey, undefined, '') ||
2575
+ legacyTranslate?.t?.(key, undefined, '') ||
2576
+ resolveLegacyInstantTranslation(legacyTranslate, namespacedKey) ||
2577
+ resolveLegacyInstantTranslation(legacyTranslate, key);
2578
+ if (translatedByLegacyService) {
2579
+ return translatedByLegacyService;
2580
+ }
2581
+ return fallback;
2582
+ }
2583
+ function resolveLegacyInstantTranslation(legacyTranslate, key) {
2584
+ const translated = legacyTranslate?.instant?.(key);
2585
+ return translated && translated !== key ? translated : '';
2586
+ }
1911
2587
 
1912
2588
  function toError(key, info) {
1913
2589
  return { [key]: info };
@@ -2044,9 +2720,7 @@ class FilesApiClient {
2044
2720
  constructor(http) {
2045
2721
  this.http = http;
2046
2722
  }
2047
- /**
2048
- * Upload a single file com metadata/conflictPolicy (compat) e/ou options JSON.
2049
- */
2723
+ /** Upload de um único arquivo com metadata/conflictPolicy e options JSON. */
2050
2724
  upload(baseUrl, file, options = {}) {
2051
2725
  const formData = new FormData();
2052
2726
  formData.append('file', file);
@@ -2063,9 +2737,7 @@ class FilesApiClient {
2063
2737
  })
2064
2738
  .pipe(retry(options.retryCount ?? 0));
2065
2739
  }
2066
- /**
2067
- * Upload múltiplo com arrays de metadata/conflictPolicy (compat) e/ou options JSON.
2068
- */
2740
+ /** Upload múltiplo com arrays de metadata/conflictPolicy e options JSON. */
2069
2741
  bulkUpload(baseUrl, files, options = {}) {
2070
2742
  const formData = new FormData();
2071
2743
  files.forEach((f) => {
@@ -2073,10 +2745,9 @@ class FilesApiClient {
2073
2745
  });
2074
2746
  if (options.optionsJson)
2075
2747
  formData.append('options', options.optionsJson);
2076
- // Suporte a failFast/failFastMode
2077
- const ff = options.failFast ?? options.failFastMode;
2078
- if (ff !== undefined)
2079
- formData.append('failFast', String(ff));
2748
+ if (options.failFast !== undefined) {
2749
+ formData.append('failFastMode', String(options.failFast));
2750
+ }
2080
2751
  // Arrays de metadata/políticas quando presentes
2081
2752
  const metas = files.map((f) => f.metadata ?? null);
2082
2753
  if (metas.some((m) => m !== null)) {
@@ -2114,13 +2785,23 @@ class PresignedUploaderService {
2114
2785
  /**
2115
2786
  * Uploads a file to a presigned URL and retries when configured.
2116
2787
  */
2117
- upload(target, file, retryCount = 0) {
2788
+ upload(target, file, options = {}, retryCount = 0) {
2118
2789
  const formData = new FormData();
2119
2790
  if (target.fields) {
2120
2791
  Object.entries(target.fields).forEach(([key, value]) => {
2121
2792
  formData.append(key, value);
2122
2793
  });
2123
2794
  }
2795
+ if (this.shouldAppendPraxisFields(target.uploadUrl)) {
2796
+ if (options.optionsJson)
2797
+ formData.append('options', options.optionsJson);
2798
+ if (options.metadata) {
2799
+ formData.append('metadata', JSON.stringify(options.metadata));
2800
+ }
2801
+ if (options.conflictPolicy) {
2802
+ formData.append('conflictPolicy', options.conflictPolicy);
2803
+ }
2804
+ }
2124
2805
  formData.append('file', file);
2125
2806
  return this.http
2126
2807
  .post(target.uploadUrl, formData, {
@@ -2131,6 +2812,18 @@ class PresignedUploaderService {
2131
2812
  })
2132
2813
  .pipe(retry(retryCount));
2133
2814
  }
2815
+ shouldAppendPraxisFields(uploadUrl) {
2816
+ if (!uploadUrl)
2817
+ return false;
2818
+ if (uploadUrl.startsWith('/'))
2819
+ return true;
2820
+ try {
2821
+ return new URL(uploadUrl, window.location.origin).origin === window.location.origin;
2822
+ }
2823
+ catch {
2824
+ return false;
2825
+ }
2826
+ }
2134
2827
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PresignedUploaderService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
2135
2828
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PresignedUploaderService, providedIn: 'root' });
2136
2829
  }
@@ -2139,38 +2832,123 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
2139
2832
  args: [{ providedIn: 'root' }]
2140
2833
  }], ctorParameters: () => [{ type: i1.HttpClient }] });
2141
2834
 
2835
+ const FILES_UPLOAD_ERROR_MESSAGES = new InjectionToken('FILES_UPLOAD_ERROR_MESSAGES');
2836
+ const DEFAULT_MESSAGES = {
2837
+ INVALID_FILE_TYPE: 'Tipo de arquivo inválido.',
2838
+ FILE_TOO_LARGE: 'Arquivo muito grande.',
2839
+ NOT_FOUND: 'Arquivo não encontrado.',
2840
+ UNAUTHORIZED: 'Requisição não autorizada.',
2841
+ RATE_LIMIT_EXCEEDED: 'Limite de requisições excedido.',
2842
+ INTERNAL_ERROR: 'Erro interno do servidor.',
2843
+ QUOTA_EXCEEDED: 'Cota excedida.',
2844
+ SEC_VIRUS_DETECTED: 'Vírus detectado no arquivo.',
2845
+ SEC_MALICIOUS_CONTENT: 'Conteúdo malicioso detectado.',
2846
+ SEC_DANGEROUS_TYPE: 'Tipo de arquivo perigoso.',
2847
+ FMT_MAGIC_MISMATCH: 'Conteúdo do arquivo incompatível.',
2848
+ FMT_CORRUPTED: 'Arquivo corrompido.',
2849
+ FMT_UNSUPPORTED: 'Formato de arquivo não suportado.',
2850
+ SYS_STORAGE_ERROR: 'Erro de armazenamento.',
2851
+ SYS_SERVICE_DOWN: 'Serviço indisponível.',
2852
+ SYS_RATE_LIMIT: 'Limite de requisições excedido.',
2853
+ // aliases em português vindos do backend
2854
+ ARQUIVO_MUITO_GRANDE: 'Arquivo muito grande.',
2855
+ TIPO_ARQUIVO_INVALIDO: 'Tipo de arquivo inválido.',
2856
+ TIPO_MIDIA_NAO_SUPORTADO: 'Tipo de mídia não suportado.',
2857
+ CAMPO_OBRIGATORIO_AUSENTE: 'Campo obrigatório ausente.',
2858
+ OPCOES_JSON_INVALIDAS: 'JSON de opções inválido.',
2859
+ ARGUMENTO_INVALIDO: 'Argumento inválido.',
2860
+ NAO_AUTORIZADO: 'Autenticação necessária.',
2861
+ ERRO_INTERNO: 'Erro interno do servidor.',
2862
+ LIMITE_TAXA_EXCEDIDO: 'Limite de taxa excedido.',
2863
+ COTA_EXCEDIDA: 'Cota excedida.',
2864
+ ARQUIVO_JA_EXISTE: 'Arquivo já existe.',
2865
+ };
2866
+ class ErrorMapperService {
2867
+ i18n;
2868
+ customMessages;
2869
+ legacyTranslate;
2870
+ constructor(i18n, customMessages, legacyTranslate) {
2871
+ this.i18n = i18n;
2872
+ this.customMessages = customMessages;
2873
+ this.legacyTranslate = legacyTranslate;
2874
+ }
2875
+ map(error, headers) {
2876
+ const primary = getPrimaryErrorItem(error);
2877
+ const code = String(primary?.code || 'UNKNOWN_ERROR');
2878
+ const translatedMessage = resolvePraxisFilesUploadText(this.i18n, `errors.${code}`, '', undefined, this.legacyTranslate);
2879
+ const defaultMessage = DEFAULT_MESSAGES[code] ??
2880
+ primary?.message ??
2881
+ (isErrorEnvelope(error) ? error.message : undefined) ??
2882
+ 'Erro desconhecido.';
2883
+ const configuredMessage = this.customMessages?.[code];
2884
+ const message = configuredMessage ??
2885
+ (translatedMessage || defaultMessage);
2886
+ // metadados do catálogo
2887
+ const meta = getErrorMeta(code);
2888
+ const mapped = {
2889
+ message,
2890
+ title: meta.title,
2891
+ severity: meta.severity,
2892
+ category: meta.category,
2893
+ phase: meta.phase,
2894
+ userAction: meta.userAction,
2895
+ };
2896
+ const isRateLimit = code === 'LIMITE_TAXA_EXCEDIDO' ||
2897
+ code === 'RATE_LIMIT_EXCEEDED' ||
2898
+ code === 'SYS_RATE_LIMIT';
2899
+ if (isRateLimit) {
2900
+ const limit = Number(headers?.get('X-RateLimit-Limit'));
2901
+ const remaining = Number(headers?.get('X-RateLimit-Remaining'));
2902
+ const resetEpochSeconds = Number(headers?.get('X-RateLimit-Reset'));
2903
+ if (!Number.isNaN(limit) &&
2904
+ !Number.isNaN(remaining) &&
2905
+ !Number.isNaN(resetEpochSeconds)) {
2906
+ mapped.rateLimit = { limit, remaining, resetEpochSeconds };
2907
+ }
2908
+ }
2909
+ return mapped;
2910
+ }
2911
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ErrorMapperService, deps: [{ token: i1$2.PraxisI18nService }, { token: FILES_UPLOAD_ERROR_MESSAGES, optional: true }, { token: TRANSLATE_LIKE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
2912
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ErrorMapperService, providedIn: 'root' });
2913
+ }
2914
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ErrorMapperService, decorators: [{
2915
+ type: Injectable,
2916
+ args: [{ providedIn: 'root' }]
2917
+ }], ctorParameters: () => [{ type: i1$2.PraxisI18nService }, { type: undefined, decorators: [{
2918
+ type: Optional
2919
+ }, {
2920
+ type: Inject,
2921
+ args: [FILES_UPLOAD_ERROR_MESSAGES]
2922
+ }] }, { type: undefined, decorators: [{
2923
+ type: Optional
2924
+ }, {
2925
+ type: Inject,
2926
+ args: [TRANSLATE_LIKE]
2927
+ }] }] });
2928
+
2929
+ let nextPraxisFilesUploadA11yId = 0;
2142
2930
  class PraxisFilesUpload {
2143
2931
  settingsPanel;
2144
2932
  dragProx;
2145
- defaultTexts;
2146
2933
  overlay;
2147
2934
  viewContainerRef;
2148
2935
  cdr;
2149
2936
  configStorage;
2937
+ i18n;
2938
+ legacyTexts;
2939
+ legacyTranslate;
2940
+ configService;
2150
2941
  api;
2151
2942
  presign;
2152
2943
  errors;
2153
- translate;
2154
2944
  config = null;
2155
2945
  _filesUploadId = '';
2156
- _uploadId = '';
2157
2946
  set filesUploadId(value) {
2158
2947
  this._filesUploadId = value ?? '';
2159
- if (!this._uploadId)
2160
- this._uploadId = this._filesUploadId;
2161
2948
  }
2162
2949
  get filesUploadId() {
2163
2950
  return this._filesUploadId;
2164
2951
  }
2165
- set uploadId(value) {
2166
- const v = value ?? '';
2167
- this._uploadId = v;
2168
- if (!this._filesUploadId)
2169
- this._filesUploadId = v;
2170
- }
2171
- get uploadId() {
2172
- return this._uploadId;
2173
- }
2174
2952
  componentInstanceId;
2175
2953
  baseUrl;
2176
2954
  displayMode = 'full';
@@ -2182,15 +2960,15 @@ class PraxisFilesUpload {
2182
2960
  rateLimited = new EventEmitter();
2183
2961
  uploadStart = new EventEmitter();
2184
2962
  uploadProgress = new EventEmitter();
2185
- uploadError = new EventEmitter();
2186
2963
  // Novos outputs Sub-issue 4
2187
2964
  retry = new EventEmitter();
2188
2965
  download = new EventEmitter();
2189
2966
  copyLink = new EventEmitter();
2190
2967
  detailsOpened = new EventEmitter();
2191
2968
  detailsClosed = new EventEmitter();
2192
- pendingFilesChange = new EventEmitter();
2969
+ pendingStateChange = new EventEmitter();
2193
2970
  proximityChange = new EventEmitter();
2971
+ readinessChange = new EventEmitter();
2194
2972
  fileInput;
2195
2973
  hostRef;
2196
2974
  overlayTmpl;
@@ -2217,6 +2995,8 @@ class PraxisFilesUpload {
2217
2995
  currentFiles = [];
2218
2996
  // Config efetiva do servidor para fallbacks
2219
2997
  serverEffective;
2998
+ readinessState = 'idle';
2999
+ readinessSource = 'local';
2220
3000
  // Proximidade/overlay
2221
3001
  showProximityOverlay = false;
2222
3002
  proxSub;
@@ -2241,19 +3021,31 @@ class PraxisFilesUpload {
2241
3021
  selectedOverlayRef;
2242
3022
  // Lista: colapso
2243
3023
  showAllResults = false;
2244
- constructor(settingsPanel, dragProx, defaultTexts, overlay, viewContainerRef, cdr, configStorage, api, presign, errors, translate) {
3024
+ initialized = false;
3025
+ generatedA11yId = `files-upload-${++nextPraxisFilesUploadA11yId}`;
3026
+ constructor(settingsPanel, dragProx, overlay, viewContainerRef, cdr, configStorage, i18n, legacyTexts, legacyTranslate, configService, api, presign, errors) {
2245
3027
  this.settingsPanel = settingsPanel;
2246
3028
  this.dragProx = dragProx;
2247
- this.defaultTexts = defaultTexts;
2248
3029
  this.overlay = overlay;
2249
3030
  this.viewContainerRef = viewContainerRef;
2250
3031
  this.cdr = cdr;
2251
3032
  this.configStorage = configStorage;
3033
+ this.i18n = i18n;
3034
+ this.legacyTexts = legacyTexts;
3035
+ this.legacyTranslate = legacyTranslate;
3036
+ this.configService = configService;
2252
3037
  this.api = api;
2253
3038
  this.presign = presign;
2254
3039
  this.errors = errors;
2255
- this.translate = translate;
2256
- this.texts = defaultTexts;
3040
+ this.texts = {
3041
+ settingsAriaLabel: '',
3042
+ dropzoneLabel: '',
3043
+ dropzoneButton: '',
3044
+ conflictPolicyLabel: '',
3045
+ metadataLabel: '',
3046
+ progressAriaLabel: '',
3047
+ rateLimitBanner: '',
3048
+ };
2257
3049
  }
2258
3050
  componentKeys = inject(ComponentKeyService);
2259
3051
  route = (() => { try {
@@ -2263,6 +3055,18 @@ class PraxisFilesUpload {
2263
3055
  return undefined;
2264
3056
  } })();
2265
3057
  warnedMissingId = false;
3058
+ requireApiClient() {
3059
+ if (!this.api) {
3060
+ throw new Error('FilesApiClient is not available.');
3061
+ }
3062
+ return this.api;
3063
+ }
3064
+ requireBaseUrl() {
3065
+ if (!this.baseUrl) {
3066
+ throw new Error(this.t('baseUrlMissing', 'Base URL não configurada. Informe [baseUrl] para habilitar o envio.'));
3067
+ }
3068
+ return this.baseUrl;
3069
+ }
2266
3070
  // Helpers de UI
2267
3071
  get showProgress() {
2268
3072
  return this.uploading;
@@ -2280,6 +3084,24 @@ class PraxisFilesUpload {
2280
3084
  get pendingCount() {
2281
3085
  return this.currentFiles.length;
2282
3086
  }
3087
+ get a11yBaseId() {
3088
+ return this.filesUploadId || this.componentInstanceId || this.generatedA11yId;
3089
+ }
3090
+ get messageRegionId() {
3091
+ return `${this.a11yBaseId}-messages`;
3092
+ }
3093
+ get progressRegionId() {
3094
+ return `${this.a11yBaseId}-progress`;
3095
+ }
3096
+ get conflictPolicyInputId() {
3097
+ return `${this.a11yBaseId}-conflict-policy`;
3098
+ }
3099
+ get metadataInputId() {
3100
+ return `${this.a11yBaseId}-metadata`;
3101
+ }
3102
+ get selectedOverlayTitleId() {
3103
+ return `${this.a11yBaseId}-selected-title`;
3104
+ }
2283
3105
  get hasLastResults() {
2284
3106
  return Array.isArray(this.lastResults) && this.lastResults.length > 0;
2285
3107
  }
@@ -2290,7 +3112,7 @@ class PraxisFilesUpload {
2290
3112
  }
2291
3113
  }
2292
3114
  // Expor método público para pais (wrapper) abrirem o seletor
2293
- triggerSelect() {
3115
+ openFileDialog() {
2294
3116
  const el = this.fileInput?.nativeElement;
2295
3117
  if (el)
2296
3118
  this.openFile(el);
@@ -2301,9 +3123,15 @@ class PraxisFilesUpload {
2301
3123
  }
2302
3124
  formatBytes(bytes) {
2303
3125
  const b = typeof bytes === 'number' ? bytes : 0;
2304
- if (b < 1024)
2305
- return `${b} B`;
2306
- const units = ['KB', 'MB', 'GB', 'TB'];
3126
+ if (b < 1024) {
3127
+ return `${b} ${this.t('sizeUnitBytes', 'Bytes')}`;
3128
+ }
3129
+ const units = [
3130
+ this.t('sizeUnitKB', 'KB'),
3131
+ this.t('sizeUnitMB', 'MB'),
3132
+ this.t('sizeUnitGB', 'GB'),
3133
+ this.t('sizeUnitTB', 'TB'),
3134
+ ];
2307
3135
  let v = b / 1024;
2308
3136
  let i = 0;
2309
3137
  while (v >= 1024 && i < units.length - 1) {
@@ -2336,6 +3164,8 @@ class PraxisFilesUpload {
2336
3164
  const key = this.storageKey();
2337
3165
  this.applyConfig(this.config ?? {});
2338
3166
  this.resolveTexts();
3167
+ this.loadServerEffectiveConfig();
3168
+ this.initialized = true;
2339
3169
  if (key) {
2340
3170
  this.configStorage.loadConfig(key).pipe(take(1)).subscribe((stored) => {
2341
3171
  if (stored) {
@@ -2345,8 +3175,13 @@ class PraxisFilesUpload {
2345
3175
  }
2346
3176
  });
2347
3177
  }
2348
- // Nota: carregamento da configuração efetiva do servidor foi desabilitado aqui
2349
- // para evitar dependência de HttpClient em testes. O fallback permanece opcional.
3178
+ }
3179
+ ngOnChanges() {
3180
+ this.applyConfig(this.config ?? {});
3181
+ this.resolveTexts();
3182
+ if (this.initialized) {
3183
+ this.loadServerEffectiveConfig();
3184
+ }
2350
3185
  }
2351
3186
  ngAfterViewInit() {
2352
3187
  // Calcular retângulo do host
@@ -2553,24 +3388,38 @@ class PraxisFilesUpload {
2553
3388
  if (this.showProximityOverlay) {
2554
3389
  this.showProximityOverlay = false;
2555
3390
  this.manageOverlay();
3391
+ this.proximityChange.emit(false);
2556
3392
  }
2557
3393
  }
2558
3394
  // Helpers de configuração e armazenamento
2559
3395
  resolveTexts() {
2560
3396
  this.texts = {
2561
- settingsAriaLabel: this.t('settingsAriaLabel', this.defaultTexts.settingsAriaLabel),
2562
- dropzoneLabel: this.t('dropzoneLabel', this.defaultTexts.dropzoneLabel),
2563
- dropzoneButton: this.t('dropzoneButton', this.defaultTexts.dropzoneButton),
2564
- conflictPolicyLabel: this.t('conflictPolicyLabel', this.defaultTexts.conflictPolicyLabel),
2565
- metadataLabel: this.t('metadataLabel', this.defaultTexts.metadataLabel),
2566
- progressAriaLabel: this.t('progressAriaLabel', this.defaultTexts.progressAriaLabel),
2567
- rateLimitBanner: this.t('rateLimitBanner', this.defaultTexts.rateLimitBanner),
3397
+ settingsAriaLabel: this.t('settingsAriaLabel', 'Abrir configurações'),
3398
+ dropzoneLabel: this.t('dropzoneLabel', 'Arraste arquivos ou'),
3399
+ dropzoneButton: this.t('dropzoneButton', 'selecionar'),
3400
+ conflictPolicyLabel: this.t('conflictPolicyLabel', 'Política de conflito'),
3401
+ metadataLabel: this.t('metadataLabel', 'Metadados (JSON)'),
3402
+ progressAriaLabel: this.t('progressAriaLabel', 'Progresso do upload'),
3403
+ rateLimitBanner: this.t('rateLimitBanner', 'Limite de requisições excedido. Tente novamente às'),
2568
3404
  };
2569
3405
  }
2570
3406
  t(key, fallback) {
2571
- const k = `praxis.filesUpload.${key}`;
2572
- const v = this.translate?.instant(k);
2573
- return v && v !== k ? v : fallback;
3407
+ return resolvePraxisFilesUploadText(this.i18n, key, fallback, this.legacyTexts, this.legacyTranslate);
3408
+ }
3409
+ get selectFilesAriaLabel() {
3410
+ return this.t('selectFiles', 'Selecionar arquivo(s)');
3411
+ }
3412
+ get policySummaryAriaLabel() {
3413
+ return this.t('policySummaryAction', 'Informacoes sobre politicas de upload');
3414
+ }
3415
+ pendingFileRemoveAriaLabel(file) {
3416
+ return `${this.t('removePendingFile', 'Remover arquivo selecionado')}: ${file.name}`;
3417
+ }
3418
+ pendingFileMoreActionsAriaLabel(file) {
3419
+ return `${this.t('moreActions', 'Mais ações')}: ${file.name}`;
3420
+ }
3421
+ resultActionAriaLabel(actionKey, fallback, fileName) {
3422
+ return `${this.t(actionKey, fallback)}: ${fileName}`;
2574
3423
  }
2575
3424
  openSettings() {
2576
3425
  if (!this.editModeEnabled)
@@ -2601,11 +3450,42 @@ class PraxisFilesUpload {
2601
3450
  }
2602
3451
  applyConfig(cfg) {
2603
3452
  this.config = { ...cfg };
2604
- this.conflictPolicy.setValue((cfg.limits?.defaultConflictPolicy ?? 'RENAME'));
3453
+ this.conflictPolicy.setValue((cfg.options?.defaultConflictPolicy ?? 'RENAME'));
2605
3454
  this.allowMultiple = (cfg.limits?.maxFilesPerBulk ?? 1) > 1;
2606
3455
  const acc = cfg.ui?.accept ?? [];
2607
3456
  this.acceptString = acc.includes('*') ? '' : acc.join(',');
2608
3457
  }
3458
+ loadServerEffectiveConfig() {
3459
+ if (!this.configService || !this.baseUrl) {
3460
+ this.setReadinessState('ready', 'local');
3461
+ return;
3462
+ }
3463
+ this.setReadinessState('loading', 'server');
3464
+ this.configService
3465
+ .getEffectiveConfig(this.baseUrl)
3466
+ .pipe(take(1))
3467
+ .subscribe({
3468
+ next: (cfg) => {
3469
+ this.serverEffective = cfg;
3470
+ this.setReadinessState('ready', 'server');
3471
+ },
3472
+ error: () => {
3473
+ // Fallback local segue válido quando a consulta de config falha.
3474
+ this.setReadinessState('ready', 'fallback');
3475
+ },
3476
+ });
3477
+ }
3478
+ setReadinessState(state, source) {
3479
+ this.readinessState = state;
3480
+ this.readinessSource = source;
3481
+ this.readinessChange.emit({
3482
+ state,
3483
+ source,
3484
+ filesUploadId: this.filesUploadId || undefined,
3485
+ baseUrl: this.baseUrl,
3486
+ });
3487
+ this.cdr.markForCheck();
3488
+ }
2609
3489
  storageKey() {
2610
3490
  const id = this.componentKeyId();
2611
3491
  return id ? `files-upload-config:${id}` : null;
@@ -2627,7 +3507,7 @@ class PraxisFilesUpload {
2627
3507
  if (this.warnedMissingId)
2628
3508
  return;
2629
3509
  this.warnedMissingId = true;
2630
- console.warn('[PraxisFilesUpload] filesUploadId ou uploadId sao necessarios para persistencia de configuracao.');
3510
+ console.warn('[PraxisFilesUpload] filesUploadId e necessario para persistencia de configuracao.');
2631
3511
  }
2632
3512
  onDragOver(evt) {
2633
3513
  evt.preventDefault();
@@ -2645,7 +3525,7 @@ class PraxisFilesUpload {
2645
3525
  if (this.config?.ui?.manualUpload) {
2646
3526
  this.currentFiles = files;
2647
3527
  this.manageSelectedOverlay();
2648
- this.pendingFilesChange.emit(this.currentFiles);
3528
+ this.emitPendingState();
2649
3529
  }
2650
3530
  else {
2651
3531
  this.startUpload(files);
@@ -2659,7 +3539,7 @@ class PraxisFilesUpload {
2659
3539
  if (this.config?.ui?.manualUpload) {
2660
3540
  this.currentFiles = files;
2661
3541
  this.manageSelectedOverlay();
2662
- this.pendingFilesChange.emit(this.currentFiles);
3542
+ this.emitPendingState();
2663
3543
  }
2664
3544
  else {
2665
3545
  this.startUpload(files);
@@ -2667,24 +3547,32 @@ class PraxisFilesUpload {
2667
3547
  input.value = '';
2668
3548
  }
2669
3549
  }
2670
- triggerUpload() {
3550
+ submitPendingFiles() {
2671
3551
  if (this.currentFiles.length) {
2672
3552
  this.hideSelectedOverlay();
2673
3553
  this.startUpload(this.currentFiles);
2674
3554
  }
2675
3555
  }
2676
- clearSelection() {
3556
+ resetPendingFiles() {
2677
3557
  this.currentFiles = [];
2678
- this.pendingFilesChange.emit(this.currentFiles);
3558
+ this.emitPendingState();
2679
3559
  this.errorMessage = '';
2680
3560
  this.hideSelectedOverlay();
2681
3561
  }
2682
3562
  // Remoção individual dentro do overlay de seleção
2683
3563
  removePendingFile(file) {
2684
3564
  this.currentFiles = this.currentFiles.filter((f) => f !== file);
2685
- this.pendingFilesChange.emit(this.currentFiles);
3565
+ this.emitPendingState();
2686
3566
  this.manageSelectedOverlay();
2687
3567
  }
3568
+ emitPendingState() {
3569
+ const files = [...this.currentFiles];
3570
+ this.pendingStateChange.emit({
3571
+ files,
3572
+ count: files.length,
3573
+ hasPending: files.length > 0,
3574
+ });
3575
+ }
2688
3576
  startUpload(files) {
2689
3577
  if (!this.baseUrl ||
2690
3578
  (this.quotaExceeded && this.config?.quotas?.blockOnExceed)) {
@@ -2727,36 +3615,11 @@ class PraxisFilesUpload {
2727
3615
  files,
2728
3616
  mode: files.length > 1 && this.allowMultiple ? 'bulk' : 'single',
2729
3617
  strategy,
2730
- uploadId: this.uploadId || this.filesUploadId,
3618
+ filesUploadId: this.filesUploadId || undefined,
2731
3619
  baseUrl: this.baseUrl,
2732
3620
  });
2733
3621
  if (files.length > 1 && this.allowMultiple) {
2734
- const policy = this.conflictPolicy.value || 'RENAME';
2735
- const bulkFiles = files.map((f) => ({
2736
- file: f,
2737
- metadata: metadataObj,
2738
- conflictPolicy: policy,
2739
- }));
2740
- this.api.bulkUpload(this.baseUrl, bulkFiles, {
2741
- optionsJson,
2742
- failFastMode: this.config?.limits?.failFast,
2743
- retryCount: this.config?.bulk?.retryCount,
2744
- }).subscribe({
2745
- next: (event) => {
2746
- if (event.type === HttpEventType.Response) {
2747
- const resp = event.body;
2748
- this.bulkComplete.emit(resp);
2749
- // Preenche estados ricos
2750
- this.lastResults = resp?.results ?? null;
2751
- this.lastStats = resp?.stats ?? null;
2752
- this.finishUpload(true, undefined, resp.results);
2753
- }
2754
- else {
2755
- this.handleProgress(event);
2756
- }
2757
- },
2758
- error: (err) => this.handleError(err),
2759
- });
3622
+ this.uploadMultipleFiles(files, strategy, metadataObj, optionsJson);
2760
3623
  }
2761
3624
  else if (strategy === 'presign' || strategy === 'auto') {
2762
3625
  const file = files[0];
@@ -2768,20 +3631,29 @@ class PraxisFilesUpload {
2768
3631
  });
2769
3632
  return;
2770
3633
  }
2771
- this.api.presign(this.baseUrl, file.name).subscribe({
3634
+ const api = this.requireApiClient();
3635
+ const baseUrl = this.requireBaseUrl();
3636
+ api.presign(baseUrl, file.name).subscribe({
2772
3637
  next: (target) => {
2773
- this.presign.upload(target, file, this.config?.bulk?.retryCount ?? 0).subscribe({
3638
+ const uploadOptions = {
3639
+ optionsJson,
3640
+ metadata: metadataObj,
3641
+ conflictPolicy: this.conflictPolicy.value || 'RENAME',
3642
+ };
3643
+ this.presign.upload(target, file, uploadOptions, this.config?.bulk?.retryCount ?? 0).subscribe({
2774
3644
  next: (event) => {
2775
3645
  if (event.type === HttpEventType.Response) {
2776
- this.finishUpload(true, {
2777
- id: '',
2778
- fileName: file.name,
2779
- contentType: file.type,
2780
- fileSize: file.size,
2781
- uploadedAt: new Date().toISOString(),
2782
- tenantId: '',
2783
- scanStatus: ScanStatus.PENDING,
2784
- });
3646
+ const responseEvent = event;
3647
+ const response = responseEvent.body instanceof HttpResponse
3648
+ ? responseEvent.body.body
3649
+ : responseEvent.body;
3650
+ const meta = this.mapUploadResponseToFileMetadata(response);
3651
+ if (meta?.id) {
3652
+ this.finishUpload(true, meta);
3653
+ }
3654
+ else {
3655
+ this.handleError(new Error('Presigned upload completed without file metadata.'));
3656
+ }
2785
3657
  }
2786
3658
  else {
2787
3659
  this.handleProgress(event);
@@ -2824,11 +3696,77 @@ class PraxisFilesUpload {
2824
3696
  }
2825
3697
  }
2826
3698
  directUpload(file, options) {
2827
- this.api.upload(this.baseUrl, file, options).subscribe({
3699
+ const api = this.requireApiClient();
3700
+ const baseUrl = this.requireBaseUrl();
3701
+ api.upload(baseUrl, file, options).subscribe({
2828
3702
  next: (event) => {
2829
3703
  if (event.type === HttpEventType.Response) {
2830
3704
  const resp = event.body;
2831
- this.finishUpload(true, resp.file);
3705
+ const meta = this.mapUploadResponseToFileMetadata(resp);
3706
+ this.finishUpload(true, meta);
3707
+ }
3708
+ else {
3709
+ this.handleProgress(event);
3710
+ }
3711
+ },
3712
+ error: (err) => this.handleError(err),
3713
+ });
3714
+ }
3715
+ uploadMultipleFiles(files, strategy, metadataObj, optionsJson) {
3716
+ if (strategy === 'direct') {
3717
+ this.uploadMultipleFilesDirect(files, optionsJson, metadataObj);
3718
+ return;
3719
+ }
3720
+ from(files)
3721
+ .pipe(concatMap((file) => this.uploadSingleFileByStrategy(file, strategy, metadataObj, optionsJson)), toArray())
3722
+ .subscribe({
3723
+ next: (results) => {
3724
+ const responseData = this.buildBulkResponseData(results);
3725
+ this.bulkComplete.emit(responseData);
3726
+ this.lastResults = results.length ? results : null;
3727
+ this.lastStats = {
3728
+ total: responseData.totalProcessed,
3729
+ succeeded: responseData.totalSuccess,
3730
+ failed: responseData.totalFailed,
3731
+ };
3732
+ if (responseData.totalSuccess === 0 && responseData.totalFailed > 0) {
3733
+ const firstError = results.find((result) => result.error)?.error;
3734
+ const mappedError = firstError ? this.mapErrorFull(firstError) : null;
3735
+ this.errorMessage =
3736
+ mappedError?.message ||
3737
+ this.t('genericUploadError', 'Erro no envio de arquivo.');
3738
+ }
3739
+ this.finishUpload(true, undefined, results);
3740
+ },
3741
+ });
3742
+ }
3743
+ uploadMultipleFilesDirect(files, optionsJson, metadataObj) {
3744
+ const api = this.requireApiClient();
3745
+ const baseUrl = this.requireBaseUrl();
3746
+ const policy = this.conflictPolicy.value || 'RENAME';
3747
+ const bulkFiles = files.map((f) => ({
3748
+ file: f,
3749
+ metadata: metadataObj,
3750
+ conflictPolicy: policy,
3751
+ }));
3752
+ api.bulkUpload(baseUrl, bulkFiles, {
3753
+ optionsJson,
3754
+ failFast: this.config?.bulk?.failFast,
3755
+ retryCount: this.config?.bulk?.retryCount,
3756
+ }).subscribe({
3757
+ next: (event) => {
3758
+ if (event.type === HttpEventType.Response) {
3759
+ const responseData = event.body.data;
3760
+ this.bulkComplete.emit(responseData);
3761
+ const results = responseData.results;
3762
+ const stats = {
3763
+ total: responseData.totalProcessed,
3764
+ succeeded: responseData.totalSuccess,
3765
+ failed: responseData.totalFailed,
3766
+ };
3767
+ this.lastResults = results.length ? results : null;
3768
+ this.lastStats = stats;
3769
+ this.finishUpload(true, undefined, results);
2832
3770
  }
2833
3771
  else {
2834
3772
  this.handleProgress(event);
@@ -2837,17 +3775,132 @@ class PraxisFilesUpload {
2837
3775
  error: (err) => this.handleError(err),
2838
3776
  });
2839
3777
  }
3778
+ uploadSingleFileByStrategy(file, strategy, metadataObj, optionsJson) {
3779
+ if (!this.presign) {
3780
+ return this.uploadSingleFileDirectResult(file, metadataObj, optionsJson);
3781
+ }
3782
+ const api = this.requireApiClient();
3783
+ const baseUrl = this.requireBaseUrl();
3784
+ const uploadOptions = {
3785
+ optionsJson,
3786
+ metadata: metadataObj,
3787
+ conflictPolicy: this.conflictPolicy.value || 'RENAME',
3788
+ retryCount: this.config?.bulk?.retryCount,
3789
+ };
3790
+ return api
3791
+ .presign(baseUrl, file.name)
3792
+ .pipe(concatMap((target) => this.presign.upload(target, file, uploadOptions, this.config?.bulk?.retryCount ?? 0)), tap((event) => {
3793
+ if (event.type !== HttpEventType.Response) {
3794
+ this.handleProgress(event);
3795
+ }
3796
+ }), filter((event) => event.type === HttpEventType.Response), take(1), map((event) => {
3797
+ const responseEvent = event;
3798
+ const meta = this.mapUploadResponseToFileMetadata(responseEvent.body instanceof HttpResponse
3799
+ ? responseEvent.body.body
3800
+ : responseEvent.body);
3801
+ if (!meta?.id) {
3802
+ return this.buildBulkFailureResult(file, this.createOperationalError('PRESIGN_METADATA_MISSING', this.t('presignMetadataMissing', 'Upload assinado concluído sem metadados do arquivo.')));
3803
+ }
3804
+ return this.buildBulkSuccessResult(file, meta);
3805
+ }), catchError((err) => {
3806
+ if (strategy === 'auto') {
3807
+ return this.uploadSingleFileDirectResult(file, metadataObj, optionsJson);
3808
+ }
3809
+ return of(this.buildBulkFailureResult(file, this.normalizeBulkError(err)));
3810
+ }), defaultIfEmpty(this.buildBulkFailureResult(file, this.createOperationalError('UPLOAD_RESPONSE_MISSING', this.t('genericUploadError', 'Erro no envio de arquivo.')))));
3811
+ }
3812
+ uploadSingleFileDirectResult(file, metadataObj, optionsJson) {
3813
+ const api = this.requireApiClient();
3814
+ const baseUrl = this.requireBaseUrl();
3815
+ return api
3816
+ .upload(baseUrl, file, {
3817
+ optionsJson,
3818
+ retryCount: this.config?.bulk?.retryCount,
3819
+ metadata: metadataObj,
3820
+ conflictPolicy: this.conflictPolicy.value || 'RENAME',
3821
+ })
3822
+ .pipe(tap((event) => {
3823
+ if (event.type !== HttpEventType.Response) {
3824
+ this.handleProgress(event);
3825
+ }
3826
+ }), filter((event) => event.type === HttpEventType.Response), take(1), map((event) => {
3827
+ const responseEvent = event;
3828
+ const meta = this.mapUploadResponseToFileMetadata(responseEvent.body);
3829
+ if (!meta?.id) {
3830
+ return this.buildBulkFailureResult(file, this.createOperationalError('UPLOAD_RESPONSE_MISSING', this.t('genericUploadError', 'Erro no envio de arquivo.')));
3831
+ }
3832
+ return this.buildBulkSuccessResult(file, meta);
3833
+ }), catchError((err) => of(this.buildBulkFailureResult(file, this.normalizeBulkError(err)))), defaultIfEmpty(this.buildBulkFailureResult(file, this.createOperationalError('UPLOAD_RESPONSE_MISSING', this.t('genericUploadError', 'Erro no envio de arquivo.')))));
3834
+ }
3835
+ buildBulkResponseData(results) {
3836
+ const totalSuccess = results.filter((result) => result.status === BulkUploadResultStatus.SUCCESS).length;
3837
+ const totalFailed = results.length - totalSuccess;
3838
+ return {
3839
+ totalProcessed: results.length,
3840
+ totalSuccess,
3841
+ totalFailed,
3842
+ overallSuccess: totalFailed === 0,
3843
+ results,
3844
+ };
3845
+ }
3846
+ buildBulkSuccessResult(file, meta) {
3847
+ return {
3848
+ fileName: file.name,
3849
+ status: BulkUploadResultStatus.SUCCESS,
3850
+ file: meta,
3851
+ };
3852
+ }
3853
+ buildBulkFailureResult(file, error) {
3854
+ return {
3855
+ fileName: file.name,
3856
+ status: BulkUploadResultStatus.FAILED,
3857
+ error,
3858
+ };
3859
+ }
3860
+ normalizeBulkError(err) {
3861
+ if (err instanceof HttpErrorResponse && err.error) {
3862
+ return err.error;
3863
+ }
3864
+ return this.createOperationalError('UPLOAD_OPERATION_FAILED', this.t('genericUploadError', 'Erro no envio de arquivo.'), err);
3865
+ }
3866
+ createOperationalError(code, message, details) {
3867
+ return {
3868
+ code,
3869
+ message,
3870
+ details,
3871
+ };
3872
+ }
3873
+ normalizeErrorEnvelope(err) {
3874
+ if (err &&
3875
+ typeof err === 'object' &&
3876
+ 'status' in err &&
3877
+ 'message' in err &&
3878
+ 'errors' in err) {
3879
+ return err;
3880
+ }
3881
+ const message = err instanceof Error && err.message
3882
+ ? err.message
3883
+ : this.t('genericUploadError', 'Erro no envio de arquivo.');
3884
+ return {
3885
+ status: 'failure',
3886
+ message,
3887
+ errors: [
3888
+ this.createOperationalError('UPLOAD_OPERATION_FAILED', message, err),
3889
+ ],
3890
+ timestamp: new Date().toISOString(),
3891
+ };
3892
+ }
2840
3893
  buildOptionsJson(customMeta) {
2841
3894
  const effective = this.serverEffective?.options;
2842
3895
  const nameConflictPolicy = this.conflictPolicy.value ||
2843
- this.config?.limits?.defaultConflictPolicy ||
3896
+ this.config?.options?.defaultConflictPolicy ||
2844
3897
  effective?.nameConflictPolicy ||
2845
3898
  'RENAME';
2846
3899
  const limits = this.config?.limits ?? {};
2847
3900
  const advanced = this.config?.options ?? {};
2848
3901
  // Garantir maxUploadSizeMb positivo usando fallback do servidor ou default 50
2849
- const maxUploadSizeMb = typeof limits.maxUploadSizeMb === 'number'
2850
- ? limits.maxUploadSizeMb
3902
+ const maxUploadSizeMb = typeof advanced.maxUploadSizeMb === 'number'
3903
+ ? advanced.maxUploadSizeMb
2851
3904
  : typeof effective?.maxUploadSizeMb === 'number'
2852
3905
  ? effective?.maxUploadSizeMb
2853
3906
  : 50;
@@ -2856,8 +3909,8 @@ class PraxisFilesUpload {
2856
3909
  options.maxUploadSizeMb = maxUploadSizeMb;
2857
3910
  }
2858
3911
  // strictValidation: fallback para server
2859
- if (typeof limits.strictValidation === 'boolean') {
2860
- options.strictValidation = limits.strictValidation;
3912
+ if (typeof advanced.strictValidation === 'boolean') {
3913
+ options.strictValidation = advanced.strictValidation;
2861
3914
  }
2862
3915
  else if (typeof effective?.strictValidation === 'boolean') {
2863
3916
  options.strictValidation = effective.strictValidation;
@@ -2926,8 +3979,8 @@ class PraxisFilesUpload {
2926
3979
  this.currentFiles = [];
2927
3980
  }
2928
3981
  if (err instanceof HttpErrorResponse && err.error) {
2929
- const mapped = this.errors?.map(err.error, err.headers) ||
2930
- err.error;
3982
+ const errorPayload = err.error;
3983
+ const mapped = this.errors?.map(errorPayload, err.headers) || err.error;
2931
3984
  this.errorMessage =
2932
3985
  mapped.message ||
2933
3986
  this.t('genericUploadError', 'Erro no envio de arquivo.');
@@ -2935,17 +3988,17 @@ class PraxisFilesUpload {
2935
3988
  this.rateLimit = mapped.rateLimit;
2936
3989
  this.rateLimited.emit(mapped.rateLimit);
2937
3990
  }
2938
- const code = String(err.error.code || '');
3991
+ const primary = getPrimaryErrorItem(errorPayload);
3992
+ const code = String(primary?.code || '');
2939
3993
  if (code === 'QUOTA_EXCEEDED' || code === 'COTA_EXCEDIDA') {
2940
3994
  this.quotaExceeded = true;
2941
3995
  }
2942
- this.error.emit(err.error);
2943
- this.uploadError.emit(err.error);
3996
+ this.error.emit(errorPayload);
2944
3997
  }
2945
3998
  else {
2946
- this.errorMessage = this.t('genericUploadError', 'Erro no envio de arquivo.');
2947
- this.error.emit(err);
2948
- this.uploadError.emit(err);
3999
+ const normalizedError = this.normalizeErrorEnvelope(err);
4000
+ this.errorMessage = normalizedError.message;
4001
+ this.error.emit(normalizedError);
2949
4002
  }
2950
4003
  }
2951
4004
  finishUpload(success, meta, results) {
@@ -2954,8 +4007,7 @@ class PraxisFilesUpload {
2954
4007
  if (success) {
2955
4008
  if (meta) {
2956
4009
  this.uploadedFiles.push({ fileName: meta.fileName, success: true });
2957
- this.uploadSuccess.emit({ file: meta });
2958
- // Opcional: preencher lista rica também para single
4010
+ this.uploadSuccess.emit(meta);
2959
4011
  this.lastResults = [
2960
4012
  {
2961
4013
  fileName: meta.fileName,
@@ -2966,7 +4018,6 @@ class PraxisFilesUpload {
2966
4018
  this.lastStats = { total: 1, succeeded: 1, failed: 0 };
2967
4019
  }
2968
4020
  if (results) {
2969
- // Mantém lista simples para compatibilidade
2970
4021
  for (const r of results) {
2971
4022
  if (r.status === BulkUploadResultStatus.SUCCESS) {
2972
4023
  this.uploadedFiles.push({ fileName: r.fileName, success: true });
@@ -2975,7 +4026,6 @@ class PraxisFilesUpload {
2975
4026
  this.uploadedFiles.push({ fileName: r.fileName, success: false });
2976
4027
  }
2977
4028
  }
2978
- // Se stats não vieram do servidor, calcula minimamente
2979
4029
  if (!this.lastStats) {
2980
4030
  const succeeded = results.filter((r) => r.status === BulkUploadResultStatus.SUCCESS).length;
2981
4031
  const failed = results.length - succeeded;
@@ -2996,6 +4046,31 @@ class PraxisFilesUpload {
2996
4046
  this.hideSelectedOverlay();
2997
4047
  this.cdr.markForCheck();
2998
4048
  }
4049
+ mapUploadResponseToFileMetadata(resp) {
4050
+ const payload = resp?.data;
4051
+ if (!payload) {
4052
+ return undefined;
4053
+ }
4054
+ return {
4055
+ id: payload.fileId || '',
4056
+ fileName: payload.originalFilename,
4057
+ contentType: payload.mimeType || '',
4058
+ fileSize: payload.fileSize,
4059
+ uploadedAt: payload.uploadTimestamp,
4060
+ tenantId: '',
4061
+ scanStatus: ScanStatus.PENDING,
4062
+ };
4063
+ }
4064
+ primaryErrorCode(error) {
4065
+ return getPrimaryErrorItem(error)?.code ?? 'UNKNOWN_ERROR';
4066
+ }
4067
+ traceIdOf(error) {
4068
+ if (!error || typeof error !== 'object' || !('traceId' in error)) {
4069
+ return undefined;
4070
+ }
4071
+ const traceId = error.traceId;
4072
+ return typeof traceId === 'string' ? traceId : undefined;
4073
+ }
2999
4074
  // Lista rica - helpers/ações
3000
4075
  get visibleResults() {
3001
4076
  if (!this.lastResults)
@@ -3104,7 +4179,6 @@ class PraxisFilesUpload {
3104
4179
  this.t('maxBulkSizeError', 'Tamanho total dos arquivos excedido.');
3105
4180
  }
3106
4181
  this.error.emit(errors);
3107
- this.uploadError.emit(errors);
3108
4182
  return false;
3109
4183
  }
3110
4184
  return true;
@@ -3113,12 +4187,12 @@ class PraxisFilesUpload {
3113
4187
  get policiesSummary() {
3114
4188
  const acc = this.config?.ui?.accept?.length
3115
4189
  ? this.config.ui.accept.join(', ')
3116
- : this.acceptString || 'qualquer';
4190
+ : this.acceptString || this.t('policySummaryAny', 'qualquer');
3117
4191
  const maxFile = this.config?.limits?.maxFileSizeBytes
3118
4192
  ? this.formatBytes(this.config.limits.maxFileSizeBytes)
3119
- : '—';
4193
+ : this.t('policySummaryNotConfigured', '—');
3120
4194
  const maxCount = this.config?.limits?.maxFilesPerBulk ?? (this.allowMultiple ? 99 : 1);
3121
- return `Tipos: ${acc} • Máx. por arquivo: ${maxFile} • Qtde: ${maxCount}`;
4195
+ return `${this.t('policySummaryTypesLabel', 'Tipos')}: ${acc} • ${this.t('policySummaryMaxPerFileLabel', 'Máx. por arquivo')}: ${maxFile} • ${this.t('policySummaryQuantityLabel', 'Qtde')}: ${maxCount}`;
3122
4196
  }
3123
4197
  resolveTemplateString(value) {
3124
4198
  if (!this.context || !value.includes('${'))
@@ -3137,29 +4211,37 @@ class PraxisFilesUpload {
3137
4211
  return acc[key];
3138
4212
  }, context);
3139
4213
  }
3140
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisFilesUpload, deps: [{ token: i1$2.SettingsPanelService }, { token: DragProximityService }, { token: FILES_UPLOAD_TEXTS }, { token: i3.Overlay }, { token: i0.ViewContainerRef }, { token: i0.ChangeDetectorRef }, { token: ASYNC_CONFIG_STORAGE }, { token: FilesApiClient, optional: true }, { token: PresignedUploaderService, optional: true }, { token: ErrorMapperService, optional: true }, { token: TRANSLATE_LIKE, optional: true }], target: i0.ɵɵFactoryTarget.Component });
3141
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisFilesUpload, isStandalone: true, selector: "praxis-files-upload", inputs: { config: "config", filesUploadId: "filesUploadId", uploadId: "uploadId", componentInstanceId: "componentInstanceId", baseUrl: "baseUrl", displayMode: "displayMode", context: "context", editModeEnabled: "editModeEnabled" }, outputs: { uploadSuccess: "uploadSuccess", bulkComplete: "bulkComplete", error: "error", rateLimited: "rateLimited", uploadStart: "uploadStart", uploadProgress: "uploadProgress", uploadError: "uploadError", retry: "retry", download: "download", copyLink: "copyLink", detailsOpened: "detailsOpened", detailsClosed: "detailsClosed", pendingFilesChange: "pendingFilesChange", proximityChange: "proximityChange" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }, { propertyName: "hostRef", first: true, predicate: ["host"], descendants: true, static: true }, { propertyName: "overlayTmpl", first: true, predicate: ["proximityOverlayTmpl"], descendants: true }, { propertyName: "selectedOverlayTmpl", first: true, predicate: ["selectedOverlayTmpl"], descendants: true }], ngImport: i0, template: `
3142
- <div class="praxis-files-upload" [class.dense]="config?.ui?.dense" #host>
4214
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisFilesUpload, deps: [{ token: i1$3.SettingsPanelService }, { token: DragProximityService }, { token: i3.Overlay }, { token: i0.ViewContainerRef }, { token: i0.ChangeDetectorRef }, { token: ASYNC_CONFIG_STORAGE }, { token: i1$2.PraxisI18nService }, { token: FILES_UPLOAD_TEXTS, optional: true }, { token: TRANSLATE_LIKE, optional: true }, { token: ConfigService, optional: true }, { token: FilesApiClient, optional: true }, { token: PresignedUploaderService, optional: true }, { token: ErrorMapperService, optional: true }], target: i0.ɵɵFactoryTarget.Component });
4215
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisFilesUpload, isStandalone: true, selector: "praxis-files-upload", inputs: { config: "config", filesUploadId: "filesUploadId", componentInstanceId: "componentInstanceId", baseUrl: "baseUrl", displayMode: "displayMode", context: "context", editModeEnabled: "editModeEnabled" }, outputs: { uploadSuccess: "uploadSuccess", bulkComplete: "bulkComplete", error: "error", rateLimited: "rateLimited", uploadStart: "uploadStart", uploadProgress: "uploadProgress", retry: "retry", download: "download", copyLink: "copyLink", detailsOpened: "detailsOpened", detailsClosed: "detailsClosed", pendingStateChange: "pendingStateChange", proximityChange: "proximityChange", readinessChange: "readinessChange" }, providers: [providePraxisFilesUploadI18n()], viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }, { propertyName: "hostRef", first: true, predicate: ["host"], descendants: true, static: true }, { propertyName: "overlayTmpl", first: true, predicate: ["proximityOverlayTmpl"], descendants: true }, { propertyName: "selectedOverlayTmpl", first: true, predicate: ["selectedOverlayTmpl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
4216
+ <div
4217
+ class="praxis-files-upload"
4218
+ [class.dense]="config?.ui?.dense"
4219
+ [attr.data-files-upload-id]="a11yBaseId"
4220
+ [attr.data-ready-state]="readinessState"
4221
+ [attr.data-ready-source]="readinessSource"
4222
+ [attr.aria-busy]="readinessState === 'loading' ? 'true' : 'false'"
4223
+ #host
4224
+ >
3143
4225
  <!-- Proximity overlay inline (fallback quando expandMode === 'inline') -->
3144
- <div
3145
- class="proximity-overlay"
4226
+ <div
4227
+ class="proximity-overlay"
3146
4228
  *ngIf="
3147
4229
  showProximityOverlay &&
3148
4230
  (config?.ui?.dropzone?.expandMode ?? 'overlay') === 'inline'
3149
4231
  "
3150
4232
  [style.height.px]="config?.ui?.dropzone?.expandHeight ?? 200"
3151
4233
  aria-hidden="true"
3152
- >
3153
- <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3154
- <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3155
- <p class="dz-hint">Solte aqui para enviar</p>
3156
- </div>
4234
+ >
4235
+ <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
4236
+ <p class="dz-title">{{ texts.dropzoneLabel }}</p>
4237
+ <p class="dz-hint">{{ t('dropzoneProximityHint', 'Solte aqui para enviar') }}</p>
4238
+ </div>
3157
4239
  <!-- Template do overlay de proximidade (renderizado via CDK Overlay) -->
3158
4240
  <ng-template #proximityOverlayTmpl>
3159
4241
  <div class="proximity-overlay" aria-hidden="true">
3160
4242
  <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3161
4243
  <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3162
- <p class="dz-hint">Solte aqui para enviar</p>
4244
+ <p class="dz-hint">{{ t('dropzoneProximityHint', 'Solte aqui para enviar') }}</p>
3163
4245
  </div>
3164
4246
  </ng-template>
3165
4247
 
@@ -3168,22 +4250,22 @@ class PraxisFilesUpload {
3168
4250
  <ng-template #selectedOverlayTmpl>
3169
4251
  <div
3170
4252
  class="selected-overlay"
3171
- role="dialog"
3172
- aria-label="Arquivos selecionados"
4253
+ role="region"
4254
+ [attr.aria-labelledby]="selectedOverlayTitleId"
3173
4255
  >
3174
4256
  <header class="sel-header">
3175
- <span class="title">Selecionados ({{ pendingCount }})</span>
4257
+ <span class="title" [id]="selectedOverlayTitleId">{{ t('selectedOverlayTitle', 'Selecionados') }} ({{ pendingCount }})</span>
3176
4258
  <span class="spacer"></span>
3177
4259
  <button
3178
4260
  mat-stroked-button
3179
4261
  color="primary"
3180
- (click)="triggerUpload()"
4262
+ (click)="submitPendingFiles()"
3181
4263
  [disabled]="!baseUrl || isUploading"
3182
4264
  >
3183
- Enviar
4265
+ {{ t('fieldActionUploadShort', 'Enviar') }}
3184
4266
  </button>
3185
- <button mat-button type="button" (click)="clearSelection()">
3186
- Cancelar
4267
+ <button mat-button type="button" (click)="resetPendingFiles()">
4268
+ {{ t('fieldActionCancelShort', 'Cancelar') }}
3187
4269
  </button>
3188
4270
  </header>
3189
4271
  <ul class="sel-list">
@@ -3204,7 +4286,7 @@ class PraxisFilesUpload {
3204
4286
  class="remove-btn"
3205
4287
  [matTooltip]="t('remove', 'Remover')"
3206
4288
  (click)="removePendingFile(f)"
3207
- [attr.aria-label]="t('remove', 'Remover')"
4289
+ [attr.aria-label]="pendingFileRemoveAriaLabel(f)"
3208
4290
  >
3209
4291
  <mat-icon [praxisIcon]="'close'"></mat-icon>
3210
4292
  </button>
@@ -3215,7 +4297,7 @@ class PraxisFilesUpload {
3215
4297
  [matMenuTriggerData]="{ file: f }"
3216
4298
  aria-haspopup="menu"
3217
4299
  [matTooltip]="t('moreActions', 'Mais ações')"
3218
- [attr.aria-label]="t('moreActions', 'Mais ações')"
4300
+ [attr.aria-label]="pendingFileMoreActionsAriaLabel(f)"
3219
4301
  >
3220
4302
  <mat-icon [praxisIcon]="'more_horiz'"></mat-icon>
3221
4303
  </button>
@@ -3256,6 +4338,9 @@ class PraxisFilesUpload {
3256
4338
  <div
3257
4339
  class="rate-limit-banner"
3258
4340
  *ngIf="rateLimit && config?.rateLimit?.showBannerOn429 !== false"
4341
+ [id]="messageRegionId"
4342
+ role="alert"
4343
+ aria-live="assertive"
3259
4344
  >
3260
4345
  {{ texts.rateLimitBanner }}
3261
4346
  {{ rateLimit.resetEpochSeconds * 1000 | date: 'shortTime' }}.
@@ -3263,32 +4348,34 @@ class PraxisFilesUpload {
3263
4348
  <div
3264
4349
  class="quota-banner"
3265
4350
  *ngIf="quotaExceeded && config?.quotas?.showQuotaWarnings !== false"
4351
+ [id]="messageRegionId"
4352
+ role="status"
4353
+ aria-live="polite"
3266
4354
  >
3267
4355
  {{ errorMessage }}
3268
4356
  </div>
3269
- <div class="error" *ngIf="errorMessage && !quotaExceeded">
4357
+ <div
4358
+ class="error"
4359
+ *ngIf="errorMessage && !quotaExceeded"
4360
+ [id]="messageRegionId"
4361
+ role="alert"
4362
+ aria-live="assertive"
4363
+ >
3270
4364
  {{ errorMessage }}
3271
4365
  </div>
3272
- <div class="error" *ngIf="!baseUrl">
3273
- Base URL não configurada. Informe [baseUrl] para habilitar o envio.
4366
+ <div
4367
+ class="error"
4368
+ *ngIf="!baseUrl"
4369
+ [id]="messageRegionId"
4370
+ role="alert"
4371
+ aria-live="assertive"
4372
+ >
4373
+ {{ t('baseUrlMissing', 'Base URL not configured. Provide [baseUrl] to enable uploads.') }}
3274
4374
  </div>
3275
4375
  <div
3276
4376
  class="dropzone"
3277
4377
  *ngIf="config?.ui?.showDropzone !== false"
3278
- [attr.role]="
3279
- !baseUrl ||
3280
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3281
- isUploading
3282
- ? null
3283
- : 'button'
3284
- "
3285
- [attr.tabindex]="
3286
- !baseUrl ||
3287
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3288
- isUploading
3289
- ? -1
3290
- : 0
3291
- "
4378
+ role="group"
3292
4379
  [class.dragover]="isDragging"
3293
4380
  [class.disabled]="
3294
4381
  !baseUrl ||
@@ -3302,39 +4389,18 @@ class PraxisFilesUpload {
3302
4389
  ? 'true'
3303
4390
  : 'false'
3304
4391
  "
3305
- [attr.aria-label]="texts.dropzoneLabel + ' ' + texts.dropzoneButton"
3306
- (click)="
3307
- !baseUrl ||
3308
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3309
- isUploading
3310
- ? null
3311
- : openFile(fileInput)
3312
- "
4392
+ [attr.aria-label]="texts.dropzoneLabel"
4393
+ [attr.aria-describedby]="messageRegionId"
3313
4394
  (drop)="onDrop($event)"
3314
4395
  (dragover)="onDragOver($event)"
3315
4396
  (dragleave)="onDragLeave($event)"
3316
- (keydown.enter)="
3317
- $event.preventDefault();
3318
- !baseUrl ||
3319
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3320
- isUploading
3321
- ? null
3322
- : openFile(fileInput)
3323
- "
3324
- (keydown.space)="
3325
- $event.preventDefault();
3326
- !baseUrl ||
3327
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3328
- isUploading
3329
- ? null
3330
- : openFile(fileInput)
3331
- "
3332
4397
  >
3333
4398
  <!-- Ações internas (ícones) -->
3334
4399
  <div class="field-actions" aria-hidden="false">
3335
4400
  <button
3336
4401
  mat-icon-button
3337
- [matTooltip]="'Selecionar arquivo(s)'"
4402
+ [matTooltip]="t('selectFiles', 'Select file(s)')"
4403
+ [attr.aria-label]="selectFilesAriaLabel"
3338
4404
  (click)="$event.stopPropagation(); openFile(fileInput)"
3339
4405
  [disabled]="!baseUrl || isUploading"
3340
4406
  >
@@ -3343,6 +4409,7 @@ class PraxisFilesUpload {
3343
4409
  <button
3344
4410
  mat-icon-button
3345
4411
  [matTooltip]="policiesSummary"
4412
+ [attr.aria-label]="policySummaryAriaLabel"
3346
4413
  (click)="$event.stopPropagation()"
3347
4414
  [disabled]="false"
3348
4415
  >
@@ -3353,9 +4420,15 @@ class PraxisFilesUpload {
3353
4420
  <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3354
4421
  <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3355
4422
  <p class="dz-hint">
3356
- Arraste e solte arquivos aqui ou clique para selecionar
4423
+ {{ t('dropzoneHint', 'Arraste e solte arquivos aqui ou clique para selecionar') }}
3357
4424
  </p>
3358
- <button type="button" mat-stroked-button (click)="openFile(fileInput)">
4425
+ <button
4426
+ type="button"
4427
+ mat-stroked-button
4428
+ [disabled]="!baseUrl || isUploading"
4429
+ [attr.aria-describedby]="messageRegionId"
4430
+ (click)="openFile(fileInput)"
4431
+ >
3359
4432
  {{ texts.dropzoneButton }}
3360
4433
  </button>
3361
4434
  </div>
@@ -3375,8 +4448,8 @@ class PraxisFilesUpload {
3375
4448
  class="conflict-policy"
3376
4449
  *ngIf="config?.ui?.showConflictPolicySelector"
3377
4450
  >
3378
- <label for="conflictPolicy">{{ texts.conflictPolicyLabel }}</label>
3379
- <select id="conflictPolicy" [formControl]="conflictPolicy">
4451
+ <label [for]="conflictPolicyInputId">{{ texts.conflictPolicyLabel }}</label>
4452
+ <select [id]="conflictPolicyInputId" [formControl]="conflictPolicy">
3380
4453
  <option *ngFor="let p of conflictPolicies" [value]="p">
3381
4454
  {{ p }}
3382
4455
  </option>
@@ -3384,18 +4457,27 @@ class PraxisFilesUpload {
3384
4457
  </div>
3385
4458
 
3386
4459
  <div class="metadata" *ngIf="config?.ui?.showMetadataForm">
3387
- <label for="metadata">{{ texts.metadataLabel }}</label>
3388
- <textarea id="metadata" [formControl]="metadata"></textarea>
4460
+ <label [for]="metadataInputId">{{ texts.metadataLabel }}</label>
4461
+ <textarea [id]="metadataInputId" [formControl]="metadata"></textarea>
3389
4462
  <div class="error" *ngIf="metadataError">{{ metadataError }}</div>
3390
4463
  </div>
3391
4464
 
3392
- <div class="progress" *ngIf="showProgress">
4465
+ <div
4466
+ class="progress"
4467
+ *ngIf="showProgress"
4468
+ [id]="progressRegionId"
4469
+ role="status"
4470
+ aria-live="polite"
4471
+ >
3393
4472
  <mat-progress-bar
3394
4473
  mode="determinate"
3395
4474
  [value]="uploadProgressValue"
3396
4475
  [attr.aria-label]="texts.progressAriaLabel"
4476
+ [attr.aria-valuemin]="0"
4477
+ [attr.aria-valuemax]="100"
4478
+ [attr.aria-valuenow]="uploadProgressValue"
3397
4479
  ></mat-progress-bar>
3398
- <span class="sr-only">{{ uploadProgressValue }}%</span>
4480
+ <span class="sr-only">{{ texts.progressAriaLabel }} {{ uploadProgressValue }}%</span>
3399
4481
  </div>
3400
4482
 
3401
4483
  <ul class="file-feedback" *ngIf="hasUploadedFiles">
@@ -3410,17 +4492,17 @@ class PraxisFilesUpload {
3410
4492
  <section class="bulk-results" *ngIf="hasLastResults">
3411
4493
  <header class="bulk-summary" *ngIf="lastStats">
3412
4494
  <div class="stat">
3413
- <span class="label">Total</span>
4495
+ <span class="label">{{ t('bulkSummaryTotal', 'Total') }}</span>
3414
4496
  <span class="value">{{ lastStats.total }}</span>
3415
4497
  </div>
3416
4498
  <div class="stat success">
3417
4499
  <mat-icon [praxisIcon]="'check_circle'"></mat-icon>
3418
- <span class="label">Sucesso</span>
4500
+ <span class="label">{{ t('bulkSummarySuccess', 'Sucesso') }}</span>
3419
4501
  <span class="value">{{ lastStats.succeeded }}</span>
3420
4502
  </div>
3421
4503
  <div class="stat failed">
3422
4504
  <mat-icon [praxisIcon]="'error'"></mat-icon>
3423
- <span class="label">Falhas</span>
4505
+ <span class="label">{{ t('bulkSummaryFailed', 'Falhas') }}</span>
3424
4506
  <span class="value">{{ lastStats.failed }}</span>
3425
4507
  </div>
3426
4508
  </header>
@@ -3451,19 +4533,24 @@ class PraxisFilesUpload {
3451
4533
  >
3452
4534
  </div>
3453
4535
  <div class="meta" *ngIf="r.file">
3454
- <span *ngIf="r.file.id">ID: {{ r.file.id }}</span>
4536
+ <span *ngIf="r.file.id"
4537
+ >{{ t('resultMetaIdLabel', 'ID') }}: {{ r.file.id }}</span
4538
+ >
3455
4539
  <span *ngIf="r.file.contentType"
3456
- >Tipo: {{ r.file.contentType }}</span
4540
+ >{{ t('resultMetaTypeLabel', 'Tipo') }}:
4541
+ {{ r.file.contentType }}</span
3457
4542
  >
3458
4543
  <span *ngIf="r.file.fileSize"
3459
- >Tamanho: {{ formatBytes(r.file.fileSize) }}</span
4544
+ >{{ t('resultMetaSizeLabel', 'Tamanho') }}:
4545
+ {{ formatBytes(r.file.fileSize) }}</span
3460
4546
  >
3461
4547
  <span *ngIf="r.file.uploadedAt"
3462
- >Enviado em: {{ r.file.uploadedAt | date: 'short' }}</span
4548
+ >{{ t('resultMetaUploadedAtLabel', 'Enviado em') }}:
4549
+ {{ r.file.uploadedAt | date: 'short' }}</span
3463
4550
  >
3464
4551
  </div>
3465
4552
  <div class="error" *ngIf="r.error as e">
3466
- <span class="code">{{ e.code }}</span>
4553
+ <span class="code">{{ primaryErrorCode(e) }}</span>
3467
4554
  <ng-container *ngIf="mapErrorFull(e) as me">
3468
4555
  <span class="title" *ngIf="me.title">{{ me.title }}:</span>
3469
4556
  <span class="msg">{{ me.message }}</span>
@@ -3471,13 +4558,13 @@ class PraxisFilesUpload {
3471
4558
  — {{ me.userAction }}</span
3472
4559
  >
3473
4560
  </ng-container>
3474
- <span class="trace" *ngIf="e.traceId"
3475
- >(trace {{ e.traceId }})</span
4561
+ <span class="trace" *ngIf="traceIdOf(e) as traceId"
4562
+ >(trace {{ traceId }})</span
3476
4563
  >
3477
4564
  </div>
3478
4565
  <div class="extra" *ngIf="r.file?.metadata as m">
3479
4566
  <details (toggle)="onDetailsToggle($event, r)">
3480
- <summary>Metadados</summary>
4567
+ <summary>{{ t('detailsMetadata', 'Metadados') }}</summary>
3481
4568
  <pre>{{ m | json }}</pre>
3482
4569
  </details>
3483
4570
  </div>
@@ -3485,14 +4572,16 @@ class PraxisFilesUpload {
3485
4572
  <div class="item-actions">
3486
4573
  <button
3487
4574
  mat-icon-button
3488
- [matTooltip]="'Detalhes'"
4575
+ [matTooltip]="t('details', 'Detalhes')"
4576
+ [attr.aria-label]="resultActionAriaLabel('details', 'Detalhes', r.fileName)"
3489
4577
  (click)="toggleDetailsFor(r)"
3490
4578
  >
3491
4579
  <mat-icon [praxisIcon]="'info'"></mat-icon>
3492
4580
  </button>
3493
4581
  <button
3494
4582
  mat-icon-button
3495
- [matTooltip]="'Reenviar'"
4583
+ [matTooltip]="t('retry', 'Reenviar')"
4584
+ [attr.aria-label]="resultActionAriaLabel('retry', 'Reenviar', r.fileName)"
3496
4585
  (click)="onRetry(r)"
3497
4586
  [disabled]="isUploading"
3498
4587
  >
@@ -3500,7 +4589,8 @@ class PraxisFilesUpload {
3500
4589
  </button>
3501
4590
  <button
3502
4591
  mat-icon-button
3503
- [matTooltip]="'Baixar'"
4592
+ [matTooltip]="t('download', 'Baixar')"
4593
+ [attr.aria-label]="resultActionAriaLabel('download', 'Baixar', r.fileName)"
3504
4594
  (click)="onDownload(r)"
3505
4595
  [disabled]="r.status !== BulkUploadResultStatus.SUCCESS"
3506
4596
  >
@@ -3508,7 +4598,8 @@ class PraxisFilesUpload {
3508
4598
  </button>
3509
4599
  <button
3510
4600
  mat-icon-button
3511
- [matTooltip]="'Copiar link'"
4601
+ [matTooltip]="t('copyLink', 'Copiar link')"
4602
+ [attr.aria-label]="resultActionAriaLabel('copyLink', 'Copiar link', r.fileName)"
3512
4603
  (click)="onCopyLink(r)"
3513
4604
  [disabled]="r.status !== BulkUploadResultStatus.SUCCESS"
3514
4605
  >
@@ -3516,7 +4607,8 @@ class PraxisFilesUpload {
3516
4607
  </button>
3517
4608
  <button
3518
4609
  mat-icon-button
3519
- [matTooltip]="'Remover'"
4610
+ [matTooltip]="t('remove', 'Remover')"
4611
+ [attr.aria-label]="resultActionAriaLabel('remove', 'Remover', r.fileName)"
3520
4612
  (click)="removeResult(r)"
3521
4613
  >
3522
4614
  <mat-icon [praxisIcon]="'close'"></mat-icon>
@@ -3533,7 +4625,7 @@ class PraxisFilesUpload {
3533
4625
  "
3534
4626
  >
3535
4627
  <button mat-button type="button" (click)="showAllResults = true">
3536
- Ver todos
4628
+ {{ t('showAll', 'Ver todos') }}
3537
4629
  </button>
3538
4630
  </div>
3539
4631
  <div
@@ -3546,18 +4638,18 @@ class PraxisFilesUpload {
3546
4638
  "
3547
4639
  >
3548
4640
  <button mat-button type="button" (click)="showAllResults = false">
3549
- Ver menos
4641
+ {{ t('showLess', 'Ver menos') }}
3550
4642
  </button>
3551
4643
  </div>
3552
4644
  </section>
3553
4645
 
3554
4646
  <ng-content></ng-content>
3555
4647
  </div>
3556
- `, isInline: true, styles: ["@charset \"UTF-8\";.praxis-files-upload{position:relative}.praxis-files-upload .settings-btn{position:absolute;top:.5rem;right:.5rem}.praxis-files-upload .rate-limit-banner,.praxis-files-upload .quota-banner,.praxis-files-upload .error{margin:.5rem 0;padding:.5rem 1rem;border-radius:var(--md-sys-shape-corner-small, 4px)}.praxis-files-upload .rate-limit-banner{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.praxis-files-upload .quota-banner{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.praxis-files-upload .dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;border:2px dashed var(--md-sys-color-outline);border-radius:var(--md-sys-shape-corner-medium, 4px);background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface);text-align:center;cursor:pointer;transition:background-color .2s;width:100%;min-height:160px;margin:.5rem 0;gap:.5rem;position:relative}.praxis-files-upload .dropzone:focus,.praxis-files-upload .dropzone:hover{outline:none;background:var(--md-sys-color-surface-container-high);border-color:var(--md-sys-color-primary)}.praxis-files-upload .dropzone.dragover{background:var(--md-sys-color-surface-container-high);border-color:var(--md-sys-color-primary)}.praxis-files-upload .dropzone.disabled{opacity:.6;cursor:not-allowed;pointer-events:none}.praxis-files-upload .dropzone p{margin:0 0 .5rem}.praxis-files-upload .dropzone .dz-icon{font-size:40px;color:var(--md-sys-color-primary);margin-bottom:.25rem}.praxis-files-upload .dropzone .dz-title{font-weight:600}.praxis-files-upload .dropzone .dz-hint{margin:0;opacity:.8;font-size:.9rem}.praxis-files-upload .dropzone .field-actions{position:absolute;top:.25rem;right:.25rem;display:flex;gap:.25rem;pointer-events:auto}.praxis-files-upload .dropzone .field-actions button[mat-icon-button]{width:32px;height:32px}.praxis-files-upload .dropzone .field-actions mat-icon{font-size:20px}.praxis-files-upload .progress{margin-top:1rem}.praxis-files-upload .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);border:0}.praxis-files-upload .bulk-results{margin-top:1rem}.praxis-files-upload .bulk-results .bulk-summary{display:flex;gap:1rem;align-items:center;margin-bottom:.5rem}.praxis-files-upload .bulk-results .bulk-summary .stat{display:flex;align-items:center;gap:.35rem;padding:.25rem .5rem;border-radius:6px;background:var(--md-sys-color-surface-container-low)}.praxis-files-upload .bulk-results .bulk-summary .stat.success{color:var(--md-sys-color-primary)}.praxis-files-upload .bulk-results .bulk-summary .stat.failed{color:var(--md-sys-color-error)}.praxis-files-upload .bulk-results .bulk-summary .stat .label{opacity:.9}.praxis-files-upload .bulk-results .bulk-summary .stat .value{font-weight:600}.praxis-files-upload .bulk-results .results-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.praxis-files-upload .bulk-results .result-item{display:flex;gap:.75rem;padding:.5rem .75rem;border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;background:var(--md-sys-color-surface-container-lowest)}.praxis-files-upload .bulk-results .result-item.ok{border-color:var(--md-sys-color-primary)}.praxis-files-upload .bulk-results .result-item.err{border-color:var(--md-sys-color-error)}.praxis-files-upload .bulk-results .result-item .status-icon{align-self:flex-start;color:currentColor}.praxis-files-upload .bulk-results .result-item .main{flex:1;min-width:0}.praxis-files-upload .bulk-results .result-item .title{display:flex;align-items:center;gap:.5rem}.praxis-files-upload .bulk-results .result-item .name{font-weight:600}.praxis-files-upload .bulk-results .result-item .badge{font-size:.75rem;padding:.1rem .4rem;border-radius:999px;background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.praxis-files-upload .bulk-results .result-item .badge.err{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.praxis-files-upload .bulk-results .result-item .meta,.praxis-files-upload .bulk-results .result-item .error{display:flex;flex-wrap:wrap;gap:.5rem 1rem;margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .error .code{font-weight:600}.praxis-files-upload .bulk-results .result-item .extra{margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .item-actions{display:flex;align-items:center;gap:.25rem}.praxis-files-upload .bulk-results .list-footer{margin-top:.5rem;display:flex;justify-content:center}.praxis-files-upload .proximity-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:2px dashed var(--md-sys-color-outline);border-radius:var(--md-sys-shape-corner-medium, 4px);background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface);text-align:center;z-index:2;pointer-events:none}.praxis-files-upload .proximity-overlay .dz-icon{font-size:40px;color:var(--md-sys-color-primary)}.praxis-files-upload .proximity-overlay .dz-title{font-weight:600}.praxis-files-upload .proximity-overlay .dz-hint{opacity:.85}.praxis-files-upload .selected-overlay{background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;box-shadow:var(--md-sys-elevation-level2, 0 2px 6px rgba(0, 0, 0, .15));max-height:260px;overflow:auto;padding:.5rem}.praxis-files-upload .selected-overlay .sel-header{position:sticky;top:0;z-index:1;background:var(--md-sys-color-surface);display:flex;align-items:center;gap:.5rem;padding-bottom:.25rem;margin-bottom:.25rem}.praxis-files-upload .selected-overlay .sel-header .title{font-weight:600}.praxis-files-upload .selected-overlay .sel-header .spacer{flex:1}.praxis-files-upload .selected-overlay .sel-list{list-style:none;margin:0;padding:0}.praxis-files-upload .selected-overlay .sel-list .sel-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--md-sys-color-outline-variant, #ccc);transition:background-color .12s ease}.praxis-files-upload .selected-overlay .sel-list .sel-item .file-icon{opacity:.9;color:var(--md-sys-color-primary, #1976d2)}.praxis-files-upload .selected-overlay .sel-list .sel-item .info{flex:1;min-width:0}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .meta{font-size:.8rem;opacity:.7}.praxis-files-upload .selected-overlay .sel-list .sel-item .remove-btn{color:var(--md-sys-color-error, #b00020)}.praxis-files-upload .selected-overlay .sel-list .sel-item .more-btn{margin-left:.25rem}.praxis-files-upload .selected-overlay .sel-list .sel-item:hover{background:var(--md-sys-color-surface-container-low)}:host ::ng-deep .praxis-files-upload-overlay-panel{pointer-events:none}:host ::ng-deep .praxis-files-selected-overlay-panel{pointer-events:auto;z-index:3}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i7.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i7.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i9.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i10.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i11$1.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i3$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i3$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i3$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i7.JsonPipe, name: "json" }, { kind: "pipe", type: i7.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4648
+ `, isInline: true, styles: ["@charset \"UTF-8\";.praxis-files-upload{position:relative}.praxis-files-upload .settings-btn{position:absolute;top:.5rem;right:.5rem}.praxis-files-upload .rate-limit-banner,.praxis-files-upload .quota-banner,.praxis-files-upload .error{margin:.5rem 0;padding:.5rem 1rem;border-radius:var(--md-sys-shape-corner-small, 4px)}.praxis-files-upload .rate-limit-banner{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.praxis-files-upload .quota-banner{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.praxis-files-upload .dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;border:2px dashed var(--md-sys-color-outline);border-radius:var(--md-sys-shape-corner-medium, 4px);background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface);text-align:center;transition:background-color .2s;width:100%;min-height:160px;margin:.5rem 0;gap:.5rem;position:relative}.praxis-files-upload .dropzone:hover{outline:none;background:var(--md-sys-color-surface-container-high);border-color:var(--md-sys-color-primary)}.praxis-files-upload .dropzone.dragover{background:var(--md-sys-color-surface-container-high);border-color:var(--md-sys-color-primary)}.praxis-files-upload .dropzone.disabled{opacity:.6;cursor:not-allowed;pointer-events:none}.praxis-files-upload .dropzone p{margin:0 0 .5rem}.praxis-files-upload .dropzone .dz-icon{font-size:40px;color:var(--md-sys-color-primary);margin-bottom:.25rem}.praxis-files-upload .dropzone .dz-title{font-weight:600}.praxis-files-upload .dropzone .dz-hint{margin:0;opacity:.8;font-size:.9rem}.praxis-files-upload .dropzone .field-actions{position:absolute;top:.25rem;right:.25rem;display:flex;gap:.25rem;pointer-events:auto}.praxis-files-upload .dropzone .field-actions button[mat-icon-button]{width:32px;height:32px}.praxis-files-upload .dropzone .field-actions mat-icon{font-size:20px}.praxis-files-upload .progress{margin-top:1rem}.praxis-files-upload .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);border:0}.praxis-files-upload .bulk-results{margin-top:1rem}.praxis-files-upload .bulk-results .bulk-summary{display:flex;gap:1rem;align-items:center;margin-bottom:.5rem}.praxis-files-upload .bulk-results .bulk-summary .stat{display:flex;align-items:center;gap:.35rem;padding:.25rem .5rem;border-radius:6px;background:var(--md-sys-color-surface-container-low)}.praxis-files-upload .bulk-results .bulk-summary .stat.success{color:var(--md-sys-color-primary)}.praxis-files-upload .bulk-results .bulk-summary .stat.failed{color:var(--md-sys-color-error)}.praxis-files-upload .bulk-results .bulk-summary .stat .label{opacity:.9}.praxis-files-upload .bulk-results .bulk-summary .stat .value{font-weight:600}.praxis-files-upload .bulk-results .results-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.praxis-files-upload .bulk-results .result-item{display:flex;gap:.75rem;padding:.5rem .75rem;border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;background:var(--md-sys-color-surface-container-lowest)}.praxis-files-upload .bulk-results .result-item.ok{border-color:var(--md-sys-color-primary)}.praxis-files-upload .bulk-results .result-item.err{border-color:var(--md-sys-color-error)}.praxis-files-upload .bulk-results .result-item .status-icon{align-self:flex-start;color:currentColor}.praxis-files-upload .bulk-results .result-item .main{flex:1;min-width:0}.praxis-files-upload .bulk-results .result-item .title{display:flex;align-items:center;gap:.5rem}.praxis-files-upload .bulk-results .result-item .name{font-weight:600}.praxis-files-upload .bulk-results .result-item .badge{font-size:.75rem;padding:.1rem .4rem;border-radius:999px;background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.praxis-files-upload .bulk-results .result-item .badge.err{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.praxis-files-upload .bulk-results .result-item .meta,.praxis-files-upload .bulk-results .result-item .error{display:flex;flex-wrap:wrap;gap:.5rem 1rem;margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .error .code{font-weight:600}.praxis-files-upload .bulk-results .result-item .extra{margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .item-actions{display:flex;align-items:center;gap:.25rem}.praxis-files-upload .bulk-results .list-footer{margin-top:.5rem;display:flex;justify-content:center}.praxis-files-upload .proximity-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:2px dashed var(--md-sys-color-outline);border-radius:var(--md-sys-shape-corner-medium, 4px);background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface);text-align:center;z-index:2;pointer-events:none}.praxis-files-upload .proximity-overlay .dz-icon{font-size:40px;color:var(--md-sys-color-primary)}.praxis-files-upload .proximity-overlay .dz-title{font-weight:600}.praxis-files-upload .proximity-overlay .dz-hint{opacity:.85}.praxis-files-upload .selected-overlay{background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;box-shadow:var(--md-sys-elevation-level2, 0 2px 6px rgba(0, 0, 0, .15));max-height:260px;overflow:auto;padding:.5rem}.praxis-files-upload .selected-overlay .sel-header{position:sticky;top:0;z-index:1;background:var(--md-sys-color-surface);display:flex;align-items:center;gap:.5rem;padding-bottom:.25rem;margin-bottom:.25rem}.praxis-files-upload .selected-overlay .sel-header .title{font-weight:600}.praxis-files-upload .selected-overlay .sel-header .spacer{flex:1}.praxis-files-upload .selected-overlay .sel-list{list-style:none;margin:0;padding:0}.praxis-files-upload .selected-overlay .sel-list .sel-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--md-sys-color-outline-variant, #ccc);transition:background-color .12s ease}.praxis-files-upload .selected-overlay .sel-list .sel-item .file-icon{opacity:.9;color:var(--md-sys-color-primary, #1976d2)}.praxis-files-upload .selected-overlay .sel-list .sel-item .info{flex:1;min-width:0}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .meta{font-size:.8rem;opacity:.7}.praxis-files-upload .selected-overlay .sel-list .sel-item .remove-btn{color:var(--md-sys-color-error, #b00020)}.praxis-files-upload .selected-overlay .sel-list .sel-item .more-btn{margin-left:.25rem}.praxis-files-upload .selected-overlay .sel-list .sel-item:hover{background:var(--md-sys-color-surface-container-low)}:host ::ng-deep .praxis-files-upload-overlay-panel{pointer-events:none}:host ::ng-deep .praxis-files-selected-overlay-panel{pointer-events:auto;z-index:3}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i9.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i9.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i11.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i11.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i10.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i13.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i14.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i14.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i14.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i9.JsonPipe, name: "json" }, { kind: "pipe", type: i9.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3557
4649
  }
3558
4650
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisFilesUpload, decorators: [{
3559
4651
  type: Component,
3560
- args: [{ selector: 'praxis-files-upload', standalone: true, imports: [
4652
+ args: [{ selector: 'praxis-files-upload', standalone: true, providers: [providePraxisFilesUploadI18n()], imports: [
3561
4653
  CommonModule,
3562
4654
  ReactiveFormsModule,
3563
4655
  MatButtonModule,
@@ -3567,27 +4659,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3567
4659
  MatMenuModule,
3568
4660
  MatTooltipModule,
3569
4661
  ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
3570
- <div class="praxis-files-upload" [class.dense]="config?.ui?.dense" #host>
4662
+ <div
4663
+ class="praxis-files-upload"
4664
+ [class.dense]="config?.ui?.dense"
4665
+ [attr.data-files-upload-id]="a11yBaseId"
4666
+ [attr.data-ready-state]="readinessState"
4667
+ [attr.data-ready-source]="readinessSource"
4668
+ [attr.aria-busy]="readinessState === 'loading' ? 'true' : 'false'"
4669
+ #host
4670
+ >
3571
4671
  <!-- Proximity overlay inline (fallback quando expandMode === 'inline') -->
3572
- <div
3573
- class="proximity-overlay"
4672
+ <div
4673
+ class="proximity-overlay"
3574
4674
  *ngIf="
3575
4675
  showProximityOverlay &&
3576
4676
  (config?.ui?.dropzone?.expandMode ?? 'overlay') === 'inline'
3577
4677
  "
3578
4678
  [style.height.px]="config?.ui?.dropzone?.expandHeight ?? 200"
3579
4679
  aria-hidden="true"
3580
- >
3581
- <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3582
- <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3583
- <p class="dz-hint">Solte aqui para enviar</p>
3584
- </div>
4680
+ >
4681
+ <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
4682
+ <p class="dz-title">{{ texts.dropzoneLabel }}</p>
4683
+ <p class="dz-hint">{{ t('dropzoneProximityHint', 'Solte aqui para enviar') }}</p>
4684
+ </div>
3585
4685
  <!-- Template do overlay de proximidade (renderizado via CDK Overlay) -->
3586
4686
  <ng-template #proximityOverlayTmpl>
3587
4687
  <div class="proximity-overlay" aria-hidden="true">
3588
4688
  <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3589
4689
  <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3590
- <p class="dz-hint">Solte aqui para enviar</p>
4690
+ <p class="dz-hint">{{ t('dropzoneProximityHint', 'Solte aqui para enviar') }}</p>
3591
4691
  </div>
3592
4692
  </ng-template>
3593
4693
 
@@ -3596,22 +4696,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3596
4696
  <ng-template #selectedOverlayTmpl>
3597
4697
  <div
3598
4698
  class="selected-overlay"
3599
- role="dialog"
3600
- aria-label="Arquivos selecionados"
4699
+ role="region"
4700
+ [attr.aria-labelledby]="selectedOverlayTitleId"
3601
4701
  >
3602
4702
  <header class="sel-header">
3603
- <span class="title">Selecionados ({{ pendingCount }})</span>
4703
+ <span class="title" [id]="selectedOverlayTitleId">{{ t('selectedOverlayTitle', 'Selecionados') }} ({{ pendingCount }})</span>
3604
4704
  <span class="spacer"></span>
3605
4705
  <button
3606
4706
  mat-stroked-button
3607
4707
  color="primary"
3608
- (click)="triggerUpload()"
4708
+ (click)="submitPendingFiles()"
3609
4709
  [disabled]="!baseUrl || isUploading"
3610
4710
  >
3611
- Enviar
4711
+ {{ t('fieldActionUploadShort', 'Enviar') }}
3612
4712
  </button>
3613
- <button mat-button type="button" (click)="clearSelection()">
3614
- Cancelar
4713
+ <button mat-button type="button" (click)="resetPendingFiles()">
4714
+ {{ t('fieldActionCancelShort', 'Cancelar') }}
3615
4715
  </button>
3616
4716
  </header>
3617
4717
  <ul class="sel-list">
@@ -3632,7 +4732,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3632
4732
  class="remove-btn"
3633
4733
  [matTooltip]="t('remove', 'Remover')"
3634
4734
  (click)="removePendingFile(f)"
3635
- [attr.aria-label]="t('remove', 'Remover')"
4735
+ [attr.aria-label]="pendingFileRemoveAriaLabel(f)"
3636
4736
  >
3637
4737
  <mat-icon [praxisIcon]="'close'"></mat-icon>
3638
4738
  </button>
@@ -3643,7 +4743,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3643
4743
  [matMenuTriggerData]="{ file: f }"
3644
4744
  aria-haspopup="menu"
3645
4745
  [matTooltip]="t('moreActions', 'Mais ações')"
3646
- [attr.aria-label]="t('moreActions', 'Mais ações')"
4746
+ [attr.aria-label]="pendingFileMoreActionsAriaLabel(f)"
3647
4747
  >
3648
4748
  <mat-icon [praxisIcon]="'more_horiz'"></mat-icon>
3649
4749
  </button>
@@ -3684,6 +4784,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3684
4784
  <div
3685
4785
  class="rate-limit-banner"
3686
4786
  *ngIf="rateLimit && config?.rateLimit?.showBannerOn429 !== false"
4787
+ [id]="messageRegionId"
4788
+ role="alert"
4789
+ aria-live="assertive"
3687
4790
  >
3688
4791
  {{ texts.rateLimitBanner }}
3689
4792
  {{ rateLimit.resetEpochSeconds * 1000 | date: 'shortTime' }}.
@@ -3691,32 +4794,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3691
4794
  <div
3692
4795
  class="quota-banner"
3693
4796
  *ngIf="quotaExceeded && config?.quotas?.showQuotaWarnings !== false"
4797
+ [id]="messageRegionId"
4798
+ role="status"
4799
+ aria-live="polite"
3694
4800
  >
3695
4801
  {{ errorMessage }}
3696
4802
  </div>
3697
- <div class="error" *ngIf="errorMessage && !quotaExceeded">
4803
+ <div
4804
+ class="error"
4805
+ *ngIf="errorMessage && !quotaExceeded"
4806
+ [id]="messageRegionId"
4807
+ role="alert"
4808
+ aria-live="assertive"
4809
+ >
3698
4810
  {{ errorMessage }}
3699
4811
  </div>
3700
- <div class="error" *ngIf="!baseUrl">
3701
- Base URL não configurada. Informe [baseUrl] para habilitar o envio.
4812
+ <div
4813
+ class="error"
4814
+ *ngIf="!baseUrl"
4815
+ [id]="messageRegionId"
4816
+ role="alert"
4817
+ aria-live="assertive"
4818
+ >
4819
+ {{ t('baseUrlMissing', 'Base URL not configured. Provide [baseUrl] to enable uploads.') }}
3702
4820
  </div>
3703
4821
  <div
3704
4822
  class="dropzone"
3705
4823
  *ngIf="config?.ui?.showDropzone !== false"
3706
- [attr.role]="
3707
- !baseUrl ||
3708
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3709
- isUploading
3710
- ? null
3711
- : 'button'
3712
- "
3713
- [attr.tabindex]="
3714
- !baseUrl ||
3715
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3716
- isUploading
3717
- ? -1
3718
- : 0
3719
- "
4824
+ role="group"
3720
4825
  [class.dragover]="isDragging"
3721
4826
  [class.disabled]="
3722
4827
  !baseUrl ||
@@ -3730,39 +4835,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3730
4835
  ? 'true'
3731
4836
  : 'false'
3732
4837
  "
3733
- [attr.aria-label]="texts.dropzoneLabel + ' ' + texts.dropzoneButton"
3734
- (click)="
3735
- !baseUrl ||
3736
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3737
- isUploading
3738
- ? null
3739
- : openFile(fileInput)
3740
- "
4838
+ [attr.aria-label]="texts.dropzoneLabel"
4839
+ [attr.aria-describedby]="messageRegionId"
3741
4840
  (drop)="onDrop($event)"
3742
4841
  (dragover)="onDragOver($event)"
3743
4842
  (dragleave)="onDragLeave($event)"
3744
- (keydown.enter)="
3745
- $event.preventDefault();
3746
- !baseUrl ||
3747
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3748
- isUploading
3749
- ? null
3750
- : openFile(fileInput)
3751
- "
3752
- (keydown.space)="
3753
- $event.preventDefault();
3754
- !baseUrl ||
3755
- (quotaExceeded && config?.quotas?.blockOnExceed) ||
3756
- isUploading
3757
- ? null
3758
- : openFile(fileInput)
3759
- "
3760
4843
  >
3761
4844
  <!-- Ações internas (ícones) -->
3762
4845
  <div class="field-actions" aria-hidden="false">
3763
4846
  <button
3764
4847
  mat-icon-button
3765
- [matTooltip]="'Selecionar arquivo(s)'"
4848
+ [matTooltip]="t('selectFiles', 'Select file(s)')"
4849
+ [attr.aria-label]="selectFilesAriaLabel"
3766
4850
  (click)="$event.stopPropagation(); openFile(fileInput)"
3767
4851
  [disabled]="!baseUrl || isUploading"
3768
4852
  >
@@ -3771,6 +4855,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3771
4855
  <button
3772
4856
  mat-icon-button
3773
4857
  [matTooltip]="policiesSummary"
4858
+ [attr.aria-label]="policySummaryAriaLabel"
3774
4859
  (click)="$event.stopPropagation()"
3775
4860
  [disabled]="false"
3776
4861
  >
@@ -3781,9 +4866,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3781
4866
  <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3782
4867
  <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3783
4868
  <p class="dz-hint">
3784
- Arraste e solte arquivos aqui ou clique para selecionar
4869
+ {{ t('dropzoneHint', 'Arraste e solte arquivos aqui ou clique para selecionar') }}
3785
4870
  </p>
3786
- <button type="button" mat-stroked-button (click)="openFile(fileInput)">
4871
+ <button
4872
+ type="button"
4873
+ mat-stroked-button
4874
+ [disabled]="!baseUrl || isUploading"
4875
+ [attr.aria-describedby]="messageRegionId"
4876
+ (click)="openFile(fileInput)"
4877
+ >
3787
4878
  {{ texts.dropzoneButton }}
3788
4879
  </button>
3789
4880
  </div>
@@ -3803,8 +4894,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3803
4894
  class="conflict-policy"
3804
4895
  *ngIf="config?.ui?.showConflictPolicySelector"
3805
4896
  >
3806
- <label for="conflictPolicy">{{ texts.conflictPolicyLabel }}</label>
3807
- <select id="conflictPolicy" [formControl]="conflictPolicy">
4897
+ <label [for]="conflictPolicyInputId">{{ texts.conflictPolicyLabel }}</label>
4898
+ <select [id]="conflictPolicyInputId" [formControl]="conflictPolicy">
3808
4899
  <option *ngFor="let p of conflictPolicies" [value]="p">
3809
4900
  {{ p }}
3810
4901
  </option>
@@ -3812,18 +4903,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3812
4903
  </div>
3813
4904
 
3814
4905
  <div class="metadata" *ngIf="config?.ui?.showMetadataForm">
3815
- <label for="metadata">{{ texts.metadataLabel }}</label>
3816
- <textarea id="metadata" [formControl]="metadata"></textarea>
4906
+ <label [for]="metadataInputId">{{ texts.metadataLabel }}</label>
4907
+ <textarea [id]="metadataInputId" [formControl]="metadata"></textarea>
3817
4908
  <div class="error" *ngIf="metadataError">{{ metadataError }}</div>
3818
4909
  </div>
3819
4910
 
3820
- <div class="progress" *ngIf="showProgress">
4911
+ <div
4912
+ class="progress"
4913
+ *ngIf="showProgress"
4914
+ [id]="progressRegionId"
4915
+ role="status"
4916
+ aria-live="polite"
4917
+ >
3821
4918
  <mat-progress-bar
3822
4919
  mode="determinate"
3823
4920
  [value]="uploadProgressValue"
3824
4921
  [attr.aria-label]="texts.progressAriaLabel"
4922
+ [attr.aria-valuemin]="0"
4923
+ [attr.aria-valuemax]="100"
4924
+ [attr.aria-valuenow]="uploadProgressValue"
3825
4925
  ></mat-progress-bar>
3826
- <span class="sr-only">{{ uploadProgressValue }}%</span>
4926
+ <span class="sr-only">{{ texts.progressAriaLabel }} {{ uploadProgressValue }}%</span>
3827
4927
  </div>
3828
4928
 
3829
4929
  <ul class="file-feedback" *ngIf="hasUploadedFiles">
@@ -3838,17 +4938,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3838
4938
  <section class="bulk-results" *ngIf="hasLastResults">
3839
4939
  <header class="bulk-summary" *ngIf="lastStats">
3840
4940
  <div class="stat">
3841
- <span class="label">Total</span>
4941
+ <span class="label">{{ t('bulkSummaryTotal', 'Total') }}</span>
3842
4942
  <span class="value">{{ lastStats.total }}</span>
3843
4943
  </div>
3844
4944
  <div class="stat success">
3845
4945
  <mat-icon [praxisIcon]="'check_circle'"></mat-icon>
3846
- <span class="label">Sucesso</span>
4946
+ <span class="label">{{ t('bulkSummarySuccess', 'Sucesso') }}</span>
3847
4947
  <span class="value">{{ lastStats.succeeded }}</span>
3848
4948
  </div>
3849
4949
  <div class="stat failed">
3850
4950
  <mat-icon [praxisIcon]="'error'"></mat-icon>
3851
- <span class="label">Falhas</span>
4951
+ <span class="label">{{ t('bulkSummaryFailed', 'Falhas') }}</span>
3852
4952
  <span class="value">{{ lastStats.failed }}</span>
3853
4953
  </div>
3854
4954
  </header>
@@ -3879,19 +4979,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3879
4979
  >
3880
4980
  </div>
3881
4981
  <div class="meta" *ngIf="r.file">
3882
- <span *ngIf="r.file.id">ID: {{ r.file.id }}</span>
4982
+ <span *ngIf="r.file.id"
4983
+ >{{ t('resultMetaIdLabel', 'ID') }}: {{ r.file.id }}</span
4984
+ >
3883
4985
  <span *ngIf="r.file.contentType"
3884
- >Tipo: {{ r.file.contentType }}</span
4986
+ >{{ t('resultMetaTypeLabel', 'Tipo') }}:
4987
+ {{ r.file.contentType }}</span
3885
4988
  >
3886
4989
  <span *ngIf="r.file.fileSize"
3887
- >Tamanho: {{ formatBytes(r.file.fileSize) }}</span
4990
+ >{{ t('resultMetaSizeLabel', 'Tamanho') }}:
4991
+ {{ formatBytes(r.file.fileSize) }}</span
3888
4992
  >
3889
4993
  <span *ngIf="r.file.uploadedAt"
3890
- >Enviado em: {{ r.file.uploadedAt | date: 'short' }}</span
4994
+ >{{ t('resultMetaUploadedAtLabel', 'Enviado em') }}:
4995
+ {{ r.file.uploadedAt | date: 'short' }}</span
3891
4996
  >
3892
4997
  </div>
3893
4998
  <div class="error" *ngIf="r.error as e">
3894
- <span class="code">{{ e.code }}</span>
4999
+ <span class="code">{{ primaryErrorCode(e) }}</span>
3895
5000
  <ng-container *ngIf="mapErrorFull(e) as me">
3896
5001
  <span class="title" *ngIf="me.title">{{ me.title }}:</span>
3897
5002
  <span class="msg">{{ me.message }}</span>
@@ -3899,13 +5004,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3899
5004
  — {{ me.userAction }}</span
3900
5005
  >
3901
5006
  </ng-container>
3902
- <span class="trace" *ngIf="e.traceId"
3903
- >(trace {{ e.traceId }})</span
5007
+ <span class="trace" *ngIf="traceIdOf(e) as traceId"
5008
+ >(trace {{ traceId }})</span
3904
5009
  >
3905
5010
  </div>
3906
5011
  <div class="extra" *ngIf="r.file?.metadata as m">
3907
5012
  <details (toggle)="onDetailsToggle($event, r)">
3908
- <summary>Metadados</summary>
5013
+ <summary>{{ t('detailsMetadata', 'Metadados') }}</summary>
3909
5014
  <pre>{{ m | json }}</pre>
3910
5015
  </details>
3911
5016
  </div>
@@ -3913,14 +5018,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3913
5018
  <div class="item-actions">
3914
5019
  <button
3915
5020
  mat-icon-button
3916
- [matTooltip]="'Detalhes'"
5021
+ [matTooltip]="t('details', 'Detalhes')"
5022
+ [attr.aria-label]="resultActionAriaLabel('details', 'Detalhes', r.fileName)"
3917
5023
  (click)="toggleDetailsFor(r)"
3918
5024
  >
3919
5025
  <mat-icon [praxisIcon]="'info'"></mat-icon>
3920
5026
  </button>
3921
5027
  <button
3922
5028
  mat-icon-button
3923
- [matTooltip]="'Reenviar'"
5029
+ [matTooltip]="t('retry', 'Reenviar')"
5030
+ [attr.aria-label]="resultActionAriaLabel('retry', 'Reenviar', r.fileName)"
3924
5031
  (click)="onRetry(r)"
3925
5032
  [disabled]="isUploading"
3926
5033
  >
@@ -3928,7 +5035,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3928
5035
  </button>
3929
5036
  <button
3930
5037
  mat-icon-button
3931
- [matTooltip]="'Baixar'"
5038
+ [matTooltip]="t('download', 'Baixar')"
5039
+ [attr.aria-label]="resultActionAriaLabel('download', 'Baixar', r.fileName)"
3932
5040
  (click)="onDownload(r)"
3933
5041
  [disabled]="r.status !== BulkUploadResultStatus.SUCCESS"
3934
5042
  >
@@ -3936,7 +5044,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3936
5044
  </button>
3937
5045
  <button
3938
5046
  mat-icon-button
3939
- [matTooltip]="'Copiar link'"
5047
+ [matTooltip]="t('copyLink', 'Copiar link')"
5048
+ [attr.aria-label]="resultActionAriaLabel('copyLink', 'Copiar link', r.fileName)"
3940
5049
  (click)="onCopyLink(r)"
3941
5050
  [disabled]="r.status !== BulkUploadResultStatus.SUCCESS"
3942
5051
  >
@@ -3944,7 +5053,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3944
5053
  </button>
3945
5054
  <button
3946
5055
  mat-icon-button
3947
- [matTooltip]="'Remover'"
5056
+ [matTooltip]="t('remove', 'Remover')"
5057
+ [attr.aria-label]="resultActionAriaLabel('remove', 'Remover', r.fileName)"
3948
5058
  (click)="removeResult(r)"
3949
5059
  >
3950
5060
  <mat-icon [praxisIcon]="'close'"></mat-icon>
@@ -3961,7 +5071,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3961
5071
  "
3962
5072
  >
3963
5073
  <button mat-button type="button" (click)="showAllResults = true">
3964
- Ver todos
5074
+ {{ t('showAll', 'Ver todos') }}
3965
5075
  </button>
3966
5076
  </div>
3967
5077
  <div
@@ -3974,37 +5084,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
3974
5084
  "
3975
5085
  >
3976
5086
  <button mat-button type="button" (click)="showAllResults = false">
3977
- Ver menos
5087
+ {{ t('showLess', 'Ver menos') }}
3978
5088
  </button>
3979
5089
  </div>
3980
5090
  </section>
3981
5091
 
3982
5092
  <ng-content></ng-content>
3983
5093
  </div>
3984
- `, styles: ["@charset \"UTF-8\";.praxis-files-upload{position:relative}.praxis-files-upload .settings-btn{position:absolute;top:.5rem;right:.5rem}.praxis-files-upload .rate-limit-banner,.praxis-files-upload .quota-banner,.praxis-files-upload .error{margin:.5rem 0;padding:.5rem 1rem;border-radius:var(--md-sys-shape-corner-small, 4px)}.praxis-files-upload .rate-limit-banner{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.praxis-files-upload .quota-banner{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.praxis-files-upload .dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;border:2px dashed var(--md-sys-color-outline);border-radius:var(--md-sys-shape-corner-medium, 4px);background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface);text-align:center;cursor:pointer;transition:background-color .2s;width:100%;min-height:160px;margin:.5rem 0;gap:.5rem;position:relative}.praxis-files-upload .dropzone:focus,.praxis-files-upload .dropzone:hover{outline:none;background:var(--md-sys-color-surface-container-high);border-color:var(--md-sys-color-primary)}.praxis-files-upload .dropzone.dragover{background:var(--md-sys-color-surface-container-high);border-color:var(--md-sys-color-primary)}.praxis-files-upload .dropzone.disabled{opacity:.6;cursor:not-allowed;pointer-events:none}.praxis-files-upload .dropzone p{margin:0 0 .5rem}.praxis-files-upload .dropzone .dz-icon{font-size:40px;color:var(--md-sys-color-primary);margin-bottom:.25rem}.praxis-files-upload .dropzone .dz-title{font-weight:600}.praxis-files-upload .dropzone .dz-hint{margin:0;opacity:.8;font-size:.9rem}.praxis-files-upload .dropzone .field-actions{position:absolute;top:.25rem;right:.25rem;display:flex;gap:.25rem;pointer-events:auto}.praxis-files-upload .dropzone .field-actions button[mat-icon-button]{width:32px;height:32px}.praxis-files-upload .dropzone .field-actions mat-icon{font-size:20px}.praxis-files-upload .progress{margin-top:1rem}.praxis-files-upload .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);border:0}.praxis-files-upload .bulk-results{margin-top:1rem}.praxis-files-upload .bulk-results .bulk-summary{display:flex;gap:1rem;align-items:center;margin-bottom:.5rem}.praxis-files-upload .bulk-results .bulk-summary .stat{display:flex;align-items:center;gap:.35rem;padding:.25rem .5rem;border-radius:6px;background:var(--md-sys-color-surface-container-low)}.praxis-files-upload .bulk-results .bulk-summary .stat.success{color:var(--md-sys-color-primary)}.praxis-files-upload .bulk-results .bulk-summary .stat.failed{color:var(--md-sys-color-error)}.praxis-files-upload .bulk-results .bulk-summary .stat .label{opacity:.9}.praxis-files-upload .bulk-results .bulk-summary .stat .value{font-weight:600}.praxis-files-upload .bulk-results .results-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.praxis-files-upload .bulk-results .result-item{display:flex;gap:.75rem;padding:.5rem .75rem;border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;background:var(--md-sys-color-surface-container-lowest)}.praxis-files-upload .bulk-results .result-item.ok{border-color:var(--md-sys-color-primary)}.praxis-files-upload .bulk-results .result-item.err{border-color:var(--md-sys-color-error)}.praxis-files-upload .bulk-results .result-item .status-icon{align-self:flex-start;color:currentColor}.praxis-files-upload .bulk-results .result-item .main{flex:1;min-width:0}.praxis-files-upload .bulk-results .result-item .title{display:flex;align-items:center;gap:.5rem}.praxis-files-upload .bulk-results .result-item .name{font-weight:600}.praxis-files-upload .bulk-results .result-item .badge{font-size:.75rem;padding:.1rem .4rem;border-radius:999px;background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.praxis-files-upload .bulk-results .result-item .badge.err{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.praxis-files-upload .bulk-results .result-item .meta,.praxis-files-upload .bulk-results .result-item .error{display:flex;flex-wrap:wrap;gap:.5rem 1rem;margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .error .code{font-weight:600}.praxis-files-upload .bulk-results .result-item .extra{margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .item-actions{display:flex;align-items:center;gap:.25rem}.praxis-files-upload .bulk-results .list-footer{margin-top:.5rem;display:flex;justify-content:center}.praxis-files-upload .proximity-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:2px dashed var(--md-sys-color-outline);border-radius:var(--md-sys-shape-corner-medium, 4px);background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface);text-align:center;z-index:2;pointer-events:none}.praxis-files-upload .proximity-overlay .dz-icon{font-size:40px;color:var(--md-sys-color-primary)}.praxis-files-upload .proximity-overlay .dz-title{font-weight:600}.praxis-files-upload .proximity-overlay .dz-hint{opacity:.85}.praxis-files-upload .selected-overlay{background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;box-shadow:var(--md-sys-elevation-level2, 0 2px 6px rgba(0, 0, 0, .15));max-height:260px;overflow:auto;padding:.5rem}.praxis-files-upload .selected-overlay .sel-header{position:sticky;top:0;z-index:1;background:var(--md-sys-color-surface);display:flex;align-items:center;gap:.5rem;padding-bottom:.25rem;margin-bottom:.25rem}.praxis-files-upload .selected-overlay .sel-header .title{font-weight:600}.praxis-files-upload .selected-overlay .sel-header .spacer{flex:1}.praxis-files-upload .selected-overlay .sel-list{list-style:none;margin:0;padding:0}.praxis-files-upload .selected-overlay .sel-list .sel-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--md-sys-color-outline-variant, #ccc);transition:background-color .12s ease}.praxis-files-upload .selected-overlay .sel-list .sel-item .file-icon{opacity:.9;color:var(--md-sys-color-primary, #1976d2)}.praxis-files-upload .selected-overlay .sel-list .sel-item .info{flex:1;min-width:0}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .meta{font-size:.8rem;opacity:.7}.praxis-files-upload .selected-overlay .sel-list .sel-item .remove-btn{color:var(--md-sys-color-error, #b00020)}.praxis-files-upload .selected-overlay .sel-list .sel-item .more-btn{margin-left:.25rem}.praxis-files-upload .selected-overlay .sel-list .sel-item:hover{background:var(--md-sys-color-surface-container-low)}:host ::ng-deep .praxis-files-upload-overlay-panel{pointer-events:none}:host ::ng-deep .praxis-files-selected-overlay-panel{pointer-events:auto;z-index:3}\n"] }]
3985
- }], ctorParameters: () => [{ type: i1$2.SettingsPanelService }, { type: DragProximityService }, { type: undefined, decorators: [{
5094
+ `, styles: ["@charset \"UTF-8\";.praxis-files-upload{position:relative}.praxis-files-upload .settings-btn{position:absolute;top:.5rem;right:.5rem}.praxis-files-upload .rate-limit-banner,.praxis-files-upload .quota-banner,.praxis-files-upload .error{margin:.5rem 0;padding:.5rem 1rem;border-radius:var(--md-sys-shape-corner-small, 4px)}.praxis-files-upload .rate-limit-banner{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.praxis-files-upload .quota-banner{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container)}.praxis-files-upload .dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;border:2px dashed var(--md-sys-color-outline);border-radius:var(--md-sys-shape-corner-medium, 4px);background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface);text-align:center;transition:background-color .2s;width:100%;min-height:160px;margin:.5rem 0;gap:.5rem;position:relative}.praxis-files-upload .dropzone:hover{outline:none;background:var(--md-sys-color-surface-container-high);border-color:var(--md-sys-color-primary)}.praxis-files-upload .dropzone.dragover{background:var(--md-sys-color-surface-container-high);border-color:var(--md-sys-color-primary)}.praxis-files-upload .dropzone.disabled{opacity:.6;cursor:not-allowed;pointer-events:none}.praxis-files-upload .dropzone p{margin:0 0 .5rem}.praxis-files-upload .dropzone .dz-icon{font-size:40px;color:var(--md-sys-color-primary);margin-bottom:.25rem}.praxis-files-upload .dropzone .dz-title{font-weight:600}.praxis-files-upload .dropzone .dz-hint{margin:0;opacity:.8;font-size:.9rem}.praxis-files-upload .dropzone .field-actions{position:absolute;top:.25rem;right:.25rem;display:flex;gap:.25rem;pointer-events:auto}.praxis-files-upload .dropzone .field-actions button[mat-icon-button]{width:32px;height:32px}.praxis-files-upload .dropzone .field-actions mat-icon{font-size:20px}.praxis-files-upload .progress{margin-top:1rem}.praxis-files-upload .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);border:0}.praxis-files-upload .bulk-results{margin-top:1rem}.praxis-files-upload .bulk-results .bulk-summary{display:flex;gap:1rem;align-items:center;margin-bottom:.5rem}.praxis-files-upload .bulk-results .bulk-summary .stat{display:flex;align-items:center;gap:.35rem;padding:.25rem .5rem;border-radius:6px;background:var(--md-sys-color-surface-container-low)}.praxis-files-upload .bulk-results .bulk-summary .stat.success{color:var(--md-sys-color-primary)}.praxis-files-upload .bulk-results .bulk-summary .stat.failed{color:var(--md-sys-color-error)}.praxis-files-upload .bulk-results .bulk-summary .stat .label{opacity:.9}.praxis-files-upload .bulk-results .bulk-summary .stat .value{font-weight:600}.praxis-files-upload .bulk-results .results-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.praxis-files-upload .bulk-results .result-item{display:flex;gap:.75rem;padding:.5rem .75rem;border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;background:var(--md-sys-color-surface-container-lowest)}.praxis-files-upload .bulk-results .result-item.ok{border-color:var(--md-sys-color-primary)}.praxis-files-upload .bulk-results .result-item.err{border-color:var(--md-sys-color-error)}.praxis-files-upload .bulk-results .result-item .status-icon{align-self:flex-start;color:currentColor}.praxis-files-upload .bulk-results .result-item .main{flex:1;min-width:0}.praxis-files-upload .bulk-results .result-item .title{display:flex;align-items:center;gap:.5rem}.praxis-files-upload .bulk-results .result-item .name{font-weight:600}.praxis-files-upload .bulk-results .result-item .badge{font-size:.75rem;padding:.1rem .4rem;border-radius:999px;background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.praxis-files-upload .bulk-results .result-item .badge.err{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.praxis-files-upload .bulk-results .result-item .meta,.praxis-files-upload .bulk-results .result-item .error{display:flex;flex-wrap:wrap;gap:.5rem 1rem;margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .error .code{font-weight:600}.praxis-files-upload .bulk-results .result-item .extra{margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .item-actions{display:flex;align-items:center;gap:.25rem}.praxis-files-upload .bulk-results .list-footer{margin-top:.5rem;display:flex;justify-content:center}.praxis-files-upload .proximity-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:2px dashed var(--md-sys-color-outline);border-radius:var(--md-sys-shape-corner-medium, 4px);background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface);text-align:center;z-index:2;pointer-events:none}.praxis-files-upload .proximity-overlay .dz-icon{font-size:40px;color:var(--md-sys-color-primary)}.praxis-files-upload .proximity-overlay .dz-title{font-weight:600}.praxis-files-upload .proximity-overlay .dz-hint{opacity:.85}.praxis-files-upload .selected-overlay{background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;box-shadow:var(--md-sys-elevation-level2, 0 2px 6px rgba(0, 0, 0, .15));max-height:260px;overflow:auto;padding:.5rem}.praxis-files-upload .selected-overlay .sel-header{position:sticky;top:0;z-index:1;background:var(--md-sys-color-surface);display:flex;align-items:center;gap:.5rem;padding-bottom:.25rem;margin-bottom:.25rem}.praxis-files-upload .selected-overlay .sel-header .title{font-weight:600}.praxis-files-upload .selected-overlay .sel-header .spacer{flex:1}.praxis-files-upload .selected-overlay .sel-list{list-style:none;margin:0;padding:0}.praxis-files-upload .selected-overlay .sel-list .sel-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--md-sys-color-outline-variant, #ccc);transition:background-color .12s ease}.praxis-files-upload .selected-overlay .sel-list .sel-item .file-icon{opacity:.9;color:var(--md-sys-color-primary, #1976d2)}.praxis-files-upload .selected-overlay .sel-list .sel-item .info{flex:1;min-width:0}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .meta{font-size:.8rem;opacity:.7}.praxis-files-upload .selected-overlay .sel-list .sel-item .remove-btn{color:var(--md-sys-color-error, #b00020)}.praxis-files-upload .selected-overlay .sel-list .sel-item .more-btn{margin-left:.25rem}.praxis-files-upload .selected-overlay .sel-list .sel-item:hover{background:var(--md-sys-color-surface-container-low)}:host ::ng-deep .praxis-files-upload-overlay-panel{pointer-events:none}:host ::ng-deep .praxis-files-selected-overlay-panel{pointer-events:auto;z-index:3}\n"] }]
5095
+ }], ctorParameters: () => [{ type: i1$3.SettingsPanelService }, { type: DragProximityService }, { type: i3.Overlay }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
5096
+ type: Inject,
5097
+ args: [ASYNC_CONFIG_STORAGE]
5098
+ }] }, { type: i1$2.PraxisI18nService }, { type: undefined, decorators: [{
5099
+ type: Optional
5100
+ }, {
3986
5101
  type: Inject,
3987
5102
  args: [FILES_UPLOAD_TEXTS]
3988
- }] }, { type: i3.Overlay }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
5103
+ }] }, { type: undefined, decorators: [{
5104
+ type: Optional
5105
+ }, {
3989
5106
  type: Inject,
3990
- args: [ASYNC_CONFIG_STORAGE]
5107
+ args: [TRANSLATE_LIKE]
5108
+ }] }, { type: ConfigService, decorators: [{
5109
+ type: Optional
3991
5110
  }] }, { type: FilesApiClient, decorators: [{
3992
5111
  type: Optional
3993
5112
  }] }, { type: PresignedUploaderService, decorators: [{
3994
5113
  type: Optional
3995
5114
  }] }, { type: ErrorMapperService, decorators: [{
3996
5115
  type: Optional
3997
- }] }, { type: undefined, decorators: [{
3998
- type: Optional
3999
- }, {
4000
- type: Inject,
4001
- args: [TRANSLATE_LIKE]
4002
5116
  }] }], propDecorators: { config: [{
4003
5117
  type: Input
4004
5118
  }], filesUploadId: [{
4005
5119
  type: Input
4006
- }], uploadId: [{
4007
- type: Input
4008
5120
  }], componentInstanceId: [{
4009
5121
  type: Input
4010
5122
  }], baseUrl: [{
@@ -4027,8 +5139,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4027
5139
  type: Output
4028
5140
  }], uploadProgress: [{
4029
5141
  type: Output
4030
- }], uploadError: [{
4031
- type: Output
4032
5142
  }], retry: [{
4033
5143
  type: Output
4034
5144
  }], download: [{
@@ -4039,10 +5149,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4039
5149
  type: Output
4040
5150
  }], detailsClosed: [{
4041
5151
  type: Output
4042
- }], pendingFilesChange: [{
5152
+ }], pendingStateChange: [{
4043
5153
  type: Output
4044
5154
  }], proximityChange: [{
4045
5155
  type: Output
5156
+ }], readinessChange: [{
5157
+ type: Output
4046
5158
  }], fileInput: [{
4047
5159
  type: ViewChild,
4048
5160
  args: ['fileInput']
@@ -4061,22 +5173,49 @@ class PdxFilesUploadFieldComponent extends SimpleBaseInputComponent {
4061
5173
  config = null;
4062
5174
  valueMode = 'metadata';
4063
5175
  baseUrl;
5176
+ uploadSuccess = new EventEmitter();
5177
+ bulkComplete = new EventEmitter();
5178
+ error = new EventEmitter();
4064
5179
  uploadError = null;
4065
5180
  uploader;
5181
+ pendingTrigger;
5182
+ pendingPanel;
4066
5183
  // Estado para UI de seleção manual
4067
5184
  pendingFiles = [];
5185
+ pendingPanelOpen = false;
5186
+ pendingOverlayPositions = [
5187
+ {
5188
+ originX: 'start',
5189
+ originY: 'bottom',
5190
+ overlayX: 'start',
5191
+ overlayY: 'top',
5192
+ offsetY: 8,
5193
+ },
5194
+ {
5195
+ originX: 'start',
5196
+ originY: 'top',
5197
+ overlayX: 'start',
5198
+ overlayY: 'bottom',
5199
+ offsetY: -8,
5200
+ },
5201
+ ];
4068
5202
  // Ações para UI de seleção manual
4069
5203
  onTriggerUpload() {
4070
- this.uploader?.triggerUpload();
5204
+ this.closePendingPanel(false);
5205
+ this.uploader?.submitPendingFiles();
4071
5206
  }
4072
5207
  onClearSelection() {
4073
- this.uploader?.clearSelection();
5208
+ this.closePendingPanel(false);
5209
+ this.uploader?.resetPendingFiles();
4074
5210
  }
4075
5211
  // Recebe o estado do componente filho
4076
- handlePendingFiles(files) {
4077
- this.pendingFiles = files;
4078
- if (files.length > 0) {
4079
- this.control().markAsTouched(); // Garante que validação de 'required' seja reavaliada
5212
+ handlePendingState(state) {
5213
+ this.pendingFiles = state.files;
5214
+ if (state.hasPending) {
5215
+ this.control().markAsTouched();
5216
+ }
5217
+ else {
5218
+ this.closePendingPanel(false);
4080
5219
  }
4081
5220
  }
4082
5221
  // Estado visual do campo compacto
@@ -4084,48 +5223,102 @@ class PdxFilesUploadFieldComponent extends SimpleBaseInputComponent {
4084
5223
  batchCount = 0;
4085
5224
  isProximityActive = false;
4086
5225
  errors = inject(ErrorMapperService);
4087
- translate = inject(TRANSLATE_LIKE, { optional: true }) ?? null;
5226
+ filesUploadI18n = inject(PraxisI18nService);
5227
+ legacyTexts = inject(FILES_UPLOAD_TEXTS, { optional: true });
5228
+ legacyTranslate = inject(TRANSLATE_LIKE, { optional: true });
4088
5229
  get showUploadError() {
4089
5230
  const c = this.control();
4090
- return c.touched && c.invalid;
5231
+ return (!!this.uploadError &&
5232
+ c.touched &&
5233
+ (c.hasError('upload') || c.hasError('uploadPartial')));
4091
5234
  }
4092
- t(key, fallback) {
4093
- const k = `praxis.filesUpload.${key}`;
4094
- const v = this.translate?.instant(k);
4095
- return v && v !== k ? v : fallback;
5235
+ resolveSingleValue(file) {
5236
+ if (this.valueMode === 'id')
5237
+ return file.id;
5238
+ if (this.valueMode === 'metadata[]')
5239
+ return [file];
5240
+ if (this.valueMode === 'id[]')
5241
+ return [file.id];
5242
+ return file;
5243
+ }
5244
+ resolveBulkValue(files) {
5245
+ if (files.length === 0)
5246
+ return null;
5247
+ if (this.valueMode === 'metadata[]')
5248
+ return files;
5249
+ if (this.valueMode === 'id[]')
5250
+ return files.map((file) => file.id);
5251
+ return this.resolveSingleValue(files[0]);
4096
5252
  }
4097
- onUploadSuccess(event) {
4098
- const value = this.valueMode === 'id' ? event.file.id : event.file;
5253
+ applyResolvedValue(file) {
5254
+ const value = this.resolveSingleValue(file);
4099
5255
  this.setValue(value);
4100
- this.control().setErrors(null);
5256
+ this.lastFileName = file.fileName || null;
5257
+ this.control().markAsTouched();
5258
+ this.control().markAsDirty();
5259
+ }
5260
+ clearUploadState() {
5261
+ this.updateUploadValidationState();
4101
5262
  this.uploadError = null;
4102
- // Atualiza visual
4103
- this.lastFileName = event.file.fileName || null;
5263
+ }
5264
+ applyUploadErrorState(message, validationKey) {
5265
+ this.updateUploadValidationState(validationKey);
5266
+ this.control().markAsTouched();
5267
+ this.control().markAsDirty();
5268
+ this.uploadError = message;
5269
+ }
5270
+ t(key, fallback) {
5271
+ return resolvePraxisFilesUploadText(this.filesUploadI18n, key, fallback, this.legacyTexts ?? undefined, this.legacyTranslate ?? undefined);
5272
+ }
5273
+ get additionalFilesChipAriaLabel() {
5274
+ const extraFiles = Math.max(this.batchCount - 1, 0);
5275
+ return `${extraFiles} ${this.t('fieldChipAria', 'arquivos adicionais selecionados')}`;
5276
+ }
5277
+ onUploadSuccess(file) {
5278
+ this.applyResolvedValue(file);
5279
+ this.clearUploadState();
4104
5280
  this.batchCount = 1;
5281
+ this.uploadSuccess.emit(file);
4105
5282
  }
4106
- onBulkComplete(resp) {
4107
- // Atualiza contador e último nome (primeiro sucesso se houver)
4108
- const total = Array.isArray(resp?.results) ? resp.results.length : 0;
4109
- this.batchCount = total;
4110
- const firstOk = resp?.results?.find((r) => r?.file?.fileName)?.file?.fileName;
4111
- if (firstOk) {
4112
- this.lastFileName = String(firstOk);
5283
+ onBulkComplete(data) {
5284
+ const results = data.results;
5285
+ const successfulResults = results.filter((r) => r?.file?.id || r?.file?.fileName);
5286
+ const failedResults = results.filter((r) => !r?.file?.id && !r?.file?.fileName);
5287
+ const successfulFiles = successfulResults
5288
+ .map((result) => result?.file)
5289
+ .filter((file) => !!file);
5290
+ this.batchCount = successfulFiles.length;
5291
+ if (successfulFiles.length > 0) {
5292
+ const value = this.resolveBulkValue(successfulFiles);
5293
+ this.setValue(value);
5294
+ this.lastFileName = successfulFiles[0]?.fileName || null;
5295
+ }
5296
+ if (failedResults.length > 0) {
5297
+ this.applyUploadErrorState(this.t('partialUploadError', 'Upload parcial concluído com falhas em parte do lote.'), 'uploadPartial');
5298
+ }
5299
+ else if (successfulResults.length > 0) {
5300
+ this.clearUploadState();
4113
5301
  }
5302
+ this.bulkComplete.emit(data);
4114
5303
  }
4115
5304
  // When an error occurs, mark the control invalid and show message
4116
5305
  onUploadError(event) {
4117
- this.control().setErrors({ upload: true });
4118
- this.control().markAsTouched();
4119
- this.control().markAsDirty();
4120
- if (event && typeof event === 'object' && 'code' in event) {
4121
- this.uploadError = this.errors.map(event).message;
5306
+ let message;
5307
+ if (event &&
5308
+ typeof event === 'object' &&
5309
+ ('code' in event ||
5310
+ 'errors' in event ||
5311
+ 'status' in event)) {
5312
+ message = this.errors.map(event).message;
4122
5313
  }
4123
5314
  else if (event && typeof event.message === 'string') {
4124
- this.uploadError = event.message;
5315
+ message = event.message;
4125
5316
  }
4126
5317
  else {
4127
- this.uploadError = this.t('genericUploadError', 'Erro no envio de arquivo.');
5318
+ message = this.t('genericUploadError', 'Erro no envio de arquivo.');
4128
5319
  }
5320
+ this.applyUploadErrorState(message, 'upload');
5321
+ this.error.emit(event);
4129
5322
  }
4130
5323
  isShellDisabled() {
4131
5324
  try {
@@ -4135,13 +5328,92 @@ class PdxFilesUploadFieldComponent extends SimpleBaseInputComponent {
4135
5328
  return false;
4136
5329
  }
4137
5330
  }
5331
+ isUploadingState() {
5332
+ return !!this.uploader?.isUploading;
5333
+ }
5334
+ get shellAriaLabel() {
5335
+ const base = this.lastFileName
5336
+ ? `${this.t('fieldSelectedSummary', 'Arquivo selecionado')}: ${this.lastFileName}`
5337
+ : this.t('placeholder', 'Selecione ou solte arquivos…');
5338
+ if (this.pendingFiles.length > 0) {
5339
+ return `${this.pendingFiles.length} ${this.t('fieldPendingAriaSuffix', 'arquivos prontos para envio')}`;
5340
+ }
5341
+ return `${base}. ${this.statusSummary}`;
5342
+ }
5343
+ get statusSummary() {
5344
+ if (this.showUploadError) {
5345
+ return this.t('fieldErrorSummary', 'Corrija o erro para continuar');
5346
+ }
5347
+ if (this.pendingFiles.length > 0) {
5348
+ return this.t('fieldPendingSummary', 'Arquivos prontos para envio');
5349
+ }
5350
+ if (this.batchCount > 1) {
5351
+ return `${this.batchCount} ${this.t('fieldSelectedPlural', 'arquivos vinculados')}`;
5352
+ }
5353
+ if (this.lastFileName) {
5354
+ return this.t('fieldSelectedSummary', 'Arquivo selecionado');
5355
+ }
5356
+ return this.t('fieldEmptySummary', 'Nenhum arquivo selecionado');
5357
+ }
4138
5358
  onProximityChange(isActive) {
4139
5359
  this.isProximityActive = isActive;
4140
5360
  }
4141
5361
  openFromShell() {
4142
5362
  if (this.isShellDisabled())
4143
5363
  return;
4144
- this.uploader?.triggerSelect();
5364
+ this.uploader?.openFileDialog();
5365
+ }
5366
+ get pendingPanelId() {
5367
+ return `${this.componentId()}-pending-panel`;
5368
+ }
5369
+ togglePendingPanel() {
5370
+ if (!this.pendingFiles.length) {
5371
+ this.closePendingPanel(false);
5372
+ return;
5373
+ }
5374
+ if (this.pendingPanelOpen) {
5375
+ this.closePendingPanel();
5376
+ return;
5377
+ }
5378
+ this.pendingPanelOpen = true;
5379
+ this.focusPendingPanelSoon();
5380
+ }
5381
+ closePendingPanel(restoreFocus = true) {
5382
+ if (!this.pendingPanelOpen) {
5383
+ if (restoreFocus) {
5384
+ this.pendingTrigger?.nativeElement.focus();
5385
+ }
5386
+ return;
5387
+ }
5388
+ this.pendingPanelOpen = false;
5389
+ if (restoreFocus) {
5390
+ this.pendingTrigger?.nativeElement.focus();
5391
+ }
5392
+ }
5393
+ onPendingTriggerKeydown(event) {
5394
+ if (event.key === 'ArrowDown' && this.pendingFiles.length > 0) {
5395
+ event.preventDefault();
5396
+ if (!this.pendingPanelOpen) {
5397
+ this.pendingPanelOpen = true;
5398
+ }
5399
+ this.focusPendingPanelSoon();
5400
+ }
5401
+ if (event.key === 'Escape') {
5402
+ event.preventDefault();
5403
+ this.closePendingPanel();
5404
+ }
5405
+ }
5406
+ onPendingPanelKeydown(event) {
5407
+ if (event.key === 'Escape') {
5408
+ event.preventDefault();
5409
+ this.closePendingPanel();
5410
+ }
5411
+ }
5412
+ onPendingOverlayDetach() {
5413
+ if (this.pendingPanelOpen) {
5414
+ this.pendingPanelOpen = false;
5415
+ this.pendingTrigger?.nativeElement.focus();
5416
+ }
4145
5417
  }
4146
5418
  // Ações dos ícones (placeholders para próxima sub-issue)
4147
5419
  onOpenInfo() { }
@@ -4150,117 +5422,215 @@ class PdxFilesUploadFieldComponent extends SimpleBaseInputComponent {
4150
5422
  this.lastFileName = null;
4151
5423
  this.batchCount = 0;
4152
5424
  this.uploadError = null;
5425
+ this.pendingFiles = [];
5426
+ this.uploader?.resetPendingFiles();
4153
5427
  try {
4154
5428
  this.setValue(null);
4155
5429
  const c = this.control();
4156
5430
  c.markAsPristine();
4157
5431
  c.markAsUntouched();
4158
- c.setErrors(null);
5432
+ this.updateUploadValidationState();
4159
5433
  }
4160
5434
  catch { }
4161
5435
  }
4162
5436
  get policySummary() {
4163
- const a = this.config?.ui?.accept?.length ? this.config.ui.accept.join(', ') : '—';
4164
- const maxB = this.config?.limits?.maxFileSizeBytes;
4165
- const maxMb = maxB ? `${(maxB / 1024 / 1024).toFixed(1)} MB` : '—';
4166
- const maxN = this.config?.limits?.maxFilesPerBulk ?? 1;
4167
- return `Tipos: ${a} • Máx/arquivo: ${maxMb} • Máx/itens: ${maxN}`;
5437
+ const accepts = this.config?.ui?.accept?.length
5438
+ ? this.config.ui.accept.join(', ')
5439
+ : this.t('fieldPolicyNotConfigured', 'Não configurado');
5440
+ const maxBytes = this.config?.limits?.maxFileSizeBytes;
5441
+ const maxPerFile = maxBytes
5442
+ ? this.formatBytes(maxBytes)
5443
+ : this.t('fieldPolicyNotConfigured', 'Não configurado');
5444
+ const maxItems = this.config?.limits?.maxFilesPerBulk ?? 1;
5445
+ return [
5446
+ `${this.t('fieldPolicyTypes', 'Tipos')}: ${accepts}`,
5447
+ `${this.t('fieldPolicyMaxPerFile', 'Máx/arquivo')}: ${maxPerFile}`,
5448
+ `${this.t('fieldPolicyMaxItems', 'Máx/itens')}: ${maxItems}`,
5449
+ ].join(' • ');
4168
5450
  }
4169
5451
  onRemovePendingFile(file, event) {
4170
- event.stopPropagation(); // Impede que o menu feche ao clicar no botão
5452
+ event.stopPropagation();
4171
5453
  this.uploader?.removePendingFile(file);
4172
5454
  }
5455
+ pendingFileSummary(file) {
5456
+ return `${file.name} ${this.formatBytes(file.size)}`;
5457
+ }
4173
5458
  formatBytes(bytes) {
4174
- if (bytes === 0)
4175
- return '0 Bytes';
5459
+ if (bytes === 0) {
5460
+ return `0 ${this.t('sizeUnitBytes', 'Bytes')}`;
5461
+ }
4176
5462
  const k = 1024;
4177
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
5463
+ const sizes = [
5464
+ this.t('sizeUnitBytes', 'Bytes'),
5465
+ this.t('sizeUnitKB', 'KB'),
5466
+ this.t('sizeUnitMB', 'MB'),
5467
+ this.t('sizeUnitGB', 'GB'),
5468
+ this.t('sizeUnitTB', 'TB'),
5469
+ ];
4178
5470
  const i = Math.floor(Math.log(bytes) / Math.log(k));
4179
5471
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
4180
5472
  }
5473
+ focusPendingPanelSoon() {
5474
+ setTimeout(() => {
5475
+ this.pendingPanel?.nativeElement.focus();
5476
+ });
5477
+ }
5478
+ updateUploadValidationState(validationKey) {
5479
+ const currentErrors = { ...(this.control().errors ?? {}) };
5480
+ delete currentErrors.upload;
5481
+ delete currentErrors.uploadPartial;
5482
+ if (validationKey) {
5483
+ currentErrors[validationKey] = true;
5484
+ }
5485
+ this.control().setErrors(Object.keys(currentErrors).length ? currentErrors : null);
5486
+ }
4181
5487
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PdxFilesUploadFieldComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4182
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PdxFilesUploadFieldComponent, isStandalone: true, selector: "pdx-material-files-upload", inputs: { config: "config", valueMode: "valueMode", baseUrl: "baseUrl" }, host: { properties: { "class": "componentCssClasses()", "attr.data-field-type": "\"files-upload\"", "attr.data-field-name": "metadata()?.name", "attr.data-component-id": "componentId()" } }, viewQueries: [{ propertyName: "uploader", first: true, predicate: ["uploader"], descendants: true }], usesInheritance: true, ngImport: i0, template: `
5488
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PdxFilesUploadFieldComponent, isStandalone: true, selector: "pdx-material-files-upload", inputs: { config: "config", valueMode: "valueMode", baseUrl: "baseUrl" }, outputs: { uploadSuccess: "uploadSuccess", bulkComplete: "bulkComplete", error: "error" }, host: { properties: { "class": "componentCssClasses()", "attr.data-field-type": "\"files-upload\"", "attr.data-field-name": "metadata()?.name", "attr.data-component-id": "componentId()" } }, providers: [providePraxisFilesUploadI18n()], viewQueries: [{ propertyName: "uploader", first: true, predicate: ["uploader"], descendants: true }, { propertyName: "pendingTrigger", first: true, predicate: ["pendingTrigger"], descendants: true }, { propertyName: "pendingPanel", first: true, predicate: ["pendingPanel"], descendants: true }], usesInheritance: true, ngImport: i0, template: `
4183
5489
  <!-- Casca compacta tipo input -->
4184
5490
  <div class="pdx-upload-field-shell"
4185
- role="button"
4186
- tabindex="0"
4187
- [attr.aria-describedby]="componentId() + '-messages'"
5491
+ role="group"
4188
5492
  [class.error]="showUploadError"
4189
5493
  [class.disabled]="isShellDisabled()"
4190
- [attr.aria-disabled]="isShellDisabled() ? 'true' : null"
4191
5494
  [class.proximity-active]="isProximityActive"
4192
- (click)="pendingFiles.length === 0 && openFromShell()"
4193
- (keydown.enter)="pendingFiles.length === 0 && openFromShell(); $event.preventDefault()"
4194
- (keydown.space)="pendingFiles.length === 0 && openFromShell(); $event.preventDefault()">
5495
+ [attr.aria-busy]="isUploadingState()"
5496
+ [attr.aria-invalid]="showUploadError"
5497
+ [attr.aria-label]="shellAriaLabel">
4195
5498
  <mat-icon class="prefix" aria-hidden="true">attach_file</mat-icon>
4196
5499
  <div class="content">
4197
5500
  <!-- ESTADO 1: Sem arquivos pendentes -->
4198
- <ng-container *ngIf="pendingFiles.length === 0; else pendingState">
4199
- <span class="placeholder" *ngIf="!lastFileName">{{ t('placeholder','Selecione ou solte arquivos…') }}</span>
4200
- <span class="value" *ngIf="lastFileName" [title]="lastFileName">{{ lastFileName }}</span>
4201
- <span class="chip" *ngIf="batchCount > 1">+{{ batchCount - 1 }}</span>
5501
+ <ng-container *ngIf="pendingFiles.length === 0">
5502
+ <button
5503
+ type="button"
5504
+ class="shell-main-btn"
5505
+ [disabled]="isShellDisabled()"
5506
+ [attr.aria-describedby]="componentId() + '-messages'"
5507
+ (click)="openFromShell()"
5508
+ >
5509
+ <span class="text-stack">
5510
+ <span class="placeholder" *ngIf="!lastFileName">{{ t('placeholder','Selecione ou solte arquivos…') }}</span>
5511
+ <span class="value" *ngIf="lastFileName" [title]="lastFileName">{{ lastFileName }}</span>
5512
+ <span class="meta">{{ statusSummary }}</span>
5513
+ </span>
5514
+ <span class="chip" *ngIf="batchCount > 1" [attr.aria-label]="additionalFilesChipAriaLabel">+{{ batchCount - 1 }}</span>
5515
+ </button>
4202
5516
  </ng-container>
4203
5517
 
4204
5518
  <!-- ESTADO 2: Com arquivos pendentes -->
4205
- <ng-template #pendingState>
4206
- <button type="button" class="pending-text-btn" [matMenuTriggerFor]="pendingFilesMenu">
4207
- <span>{{ pendingFiles.length }} arquivo(s) selecionado(s)</span>
4208
- <mat-icon>arrow_drop_down</mat-icon>
5519
+ <ng-container *ngIf="pendingFiles.length > 0">
5520
+ <button
5521
+ #pendingTrigger
5522
+ type="button"
5523
+ class="pending-text-btn"
5524
+ [attr.aria-describedby]="componentId() + '-messages'"
5525
+ [attr.aria-controls]="pendingPanelId"
5526
+ [attr.aria-expanded]="pendingPanelOpen"
5527
+ cdkOverlayOrigin
5528
+ #pendingOverlayOrigin="cdkOverlayOrigin"
5529
+ (click)="togglePendingPanel()"
5530
+ (keydown)="onPendingTriggerKeydown($event)"
5531
+ >
5532
+ <span class="text-stack">
5533
+ <span class="value">{{ pendingFiles.length }} {{ t('fieldPendingCountSuffix','file(s) selected') }}</span>
5534
+ <span class="meta">{{ t('fieldPendingSummary','Arquivos prontos para envio') }}</span>
5535
+ </span>
5536
+ <mat-icon aria-hidden="true">arrow_drop_down</mat-icon>
4209
5537
  </button>
4210
- </ng-template>
5538
+ <ng-template
5539
+ cdkConnectedOverlay
5540
+ [cdkConnectedOverlayOrigin]="pendingOverlayOrigin"
5541
+ [cdkConnectedOverlayOpen]="pendingPanelOpen"
5542
+ [cdkConnectedOverlayHasBackdrop]="true"
5543
+ cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
5544
+ [cdkConnectedOverlayPositions]="pendingOverlayPositions"
5545
+ (backdropClick)="closePendingPanel()"
5546
+ (detach)="onPendingOverlayDetach()"
5547
+ >
5548
+ <div
5549
+ #pendingPanel
5550
+ class="pending-files-overlay"
5551
+ [id]="pendingPanelId"
5552
+ role="list"
5553
+ tabindex="-1"
5554
+ [attr.aria-label]="t('fieldPendingSummary','Arquivos prontos para envio')"
5555
+ (keydown)="onPendingPanelKeydown($event)"
5556
+ >
5557
+ <div
5558
+ class="pending-file-item"
5559
+ *ngFor="let file of pendingFiles"
5560
+ role="listitem"
5561
+ [attr.aria-label]="pendingFileSummary(file)"
5562
+ >
5563
+ <div class="file-info" [title]="file.name">
5564
+ <span class="file-name">{{ file.name }}</span>
5565
+ <span class="file-size">({{ formatBytes(file.size) }})</span>
5566
+ </div>
5567
+ <span class="spacer"></span>
5568
+ <button
5569
+ type="button"
5570
+ mat-icon-button
5571
+ class="remove-pending-btn"
5572
+ (click)="onRemovePendingFile(file, $event)"
5573
+ [attr.aria-label]="t('removePendingFile','Remove selected file') + ': ' + file.name"
5574
+ >
5575
+ <mat-icon>close</mat-icon>
5576
+ </button>
5577
+ </div>
5578
+ </div>
5579
+ </ng-template>
5580
+ </ng-container>
4211
5581
  </div>
4212
5582
  <div class="suffixes">
4213
5583
  <!-- Ações para arquivos pendentes -->
4214
5584
  <ng-container *ngIf="pendingFiles.length > 0">
4215
- <button type="button" class="icon-btn" color="primary" title="Enviar" aria-label="Enviar" (click)="onTriggerUpload(); $event.stopPropagation()">
4216
- <mat-icon>upload</mat-icon>
5585
+ <button
5586
+ type="button"
5587
+ class="action-btn primary"
5588
+ [attr.aria-label]="t('fieldActionUpload','Enviar arquivos selecionados')"
5589
+ (click)="onTriggerUpload(); $event.stopPropagation()"
5590
+ >
5591
+ <mat-icon aria-hidden="true">upload</mat-icon>
5592
+ <span>{{ t('fieldActionUploadShort','Enviar') }}</span>
4217
5593
  </button>
4218
- <button type="button" class="icon-btn" color="warn" title="Cancelar Seleção" aria-label="Cancelar Seleção" (click)="onClearSelection(); $event.stopPropagation()">
4219
- <mat-icon>cancel</mat-icon>
5594
+ <button
5595
+ type="button"
5596
+ class="action-btn"
5597
+ [attr.aria-label]="t('fieldActionCancel','Cancelar seleção atual')"
5598
+ (click)="onClearSelection(); $event.stopPropagation()"
5599
+ >
5600
+ <mat-icon aria-hidden="true">cancel</mat-icon>
5601
+ <span>{{ t('fieldActionCancelShort','Cancelar') }}</span>
4220
5602
  </button>
4221
5603
  </ng-container>
4222
5604
 
4223
5605
  <!-- Ações padrão -->
4224
- <button type="button" class="icon-btn" title="Informações" aria-label="Informações" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matTooltip]="policySummary">
5606
+ <button type="button" class="icon-btn" [attr.title]="t('fieldInfoAction','Informações')" [attr.aria-label]="t('fieldInfoAction','Informações')" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matTooltip]="policySummary">
4225
5607
  <mat-icon>info</mat-icon>
4226
5608
  </button>
4227
- <button type="button" class="icon-btn" title="Mais ações" aria-label="Mais ações" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matMenuTriggerFor]="moreMenu">
5609
+ <button type="button" class="icon-btn" [attr.title]="t('fieldMoreActions','Mais ações')" [attr.aria-label]="t('fieldMoreActions','Mais ações')" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matMenuTriggerFor]="moreMenu">
4228
5610
  <mat-icon>more_horiz</mat-icon>
4229
5611
  </button>
4230
5612
  <mat-menu #moreMenu="matMenu">
4231
5613
  <button mat-menu-item (click)="openFromShell()">
4232
5614
  <mat-icon>upload_file</mat-icon>
4233
- <span>Selecionar arquivo(s)</span>
5615
+ <span>{{ t('fieldActionSelect','Selecionar arquivo(s)') }}</span>
4234
5616
  </button>
4235
5617
  <button mat-menu-item (click)="onClear()" [disabled]="!lastFileName && batchCount === 0">
4236
5618
  <mat-icon>clear</mat-icon>
4237
- <span>Limpar</span>
5619
+ <span>{{ t('fieldActionClear','Limpar') }}</span>
4238
5620
  </button>
4239
5621
  </mat-menu>
4240
5622
  </div>
4241
5623
  </div>
4242
5624
 
4243
- <!-- Menu para exibir arquivos pendentes -->
4244
- <mat-menu #pendingFilesMenu="matMenu" class="pending-files-menu">
4245
- <ng-template matMenuContent>
4246
- <button mat-menu-item *ngFor="let file of pendingFiles" (click)="$event.stopPropagation(); $event.preventDefault();" [title]="file.name">
4247
- <div class="pending-file-item">
4248
- <div class="file-info">
4249
- <span class="file-name">{{ file.name }}</span>
4250
- <span class="file-size">({{ formatBytes(file.size) }})</span>
4251
- </div>
4252
- <span class="spacer"></span>
4253
- <button mat-icon-button class="remove-pending-btn" (click)="onRemovePendingFile(file, $event)" [attr.aria-label]="'Remover ' + file.name">
4254
- <mat-icon>close</mat-icon>
4255
- </button>
4256
- </div>
4257
- </button>
4258
- </ng-template>
4259
- </mat-menu>
4260
-
4261
5625
  <!-- Mensagens do campo (hint/erro) -->
4262
- <div class="field-messages" [id]="componentId() + '-messages'">
5626
+ <div
5627
+ class="field-messages"
5628
+ [id]="componentId() + '-messages'"
5629
+ [attr.role]="showUploadError ? 'alert' : 'status'"
5630
+ [attr.aria-live]="showUploadError ? 'assertive' : 'polite'"
5631
+ >
4263
5632
  <div class="hint" *ngIf="metadata()?.hint && !showUploadError">{{ metadata()?.hint }}</div>
5633
+ <div class="hint" *ngIf="!metadata()?.hint && !showUploadError">{{ statusSummary }}</div>
4264
5634
  <div class="error" *ngIf="showUploadError">{{ uploadError || t('genericUploadError','Erro no envio de arquivo.') }}</div>
4265
5635
  </div>
4266
5636
 
@@ -4275,94 +5645,167 @@ class PdxFilesUploadFieldComponent extends SimpleBaseInputComponent {
4275
5645
  (uploadSuccess)="onUploadSuccess($event)"
4276
5646
  (bulkComplete)="onBulkComplete($event)"
4277
5647
  (error)="onUploadError($event)"
4278
- (pendingFilesChange)="handlePendingFiles($event)"
5648
+ (proximityChange)="onProximityChange($event)"
5649
+ (pendingStateChange)="handlePendingState($event)"
4279
5650
  />
4280
- `, isInline: true, styles: [":host{position:relative}.pdx-upload-field-shell{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border:1px solid var(--pfx-surface-border, #ccc);border-radius:8px;background:var(--pfx-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface);min-height:40px;cursor:pointer}.pdx-upload-field-shell:focus-within{outline:2px solid var(--md-sys-color-primary,#1976d2);outline-offset:2px}.pdx-upload-field-shell.error{border-color:var(--md-sys-color-error,#b00020)}.pdx-upload-field-shell.disabled{opacity:.6;cursor:not-allowed}.pdx-upload-field-shell .prefix{color:var(--md-sys-color-primary,#1976d2)}.pdx-upload-field-shell .content{flex:1;min-width:0;display:flex;align-items:center;gap:.5rem}.pdx-upload-field-shell .placeholder{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pdx-upload-field-shell .chip{font-size:.75rem;padding:0 .4rem;border-radius:999px;background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.pdx-upload-field-shell .suffixes{display:flex;align-items:center;gap:.25rem}.pdx-upload-field-shell .icon-btn{border:0;background:transparent;padding:.25rem;border-radius:6px;cursor:pointer;color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .icon-btn:hover{background:#ffffff0d}.pdx-upload-field-shell.disabled .icon-btn{cursor:not-allowed}.field-messages{font-size:.82rem;margin-top:.25rem}.field-messages .hint{opacity:.8}.field-messages .error{color:var(--md-sys-color-error,#b00020)}.pending-text-btn{display:flex;align-items:center;gap:.25rem;background:0;border:0;color:var(--md-sys-color-on-surface);font-family:inherit;font-size:inherit;padding:0;margin:0;cursor:pointer}.pending-text-btn span,.pending-text-btn mat-icon{color:inherit}.pending-files-menu .mat-mdc-menu-content{padding:0}.pending-file-item{display:flex;align-items:center;width:100%;gap:.5rem;padding:8px 16px}.pending-file-item .file-info{flex:1;min-width:0;text-align:left}.pending-file-item .file-name{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pending-file-item .file-size{font-size:.8rem;opacity:.7}.pending-file-item .spacer{flex:1 1 auto}.pending-file-item .remove-pending-btn{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell.proximity-active{border-style:dashed;border-color:var(--md-sys-color-primary);transform:scale(1.02);background:var(--md-sys-color-surface-container-low);transition:all .2s ease-in-out}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i7.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i7.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: PraxisFilesUpload, selector: "praxis-files-upload", inputs: ["config", "filesUploadId", "uploadId", "componentInstanceId", "baseUrl", "displayMode", "context", "editModeEnabled"], outputs: ["uploadSuccess", "bulkComplete", "error", "rateLimited", "uploadStart", "uploadProgress", "uploadError", "retry", "download", "copyLink", "detailsOpened", "detailsClosed", "pendingFilesChange", "proximityChange"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i10.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i3$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i3$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i3$1.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i3$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
5651
+ `, isInline: true, styles: [":host{position:relative}.pdx-upload-field-shell{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border:1px solid var(--pfx-surface-border, #ccc);border-radius:8px;background:var(--pfx-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface);min-height:40px}.pdx-upload-field-shell:focus-within{outline:2px solid var(--md-sys-color-primary,#1976d2);outline-offset:2px}.pdx-upload-field-shell.error{border-color:var(--md-sys-color-error,#b00020)}.pdx-upload-field-shell.disabled{opacity:.6;cursor:not-allowed}.pdx-upload-field-shell .prefix{color:var(--md-sys-color-primary,#1976d2)}.pdx-upload-field-shell .content{flex:1;min-width:0;display:flex;align-items:center;gap:.5rem}.pdx-upload-field-shell .shell-main-btn{display:flex;align-items:center;gap:.75rem;flex:1;min-width:0;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.pdx-upload-field-shell .shell-main-btn:disabled{cursor:not-allowed}.pdx-upload-field-shell .text-stack{display:flex;flex-direction:column;min-width:0;gap:.125rem}.pdx-upload-field-shell .placeholder{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pdx-upload-field-shell .meta{font-size:.78rem;color:var(--md-sys-color-on-surface-variant);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pdx-upload-field-shell .chip{font-size:.75rem;padding:0 .4rem;border-radius:999px;background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.pdx-upload-field-shell .suffixes{display:flex;align-items:center;gap:.375rem}.pdx-upload-field-shell .action-btn{display:inline-flex;align-items:center;gap:.35rem;border:1px solid var(--pfx-surface-border, #ccc);background:transparent;padding:.35rem .6rem;border-radius:999px;cursor:pointer;color:var(--md-sys-color-on-surface);font:inherit}.pdx-upload-field-shell .action-btn.primary{background:var(--md-sys-color-primary);border-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary)}.pdx-upload-field-shell .action-btn mat-icon{font-size:18px;width:18px;height:18px}.pdx-upload-field-shell .icon-btn{border:0;background:transparent;padding:.25rem;border-radius:6px;cursor:pointer;color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .icon-btn:hover{background:#ffffff0d}.pdx-upload-field-shell.disabled .icon-btn,.pdx-upload-field-shell.disabled .action-btn{cursor:not-allowed}.field-messages{font-size:.82rem;margin-top:.25rem}.field-messages .hint{opacity:.8}.field-messages .error{color:var(--md-sys-color-error,#b00020)}.pending-text-btn{display:flex;align-items:center;gap:.5rem;background:0;border:0;color:var(--md-sys-color-on-surface);font-family:inherit;font-size:inherit;padding:0;margin:0;cursor:pointer;min-width:0;text-align:left}.pending-text-btn span,.pending-text-btn mat-icon{color:inherit}.pending-files-overlay{display:flex;flex-direction:column;min-width:min(320px,calc(100vw - 32px));max-width:min(480px,calc(100vw - 32px));max-height:240px;overflow:auto;padding:.25rem 0;border:1px solid var(--pfx-surface-border, #ccc);border-radius:12px;background:var(--pfx-surface, var(--md-sys-color-surface));box-shadow:0 12px 30px #0000002e;outline:none}.pending-file-item{display:flex;align-items:center;width:100%;gap:.5rem;padding:8px 16px}.pending-file-item .file-info{flex:1;min-width:0;text-align:left}.pending-file-item .file-name{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pending-file-item .file-size{font-size:.8rem;opacity:.7}.pending-file-item .spacer{flex:1 1 auto}.pending-file-item .remove-pending-btn{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell.proximity-active{border-style:dashed;border-color:var(--md-sys-color-primary);transform:scale(1.02);background:var(--md-sys-color-surface-container-low);transition:all .2s ease-in-out}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i9.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i9.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: PraxisFilesUpload, selector: "praxis-files-upload", inputs: ["config", "filesUploadId", "componentInstanceId", "baseUrl", "displayMode", "context", "editModeEnabled"], outputs: ["uploadSuccess", "bulkComplete", "error", "rateLimited", "uploadStart", "uploadProgress", "retry", "download", "copyLink", "detailsOpened", "detailsClosed", "pendingStateChange", "proximityChange", "readinessChange"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i10.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i14.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i14.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i14.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }] });
4281
5652
  }
4282
5653
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PdxFilesUploadFieldComponent, decorators: [{
4283
5654
  type: Component,
4284
- args: [{ selector: 'pdx-material-files-upload', standalone: true, imports: [CommonModule, PraxisFilesUpload, MatIconModule, MatMenuModule, MatTooltipModule], template: `
5655
+ args: [{ selector: 'pdx-material-files-upload', standalone: true, providers: [providePraxisFilesUploadI18n()], imports: [
5656
+ CommonModule,
5657
+ PraxisFilesUpload,
5658
+ MatIconModule,
5659
+ MatMenuModule,
5660
+ MatTooltipModule,
5661
+ CdkConnectedOverlay,
5662
+ CdkOverlayOrigin,
5663
+ ], template: `
4285
5664
  <!-- Casca compacta tipo input -->
4286
5665
  <div class="pdx-upload-field-shell"
4287
- role="button"
4288
- tabindex="0"
4289
- [attr.aria-describedby]="componentId() + '-messages'"
5666
+ role="group"
4290
5667
  [class.error]="showUploadError"
4291
5668
  [class.disabled]="isShellDisabled()"
4292
- [attr.aria-disabled]="isShellDisabled() ? 'true' : null"
4293
5669
  [class.proximity-active]="isProximityActive"
4294
- (click)="pendingFiles.length === 0 && openFromShell()"
4295
- (keydown.enter)="pendingFiles.length === 0 && openFromShell(); $event.preventDefault()"
4296
- (keydown.space)="pendingFiles.length === 0 && openFromShell(); $event.preventDefault()">
5670
+ [attr.aria-busy]="isUploadingState()"
5671
+ [attr.aria-invalid]="showUploadError"
5672
+ [attr.aria-label]="shellAriaLabel">
4297
5673
  <mat-icon class="prefix" aria-hidden="true">attach_file</mat-icon>
4298
5674
  <div class="content">
4299
5675
  <!-- ESTADO 1: Sem arquivos pendentes -->
4300
- <ng-container *ngIf="pendingFiles.length === 0; else pendingState">
4301
- <span class="placeholder" *ngIf="!lastFileName">{{ t('placeholder','Selecione ou solte arquivos…') }}</span>
4302
- <span class="value" *ngIf="lastFileName" [title]="lastFileName">{{ lastFileName }}</span>
4303
- <span class="chip" *ngIf="batchCount > 1">+{{ batchCount - 1 }}</span>
5676
+ <ng-container *ngIf="pendingFiles.length === 0">
5677
+ <button
5678
+ type="button"
5679
+ class="shell-main-btn"
5680
+ [disabled]="isShellDisabled()"
5681
+ [attr.aria-describedby]="componentId() + '-messages'"
5682
+ (click)="openFromShell()"
5683
+ >
5684
+ <span class="text-stack">
5685
+ <span class="placeholder" *ngIf="!lastFileName">{{ t('placeholder','Selecione ou solte arquivos…') }}</span>
5686
+ <span class="value" *ngIf="lastFileName" [title]="lastFileName">{{ lastFileName }}</span>
5687
+ <span class="meta">{{ statusSummary }}</span>
5688
+ </span>
5689
+ <span class="chip" *ngIf="batchCount > 1" [attr.aria-label]="additionalFilesChipAriaLabel">+{{ batchCount - 1 }}</span>
5690
+ </button>
4304
5691
  </ng-container>
4305
5692
 
4306
5693
  <!-- ESTADO 2: Com arquivos pendentes -->
4307
- <ng-template #pendingState>
4308
- <button type="button" class="pending-text-btn" [matMenuTriggerFor]="pendingFilesMenu">
4309
- <span>{{ pendingFiles.length }} arquivo(s) selecionado(s)</span>
4310
- <mat-icon>arrow_drop_down</mat-icon>
5694
+ <ng-container *ngIf="pendingFiles.length > 0">
5695
+ <button
5696
+ #pendingTrigger
5697
+ type="button"
5698
+ class="pending-text-btn"
5699
+ [attr.aria-describedby]="componentId() + '-messages'"
5700
+ [attr.aria-controls]="pendingPanelId"
5701
+ [attr.aria-expanded]="pendingPanelOpen"
5702
+ cdkOverlayOrigin
5703
+ #pendingOverlayOrigin="cdkOverlayOrigin"
5704
+ (click)="togglePendingPanel()"
5705
+ (keydown)="onPendingTriggerKeydown($event)"
5706
+ >
5707
+ <span class="text-stack">
5708
+ <span class="value">{{ pendingFiles.length }} {{ t('fieldPendingCountSuffix','file(s) selected') }}</span>
5709
+ <span class="meta">{{ t('fieldPendingSummary','Arquivos prontos para envio') }}</span>
5710
+ </span>
5711
+ <mat-icon aria-hidden="true">arrow_drop_down</mat-icon>
4311
5712
  </button>
4312
- </ng-template>
5713
+ <ng-template
5714
+ cdkConnectedOverlay
5715
+ [cdkConnectedOverlayOrigin]="pendingOverlayOrigin"
5716
+ [cdkConnectedOverlayOpen]="pendingPanelOpen"
5717
+ [cdkConnectedOverlayHasBackdrop]="true"
5718
+ cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
5719
+ [cdkConnectedOverlayPositions]="pendingOverlayPositions"
5720
+ (backdropClick)="closePendingPanel()"
5721
+ (detach)="onPendingOverlayDetach()"
5722
+ >
5723
+ <div
5724
+ #pendingPanel
5725
+ class="pending-files-overlay"
5726
+ [id]="pendingPanelId"
5727
+ role="list"
5728
+ tabindex="-1"
5729
+ [attr.aria-label]="t('fieldPendingSummary','Arquivos prontos para envio')"
5730
+ (keydown)="onPendingPanelKeydown($event)"
5731
+ >
5732
+ <div
5733
+ class="pending-file-item"
5734
+ *ngFor="let file of pendingFiles"
5735
+ role="listitem"
5736
+ [attr.aria-label]="pendingFileSummary(file)"
5737
+ >
5738
+ <div class="file-info" [title]="file.name">
5739
+ <span class="file-name">{{ file.name }}</span>
5740
+ <span class="file-size">({{ formatBytes(file.size) }})</span>
5741
+ </div>
5742
+ <span class="spacer"></span>
5743
+ <button
5744
+ type="button"
5745
+ mat-icon-button
5746
+ class="remove-pending-btn"
5747
+ (click)="onRemovePendingFile(file, $event)"
5748
+ [attr.aria-label]="t('removePendingFile','Remove selected file') + ': ' + file.name"
5749
+ >
5750
+ <mat-icon>close</mat-icon>
5751
+ </button>
5752
+ </div>
5753
+ </div>
5754
+ </ng-template>
5755
+ </ng-container>
4313
5756
  </div>
4314
5757
  <div class="suffixes">
4315
5758
  <!-- Ações para arquivos pendentes -->
4316
5759
  <ng-container *ngIf="pendingFiles.length > 0">
4317
- <button type="button" class="icon-btn" color="primary" title="Enviar" aria-label="Enviar" (click)="onTriggerUpload(); $event.stopPropagation()">
4318
- <mat-icon>upload</mat-icon>
5760
+ <button
5761
+ type="button"
5762
+ class="action-btn primary"
5763
+ [attr.aria-label]="t('fieldActionUpload','Enviar arquivos selecionados')"
5764
+ (click)="onTriggerUpload(); $event.stopPropagation()"
5765
+ >
5766
+ <mat-icon aria-hidden="true">upload</mat-icon>
5767
+ <span>{{ t('fieldActionUploadShort','Enviar') }}</span>
4319
5768
  </button>
4320
- <button type="button" class="icon-btn" color="warn" title="Cancelar Seleção" aria-label="Cancelar Seleção" (click)="onClearSelection(); $event.stopPropagation()">
4321
- <mat-icon>cancel</mat-icon>
5769
+ <button
5770
+ type="button"
5771
+ class="action-btn"
5772
+ [attr.aria-label]="t('fieldActionCancel','Cancelar seleção atual')"
5773
+ (click)="onClearSelection(); $event.stopPropagation()"
5774
+ >
5775
+ <mat-icon aria-hidden="true">cancel</mat-icon>
5776
+ <span>{{ t('fieldActionCancelShort','Cancelar') }}</span>
4322
5777
  </button>
4323
5778
  </ng-container>
4324
5779
 
4325
5780
  <!-- Ações padrão -->
4326
- <button type="button" class="icon-btn" title="Informações" aria-label="Informações" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matTooltip]="policySummary">
5781
+ <button type="button" class="icon-btn" [attr.title]="t('fieldInfoAction','Informações')" [attr.aria-label]="t('fieldInfoAction','Informações')" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matTooltip]="policySummary">
4327
5782
  <mat-icon>info</mat-icon>
4328
5783
  </button>
4329
- <button type="button" class="icon-btn" title="Mais ações" aria-label="Mais ações" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matMenuTriggerFor]="moreMenu">
5784
+ <button type="button" class="icon-btn" [attr.title]="t('fieldMoreActions','Mais ações')" [attr.aria-label]="t('fieldMoreActions','Mais ações')" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matMenuTriggerFor]="moreMenu">
4330
5785
  <mat-icon>more_horiz</mat-icon>
4331
5786
  </button>
4332
5787
  <mat-menu #moreMenu="matMenu">
4333
5788
  <button mat-menu-item (click)="openFromShell()">
4334
5789
  <mat-icon>upload_file</mat-icon>
4335
- <span>Selecionar arquivo(s)</span>
5790
+ <span>{{ t('fieldActionSelect','Selecionar arquivo(s)') }}</span>
4336
5791
  </button>
4337
5792
  <button mat-menu-item (click)="onClear()" [disabled]="!lastFileName && batchCount === 0">
4338
5793
  <mat-icon>clear</mat-icon>
4339
- <span>Limpar</span>
5794
+ <span>{{ t('fieldActionClear','Limpar') }}</span>
4340
5795
  </button>
4341
5796
  </mat-menu>
4342
5797
  </div>
4343
5798
  </div>
4344
5799
 
4345
- <!-- Menu para exibir arquivos pendentes -->
4346
- <mat-menu #pendingFilesMenu="matMenu" class="pending-files-menu">
4347
- <ng-template matMenuContent>
4348
- <button mat-menu-item *ngFor="let file of pendingFiles" (click)="$event.stopPropagation(); $event.preventDefault();" [title]="file.name">
4349
- <div class="pending-file-item">
4350
- <div class="file-info">
4351
- <span class="file-name">{{ file.name }}</span>
4352
- <span class="file-size">({{ formatBytes(file.size) }})</span>
4353
- </div>
4354
- <span class="spacer"></span>
4355
- <button mat-icon-button class="remove-pending-btn" (click)="onRemovePendingFile(file, $event)" [attr.aria-label]="'Remover ' + file.name">
4356
- <mat-icon>close</mat-icon>
4357
- </button>
4358
- </div>
4359
- </button>
4360
- </ng-template>
4361
- </mat-menu>
4362
-
4363
5800
  <!-- Mensagens do campo (hint/erro) -->
4364
- <div class="field-messages" [id]="componentId() + '-messages'">
5801
+ <div
5802
+ class="field-messages"
5803
+ [id]="componentId() + '-messages'"
5804
+ [attr.role]="showUploadError ? 'alert' : 'status'"
5805
+ [attr.aria-live]="showUploadError ? 'assertive' : 'polite'"
5806
+ >
4365
5807
  <div class="hint" *ngIf="metadata()?.hint && !showUploadError">{{ metadata()?.hint }}</div>
5808
+ <div class="hint" *ngIf="!metadata()?.hint && !showUploadError">{{ statusSummary }}</div>
4366
5809
  <div class="error" *ngIf="showUploadError">{{ uploadError || t('genericUploadError','Erro no envio de arquivo.') }}</div>
4367
5810
  </div>
4368
5811
 
@@ -4377,83 +5820,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
4377
5820
  (uploadSuccess)="onUploadSuccess($event)"
4378
5821
  (bulkComplete)="onBulkComplete($event)"
4379
5822
  (error)="onUploadError($event)"
4380
- (pendingFilesChange)="handlePendingFiles($event)"
5823
+ (proximityChange)="onProximityChange($event)"
5824
+ (pendingStateChange)="handlePendingState($event)"
4381
5825
  />
4382
5826
  `, host: {
4383
5827
  '[class]': 'componentCssClasses()',
4384
5828
  '[attr.data-field-type]': '"files-upload"',
4385
5829
  '[attr.data-field-name]': 'metadata()?.name',
4386
5830
  '[attr.data-component-id]': 'componentId()',
4387
- }, styles: [":host{position:relative}.pdx-upload-field-shell{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border:1px solid var(--pfx-surface-border, #ccc);border-radius:8px;background:var(--pfx-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface);min-height:40px;cursor:pointer}.pdx-upload-field-shell:focus-within{outline:2px solid var(--md-sys-color-primary,#1976d2);outline-offset:2px}.pdx-upload-field-shell.error{border-color:var(--md-sys-color-error,#b00020)}.pdx-upload-field-shell.disabled{opacity:.6;cursor:not-allowed}.pdx-upload-field-shell .prefix{color:var(--md-sys-color-primary,#1976d2)}.pdx-upload-field-shell .content{flex:1;min-width:0;display:flex;align-items:center;gap:.5rem}.pdx-upload-field-shell .placeholder{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pdx-upload-field-shell .chip{font-size:.75rem;padding:0 .4rem;border-radius:999px;background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.pdx-upload-field-shell .suffixes{display:flex;align-items:center;gap:.25rem}.pdx-upload-field-shell .icon-btn{border:0;background:transparent;padding:.25rem;border-radius:6px;cursor:pointer;color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .icon-btn:hover{background:#ffffff0d}.pdx-upload-field-shell.disabled .icon-btn{cursor:not-allowed}.field-messages{font-size:.82rem;margin-top:.25rem}.field-messages .hint{opacity:.8}.field-messages .error{color:var(--md-sys-color-error,#b00020)}.pending-text-btn{display:flex;align-items:center;gap:.25rem;background:0;border:0;color:var(--md-sys-color-on-surface);font-family:inherit;font-size:inherit;padding:0;margin:0;cursor:pointer}.pending-text-btn span,.pending-text-btn mat-icon{color:inherit}.pending-files-menu .mat-mdc-menu-content{padding:0}.pending-file-item{display:flex;align-items:center;width:100%;gap:.5rem;padding:8px 16px}.pending-file-item .file-info{flex:1;min-width:0;text-align:left}.pending-file-item .file-name{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pending-file-item .file-size{font-size:.8rem;opacity:.7}.pending-file-item .spacer{flex:1 1 auto}.pending-file-item .remove-pending-btn{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell.proximity-active{border-style:dashed;border-color:var(--md-sys-color-primary);transform:scale(1.02);background:var(--md-sys-color-surface-container-low);transition:all .2s ease-in-out}\n"] }]
5831
+ }, styles: [":host{position:relative}.pdx-upload-field-shell{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border:1px solid var(--pfx-surface-border, #ccc);border-radius:8px;background:var(--pfx-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface);min-height:40px}.pdx-upload-field-shell:focus-within{outline:2px solid var(--md-sys-color-primary,#1976d2);outline-offset:2px}.pdx-upload-field-shell.error{border-color:var(--md-sys-color-error,#b00020)}.pdx-upload-field-shell.disabled{opacity:.6;cursor:not-allowed}.pdx-upload-field-shell .prefix{color:var(--md-sys-color-primary,#1976d2)}.pdx-upload-field-shell .content{flex:1;min-width:0;display:flex;align-items:center;gap:.5rem}.pdx-upload-field-shell .shell-main-btn{display:flex;align-items:center;gap:.75rem;flex:1;min-width:0;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.pdx-upload-field-shell .shell-main-btn:disabled{cursor:not-allowed}.pdx-upload-field-shell .text-stack{display:flex;flex-direction:column;min-width:0;gap:.125rem}.pdx-upload-field-shell .placeholder{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pdx-upload-field-shell .meta{font-size:.78rem;color:var(--md-sys-color-on-surface-variant);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pdx-upload-field-shell .chip{font-size:.75rem;padding:0 .4rem;border-radius:999px;background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.pdx-upload-field-shell .suffixes{display:flex;align-items:center;gap:.375rem}.pdx-upload-field-shell .action-btn{display:inline-flex;align-items:center;gap:.35rem;border:1px solid var(--pfx-surface-border, #ccc);background:transparent;padding:.35rem .6rem;border-radius:999px;cursor:pointer;color:var(--md-sys-color-on-surface);font:inherit}.pdx-upload-field-shell .action-btn.primary{background:var(--md-sys-color-primary);border-color:var(--md-sys-color-primary);color:var(--md-sys-color-on-primary)}.pdx-upload-field-shell .action-btn mat-icon{font-size:18px;width:18px;height:18px}.pdx-upload-field-shell .icon-btn{border:0;background:transparent;padding:.25rem;border-radius:6px;cursor:pointer;color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .icon-btn:hover{background:#ffffff0d}.pdx-upload-field-shell.disabled .icon-btn,.pdx-upload-field-shell.disabled .action-btn{cursor:not-allowed}.field-messages{font-size:.82rem;margin-top:.25rem}.field-messages .hint{opacity:.8}.field-messages .error{color:var(--md-sys-color-error,#b00020)}.pending-text-btn{display:flex;align-items:center;gap:.5rem;background:0;border:0;color:var(--md-sys-color-on-surface);font-family:inherit;font-size:inherit;padding:0;margin:0;cursor:pointer;min-width:0;text-align:left}.pending-text-btn span,.pending-text-btn mat-icon{color:inherit}.pending-files-overlay{display:flex;flex-direction:column;min-width:min(320px,calc(100vw - 32px));max-width:min(480px,calc(100vw - 32px));max-height:240px;overflow:auto;padding:.25rem 0;border:1px solid var(--pfx-surface-border, #ccc);border-radius:12px;background:var(--pfx-surface, var(--md-sys-color-surface));box-shadow:0 12px 30px #0000002e;outline:none}.pending-file-item{display:flex;align-items:center;width:100%;gap:.5rem;padding:8px 16px}.pending-file-item .file-info{flex:1;min-width:0;text-align:left}.pending-file-item .file-name{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pending-file-item .file-size{font-size:.8rem;opacity:.7}.pending-file-item .spacer{flex:1 1 auto}.pending-file-item .remove-pending-btn{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell.proximity-active{border-style:dashed;border-color:var(--md-sys-color-primary);transform:scale(1.02);background:var(--md-sys-color-surface-container-low);transition:all .2s ease-in-out}\n"] }]
4388
5832
  }], propDecorators: { config: [{
4389
5833
  type: Input
4390
5834
  }], valueMode: [{
4391
5835
  type: Input
4392
5836
  }], baseUrl: [{
4393
5837
  type: Input
5838
+ }], uploadSuccess: [{
5839
+ type: Output
5840
+ }], bulkComplete: [{
5841
+ type: Output
5842
+ }], error: [{
5843
+ type: Output
4394
5844
  }], uploader: [{
4395
5845
  type: ViewChild,
4396
5846
  args: ['uploader']
5847
+ }], pendingTrigger: [{
5848
+ type: ViewChild,
5849
+ args: ['pendingTrigger']
5850
+ }], pendingPanel: [{
5851
+ type: ViewChild,
5852
+ args: ['pendingPanel']
4397
5853
  }] } });
4398
5854
 
4399
- const FILES_UPLOAD_PT_BR = {
4400
- 'praxis.filesUpload.settingsAriaLabel': 'Abrir configurações',
4401
- 'praxis.filesUpload.dropzoneLabel': 'Arraste arquivos ou',
4402
- 'praxis.filesUpload.dropzoneButton': 'selecionar',
4403
- 'praxis.filesUpload.conflictPolicyLabel': 'Política de conflito',
4404
- 'praxis.filesUpload.metadataLabel': 'Metadados (JSON)',
4405
- 'praxis.filesUpload.progressAriaLabel': 'Progresso do upload',
4406
- 'praxis.filesUpload.rateLimitBanner': 'Limite de requisições excedido. Tente novamente às',
4407
- 'praxis.filesUpload.settingsTitle': 'Configuração de Upload de Arquivos',
4408
- 'praxis.filesUpload.statusSuccess': 'Enviado',
4409
- 'praxis.filesUpload.statusError': 'Erro',
4410
- 'praxis.filesUpload.invalidMetadata': 'Metadados inválidos.',
4411
- 'praxis.filesUpload.genericUploadError': 'Erro no envio de arquivo.',
4412
- 'praxis.filesUpload.acceptError': 'Tipo de arquivo não permitido.',
4413
- 'praxis.filesUpload.maxFileSizeError': 'Arquivo excede o tamanho máximo.',
4414
- 'praxis.filesUpload.maxFilesPerBulkError': 'Quantidade de arquivos excedida.',
4415
- 'praxis.filesUpload.maxBulkSizeError': 'Tamanho total dos arquivos excedido.',
4416
- 'praxis.filesUpload.errors.INVALID_FILE_TYPE': 'Tipo de arquivo inválido.',
4417
- 'praxis.filesUpload.errors.FILE_TOO_LARGE': 'Arquivo muito grande.',
4418
- 'praxis.filesUpload.errors.NOT_FOUND': 'Arquivo não encontrado.',
4419
- 'praxis.filesUpload.errors.UNAUTHORIZED': 'Requisição não autorizada.',
4420
- 'praxis.filesUpload.errors.RATE_LIMIT_EXCEEDED': 'Limite de requisições excedido.',
4421
- 'praxis.filesUpload.errors.INTERNAL_ERROR': 'Erro interno do servidor.',
4422
- 'praxis.filesUpload.errors.QUOTA_EXCEEDED': 'Cota excedida.',
4423
- 'praxis.filesUpload.errors.SEC_VIRUS_DETECTED': 'Vírus detectado no arquivo.',
4424
- 'praxis.filesUpload.errors.SEC_MALICIOUS_CONTENT': 'Conteúdo malicioso detectado.',
4425
- 'praxis.filesUpload.errors.SEC_DANGEROUS_TYPE': 'Tipo de arquivo perigoso.',
4426
- 'praxis.filesUpload.errors.FMT_MAGIC_MISMATCH': 'Conteúdo do arquivo incompatível.',
4427
- 'praxis.filesUpload.errors.FMT_CORRUPTED': 'Arquivo corrompido.',
4428
- 'praxis.filesUpload.errors.FMT_UNSUPPORTED': 'Formato de arquivo não suportado.',
4429
- 'praxis.filesUpload.errors.SYS_STORAGE_ERROR': 'Erro de armazenamento.',
4430
- 'praxis.filesUpload.errors.SYS_SERVICE_DOWN': 'Serviço indisponível.',
4431
- 'praxis.filesUpload.errors.SYS_RATE_LIMIT': 'Limite de requisições excedido.',
4432
- // Aliases PT-BR do backend e códigos adicionais
4433
- 'praxis.filesUpload.errors.ARQUIVO_MUITO_GRANDE': 'Arquivo muito grande.',
4434
- 'praxis.filesUpload.errors.TIPO_ARQUIVO_INVALIDO': 'Tipo de arquivo inválido.',
4435
- 'praxis.filesUpload.errors.TIPO_MIDIA_NAO_SUPORTADO': 'Tipo de mídia não suportado.',
4436
- 'praxis.filesUpload.errors.CAMPO_OBRIGATORIO_AUSENTE': 'Campo obrigatório ausente.',
4437
- 'praxis.filesUpload.errors.OPCOES_JSON_INVALIDAS': 'JSON de opções inválido.',
4438
- 'praxis.filesUpload.errors.ARGUMENTO_INVALIDO': 'Argumento inválido.',
4439
- 'praxis.filesUpload.errors.NAO_AUTORIZADO': 'Autenticação necessária.',
4440
- 'praxis.filesUpload.errors.ERRO_INTERNO': 'Erro interno do servidor.',
4441
- 'praxis.filesUpload.errors.LIMITE_TAXA_EXCEDIDO': 'Limite de taxa excedido.',
4442
- 'praxis.filesUpload.errors.COTA_EXCEDIDA': 'Cota excedida.',
4443
- 'praxis.filesUpload.errors.ARQUIVO_JA_EXISTE': 'Arquivo já existe.',
4444
- // Outros possíveis códigos mapeados no catálogo
4445
- 'praxis.filesUpload.errors.INVALID_JSON_OPTIONS': 'JSON de opções inválido.',
4446
- 'praxis.filesUpload.errors.EMPTY_FILENAME': 'Nome do arquivo obrigatório.',
4447
- 'praxis.filesUpload.errors.FILE_EXISTS': 'Arquivo já existe.',
4448
- 'praxis.filesUpload.errors.PATH_TRAVERSAL': 'Path traversal detectado.',
4449
- 'praxis.filesUpload.errors.INSUFFICIENT_STORAGE': 'Sem espaço em disco.',
4450
- 'praxis.filesUpload.errors.UPLOAD_TIMEOUT': 'Tempo esgotado no upload.',
4451
- 'praxis.filesUpload.errors.BULK_UPLOAD_TIMEOUT': 'Tempo esgotado no upload em lote.',
4452
- 'praxis.filesUpload.errors.BULK_UPLOAD_CANCELLED': 'Upload em lote cancelado.',
4453
- 'praxis.filesUpload.errors.USER_CANCELLED': 'Upload cancelado pelo usuário.',
4454
- 'praxis.filesUpload.errors.UNKNOWN_ERROR': 'Erro desconhecido.',
4455
- };
4456
-
4457
5855
  const PRAXIS_FILES_UPLOAD_COMPONENT_METADATA = {
4458
5856
  id: 'praxis-files-upload',
4459
5857
  selector: 'praxis-files-upload',
@@ -4474,12 +5872,6 @@ const PRAXIS_FILES_UPLOAD_COMPONENT_METADATA = {
4474
5872
  label: 'Upload ID',
4475
5873
  description: 'Identificador logico do upload para persistencia e rastreamento.',
4476
5874
  },
4477
- {
4478
- name: 'uploadId',
4479
- type: 'string',
4480
- label: 'Upload ID (alias)',
4481
- description: 'Alias funcional de filesUploadId para compatibilidade com hosts.',
4482
- },
4483
5875
  {
4484
5876
  name: 'componentInstanceId',
4485
5877
  type: 'string',
@@ -4516,13 +5908,13 @@ const PRAXIS_FILES_UPLOAD_COMPONENT_METADATA = {
4516
5908
  outputs: [
4517
5909
  {
4518
5910
  name: 'uploadSuccess',
4519
- type: '{ file: FileMetadata }',
5911
+ type: 'FileMetadata',
4520
5912
  label: 'Upload concluido',
4521
5913
  description: 'Disparado quando o upload e finalizado com sucesso.',
4522
5914
  },
4523
5915
  {
4524
5916
  name: 'bulkComplete',
4525
- type: 'BulkUploadResponse',
5917
+ type: 'BulkUploadResponseData',
4526
5918
  label: 'Upload em lote concluido',
4527
5919
  description: 'Disparado quando o upload em lote e finalizado.',
4528
5920
  },
@@ -4550,12 +5942,6 @@ const PRAXIS_FILES_UPLOAD_COMPONENT_METADATA = {
4550
5942
  label: 'Progresso',
4551
5943
  description: 'Disparado conforme o progresso de upload.',
4552
5944
  },
4553
- {
4554
- name: 'uploadError',
4555
- type: 'ErrorResponse',
4556
- label: 'Erro no upload',
4557
- description: 'Disparado quando ocorre erro durante o upload.',
4558
- },
4559
5945
  {
4560
5946
  name: 'retry',
4561
5947
  type: 'BulkUploadFileResult',
@@ -4587,10 +5973,10 @@ const PRAXIS_FILES_UPLOAD_COMPONENT_METADATA = {
4587
5973
  description: 'Disparado quando os detalhes do item sao fechados.',
4588
5974
  },
4589
5975
  {
4590
- name: 'pendingFilesChange',
4591
- type: 'File[]',
5976
+ name: 'pendingStateChange',
5977
+ type: 'PendingFilesState',
4592
5978
  label: 'Arquivos pendentes',
4593
- description: 'Disparado quando a lista de arquivos pendentes muda.',
5979
+ description: 'Disparado quando o estado de selecao pendente muda.',
4594
5980
  },
4595
5981
  {
4596
5982
  name: 'proximityChange',
@@ -4728,7 +6114,7 @@ const FILES_UPLOAD_AI_CAPABILITIES = {
4728
6114
  notes: [
4729
6115
  'limits.maxFileSizeBytes and limits.maxBulkSizeBytes are bytes (e.g., 10485760 for 10MB).',
4730
6116
  'options.allowedExtensions is enforced by backend validation; ui.accept is only a client hint.',
4731
- 'Use limits.defaultConflictPolicy to control name collisions on upload.',
6117
+ 'Use options.defaultConflictPolicy to control name collisions on upload.',
4732
6118
  ],
4733
6119
  capabilities: [
4734
6120
  { path: 'strategy', category: 'strategy', valueKind: 'enum', allowedValues: ENUMS.strategy, description: 'Upload strategy.' },
@@ -4759,11 +6145,10 @@ const FILES_UPLOAD_AI_CAPABILITIES = {
4759
6145
  { path: 'limits.maxFileSizeBytes', category: 'limits', valueKind: 'number', description: 'Max file size (bytes).' },
4760
6146
  { path: 'limits.maxFilesPerBulk', category: 'limits', valueKind: 'number', description: 'Max files per bulk upload.' },
4761
6147
  { path: 'limits.maxBulkSizeBytes', category: 'limits', valueKind: 'number', description: 'Max bulk size (bytes).' },
4762
- { path: 'limits.defaultConflictPolicy', category: 'limits', valueKind: 'enum', allowedValues: ENUMS.conflictPolicy, description: 'Default conflict policy.' },
4763
- { path: 'limits.failFast', category: 'limits', valueKind: 'boolean', description: 'Abort on first error.' },
4764
- { path: 'limits.strictValidation', category: 'limits', valueKind: 'boolean', description: 'Strict client validation.' },
4765
- { path: 'limits.maxUploadSizeMb', category: 'limits', valueKind: 'number', description: 'Max upload size in MB.' },
4766
6148
  { path: 'options', category: 'options', valueKind: 'object', description: 'Backend options sent as JSON.' },
6149
+ { path: 'options.defaultConflictPolicy', category: 'options', valueKind: 'enum', allowedValues: ENUMS.conflictPolicy, description: 'Default conflict policy.' },
6150
+ { path: 'options.strictValidation', category: 'options', valueKind: 'boolean', description: 'Strict backend validation.' },
6151
+ { path: 'options.maxUploadSizeMb', category: 'options', valueKind: 'number', description: 'Max upload size in MB.' },
4767
6152
  { path: 'options.allowedExtensions', category: 'options', valueKind: 'array', description: 'Allowed file extensions.' },
4768
6153
  { path: 'options.allowedExtensions[]', category: 'options', valueKind: 'string', description: 'Single extension (e.g., pdf).' },
4769
6154
  { path: 'options.acceptMimeTypes', category: 'options', valueKind: 'array', description: 'Allowed mime types.' },
@@ -4771,6 +6156,7 @@ const FILES_UPLOAD_AI_CAPABILITIES = {
4771
6156
  { path: 'options.targetDirectory', category: 'options', valueKind: 'string', description: 'Target directory on backend.' },
4772
6157
  { path: 'options.enableVirusScanning', category: 'options', valueKind: 'boolean', description: 'Enable virus scanning.' },
4773
6158
  { path: 'bulk', category: 'bulk', valueKind: 'object', description: 'Bulk upload tuning.' },
6159
+ { path: 'bulk.failFast', category: 'bulk', valueKind: 'boolean', description: 'Abort on first error.' },
4774
6160
  { path: 'bulk.parallelUploads', category: 'bulk', valueKind: 'number', description: 'Parallel uploads count.' },
4775
6161
  { path: 'bulk.retryCount', category: 'bulk', valueKind: 'number', description: 'Retry count.' },
4776
6162
  { path: 'bulk.retryBackoffMs', category: 'bulk', valueKind: 'number', description: 'Retry backoff (ms).' },
@@ -4799,5 +6185,5 @@ const FILES_UPLOAD_AI_CAPABILITIES = {
4799
6185
  * Generated bundle index. Do not edit.
4800
6186
  */
4801
6187
 
4802
- export { BulkUploadResultStatus, ErrorCode, ErrorMapperService, FILES_UPLOAD_AI_CAPABILITIES, FILES_UPLOAD_ERROR_MESSAGES, FILES_UPLOAD_PT_BR, FILES_UPLOAD_TEXTS, FilesApiClient, PRAXIS_FILES_UPLOAD_COMPONENT_METADATA, PdxFilesUploadFieldComponent, PraxisFilesUpload, PraxisFilesUploadConfigEditor, PresignedUploaderService, ScanStatus, TRANSLATE_LIKE, acceptValidator, maxBulkSizeValidator, maxFileSizeValidator, maxFilesPerBulkValidator, providePraxisFilesUploadMetadata };
6188
+ export { BulkUploadResultStatus, ErrorCode, ErrorMapperService, FILES_UPLOAD_AI_CAPABILITIES, FILES_UPLOAD_EN_US, FILES_UPLOAD_ERROR_MESSAGES, FILES_UPLOAD_PT_BR, FILES_UPLOAD_TEXTS, FilesApiClient, PRAXIS_FILES_UPLOAD_COMPONENT_METADATA, PdxFilesUploadFieldComponent, PraxisFilesUpload, PraxisFilesUploadConfigEditor, PresignedUploaderService, ScanStatus, TRANSLATE_LIKE, acceptValidator, createPraxisFilesUploadI18nConfig, getPrimaryErrorItem, isErrorEnvelope, maxBulkSizeValidator, maxFileSizeValidator, maxFilesPerBulkValidator, providePraxisFilesUploadI18n, providePraxisFilesUploadMetadata, resolvePraxisFilesUploadText };
4803
6189
  //# sourceMappingURL=praxisui-files-upload.mjs.map