@praxisui/dynamic-fields 9.0.0-beta.16 → 9.0.0-beta.18

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.
@@ -130,6 +130,7 @@ const PRAXIS_DYNAMIC_FIELDS_EN_US = {
130
130
  'praxis.dynamicFields.validation.unique': 'Value is already in use',
131
131
  'praxis.dynamicFields.validation.fileType': 'File type is not allowed',
132
132
  'praxis.dynamicFields.validation.maxFileSize': 'File exceeds the maximum size',
133
+ 'praxis.dynamicFields.validation.maxFiles': 'Select at most {{count}} file(s)',
133
134
  'praxis.dynamicFields.validation.minWords': 'Text has too few words',
134
135
  'praxis.dynamicFields.validation.conditional': 'Validation rule not satisfied',
135
136
  'praxis.dynamicFields.validation.invalidValue': 'Invalid value',
@@ -142,6 +143,16 @@ const PRAXIS_DYNAMIC_FIELDS_EN_US = {
142
143
  'praxis.dynamicFields.actions.clear.tooltip': 'Clear',
143
144
  'praxis.dynamicFields.actions.clear.ariaLabel': 'Clear field',
144
145
  'praxis.dynamicFields.actions.clear.ariaLabelWithLabel': 'Clear {{label}}',
146
+ 'praxis.dynamicFields.fileUpload.emptySummary': 'No file selected',
147
+ 'praxis.dynamicFields.fileUpload.multipleSummary': '{{count}} files selected',
148
+ 'praxis.dynamicFields.fileUpload.clearSelection': 'Clear',
149
+ 'praxis.dynamicFields.fileUpload.clearSelectionAriaLabel': 'Clear selected file',
150
+ 'praxis.dynamicFields.fileUpload.selectedSummaryAriaLabel': 'Selected file',
151
+ 'praxis.dynamicFields.fileUpload.previewAlt': 'Selected image preview',
152
+ 'praxis.dynamicFields.fileUpload.sizeUnitBytes': 'B',
153
+ 'praxis.dynamicFields.fileUpload.sizeUnitKb': 'KB',
154
+ 'praxis.dynamicFields.fileUpload.sizeUnitMb': 'MB',
155
+ 'praxis.dynamicFields.fileUpload.sizeUnitGb': 'GB',
145
156
  'praxis.dynamicFields.array.add': 'Add item',
146
157
  'praxis.dynamicFields.array.remove': 'Remove',
147
158
  'praxis.dynamicFields.array.empty': 'No items registered.',
@@ -509,6 +520,7 @@ const PRAXIS_DYNAMIC_FIELDS_PT_BR = {
509
520
  'praxis.dynamicFields.validation.unique': 'Valor já utilizado',
510
521
  'praxis.dynamicFields.validation.fileType': 'Tipo de arquivo não permitido',
511
522
  'praxis.dynamicFields.validation.maxFileSize': 'Arquivo excede o tamanho máximo',
523
+ 'praxis.dynamicFields.validation.maxFiles': 'Selecione no máximo {{count}} arquivo(s)',
512
524
  'praxis.dynamicFields.validation.minWords': 'Texto com poucas palavras',
513
525
  'praxis.dynamicFields.validation.conditional': 'Regra de validação não atendida',
514
526
  'praxis.dynamicFields.validation.invalidValue': 'Valor inválido',
@@ -521,6 +533,16 @@ const PRAXIS_DYNAMIC_FIELDS_PT_BR = {
521
533
  'praxis.dynamicFields.actions.clear.tooltip': 'Limpar',
522
534
  'praxis.dynamicFields.actions.clear.ariaLabel': 'Limpar campo',
523
535
  'praxis.dynamicFields.actions.clear.ariaLabelWithLabel': 'Limpar {{label}}',
536
+ 'praxis.dynamicFields.fileUpload.emptySummary': 'Nenhum arquivo selecionado',
537
+ 'praxis.dynamicFields.fileUpload.multipleSummary': '{{count}} arquivos selecionados',
538
+ 'praxis.dynamicFields.fileUpload.clearSelection': 'Limpar',
539
+ 'praxis.dynamicFields.fileUpload.clearSelectionAriaLabel': 'Limpar arquivo selecionado',
540
+ 'praxis.dynamicFields.fileUpload.selectedSummaryAriaLabel': 'Arquivo selecionado',
541
+ 'praxis.dynamicFields.fileUpload.previewAlt': 'Pré-visualização da imagem selecionada',
542
+ 'praxis.dynamicFields.fileUpload.sizeUnitBytes': 'B',
543
+ 'praxis.dynamicFields.fileUpload.sizeUnitKb': 'KB',
544
+ 'praxis.dynamicFields.fileUpload.sizeUnitMb': 'MB',
545
+ 'praxis.dynamicFields.fileUpload.sizeUnitGb': 'GB',
524
546
  'praxis.dynamicFields.array.add': 'Adicionar item',
525
547
  'praxis.dynamicFields.array.remove': 'Remover',
526
548
  'praxis.dynamicFields.array.empty': 'Nenhum item cadastrado.',
@@ -775,7 +797,7 @@ const PRAXIS_DYNAMIC_FIELDS_PT_BR = {
775
797
  'praxis.dynamicFields.entityLookup.action.create': 'Novo',
776
798
  'praxis.dynamicFields.entityLookup.action.clear': 'Limpar',
777
799
  'praxis.dynamicFields.entityLookup.sort.label': 'Ordenar por',
778
- 'praxis.dynamicFields.entityLookup.sort.default': 'Padrao',
800
+ 'praxis.dynamicFields.entityLookup.sort.default': 'Padrão',
779
801
  'praxis.dynamicFields.entityLookup.filter.clearAll': 'Limpar filtros',
780
802
  'praxis.dynamicFields.entityLookup.dialog.open': 'Busca avançada',
781
803
  'praxis.dynamicFields.entityLookup.dialog.openSupporting': 'Refine a busca com filtros, ordenacao e contexto.',
@@ -812,7 +834,7 @@ const PRAXIS_DYNAMIC_FIELDS_PT_BR = {
812
834
  'praxis.dynamicFields.entityLookup.dialog.preview.legacy': 'Valor legado',
813
835
  'praxis.dynamicFields.entityLookup.dialog.preview.blocked': 'Bloqueado',
814
836
  'praxis.dynamicFields.entityLookup.dialog.preview.selectable': 'Selecionavel',
815
- 'praxis.dynamicFields.entityLookup.selectionLimitReached': 'Limite de {max} selecoes atingido.',
837
+ 'praxis.dynamicFields.entityLookup.selectionLimitReached': 'Limite de {max} seleções atingido.',
816
838
  'praxis.dynamicFields.entityLookup.action.removeSelection': 'Remover {label}',
817
839
  'praxis.dynamicFields.entityLookup.noResults': 'Nenhuma entidade encontrada para os filtros atuais.',
818
840
  'praxis.dynamicFields.entityLookup.unmetDependencies': 'Preencha os campos dependentes antes de buscar.',
@@ -864,7 +886,7 @@ const PRAXIS_DYNAMIC_FIELDS_PT_BR = {
864
886
  'praxis.dynamicFields.editorial.avatar.inputs.emphasis.description': 'Estado de destaque visual usado para avatares selecionados, principais ou de atenção sem alterar valores submetidos.',
865
887
  'praxis.dynamicFields.editorial.avatar.inputs.emphasisColor.description': 'Token de cor usado pelo anel ou halo de destaque.',
866
888
  'praxis.dynamicFields.editorial.avatar.inputs.emphasisLabel.description': 'Rotulo acessivel que descreve o significado do destaque visual.',
867
- 'praxis.dynamicFields.editorial.avatar.inputs.initialsMaxLength.description': 'Numero maximo de iniciais renderizadas',
889
+ 'praxis.dynamicFields.editorial.avatar.inputs.initialsMaxLength.description': 'Número máximo de iniciais renderizadas',
868
890
  'praxis.dynamicFields.editorial.avatar.inputs.tooltip.description': 'Texto do tooltip',
869
891
  'praxis.dynamicFields.editorial.avatar.inputs.ariaLabel.description': 'Rótulo ARIA',
870
892
  'praxis.dynamicFields.editorial.avatar.inputs.class.description': 'Classes CSS adicionais no host',
@@ -2980,8 +3002,8 @@ class SimpleBaseButtonComponent {
2980
3002
  // Adicionar confirmação se configurada
2981
3003
  if (action && meta.confirmDialog) {
2982
3004
  action.confirmation = {
2983
- title: meta.confirmDialog.title || this.tDynamicFields('button.confirmTitle', 'Confirmar acao'),
2984
- message: meta.confirmDialog.message || this.tDynamicFields('button.confirmMessage', 'Deseja continuar com esta acao?'),
3005
+ title: meta.confirmDialog.title || this.tDynamicFields('button.confirmTitle', 'Confirmar ação'),
3006
+ message: meta.confirmDialog.message || this.tDynamicFields('button.confirmMessage', 'Deseja continuar com esta ação?'),
2985
3007
  confirmLabel: meta.confirmDialog.confirmLabel || this.tDynamicFields('dialog.confirm', 'Confirmar'),
2986
3008
  cancelLabel: meta.confirmDialog.cancelLabel || this.tDynamicFields('dialog.cancel', 'Cancelar'),
2987
3009
  };
@@ -8809,7 +8831,7 @@ const LoggerPresets = {
8809
8831
  }
8810
8832
  };
8811
8833
 
8812
- const EMPTY_DISPLAY = '-- Nao informado';
8834
+ const EMPTY_DISPLAY = '-- Não informado';
8813
8835
  function isISODate(v) {
8814
8836
  return /^\d{4}-\d{2}-\d{2}$/.test(v);
8815
8837
  }
@@ -9064,7 +9086,7 @@ function resolveFieldPresentation(field, value, options) {
9064
9086
  const config = buildValuePresentationConfig(field, value);
9065
9087
  if (!config)
9066
9088
  return null;
9067
- const effectiveLocalization = mergeLocalization(options?.localization, buildLegacyFieldLocalization(field));
9089
+ const effectiveLocalization = mergeLocalization(mergeLocalization(buildLegacyFieldLocalization(field), field?.localization), options?.localization);
9068
9090
  const fallbackCurrencyCode = config.type === 'currency' && !config.currency?.code
9069
9091
  ? inferCurrencyCodeFromLocale(effectiveLocalization?.locale ||
9070
9092
  options?.surfaceLocale ||
@@ -9072,7 +9094,7 @@ function resolveFieldPresentation(field, value, options) {
9072
9094
  undefined)
9073
9095
  : undefined;
9074
9096
  return resolveValuePresentation(config, {
9075
- valueLocale: resolveFieldLocale(field),
9097
+ valueLocale: effectiveLocalization?.locale,
9076
9098
  localization: fallbackCurrencyCode
9077
9099
  ? mergeLocalization(effectiveLocalization, {
9078
9100
  ...(effectiveLocalization?.locale ? { locale: effectiveLocalization.locale } : {}),
@@ -9356,7 +9378,7 @@ function formatBooleanPresentation(value, locale) {
9356
9378
  function resolveBooleanLabels(locale) {
9357
9379
  const normalized = String(locale || '').trim().toLowerCase();
9358
9380
  if (normalized.startsWith('pt')) {
9359
- return ['Sim', 'Nao'];
9381
+ return ['Sim', 'Não'];
9360
9382
  }
9361
9383
  if (normalized.startsWith('es')) {
9362
9384
  return ['Si', 'No'];
@@ -9729,7 +9751,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
9729
9751
  * project additional chrome around the field.
9730
9752
  */
9731
9753
  class FieldShellComponent {
9732
- hostLocale = inject(LOCALE_ID, { optional: true }) ?? 'en-US';
9754
+ angularLocale = inject(LOCALE_ID, { optional: true }) ?? 'en-US';
9755
+ i18n = inject(PraxisI18nService, { optional: true });
9733
9756
  cdr = inject(ChangeDetectorRef);
9734
9757
  optionDisplayResolver = inject(OptionDisplayResolverService);
9735
9758
  crudService = inject(GenericCrudService, {
@@ -9898,7 +9921,7 @@ class FieldShellComponent {
9898
9921
  const formatted = typeof fn === 'function'
9899
9922
  ? fn(raw)
9900
9923
  : formatDisplayValue(this.field, raw, {
9901
- appLocale: this.hostLocale,
9924
+ appLocale: this.resolvePresentationAppLocale(),
9902
9925
  surfaceLocale: this.field?.localization?.locale ?? this.field?.locale ?? undefined,
9903
9926
  localization: this.field?.localization ?? undefined,
9904
9927
  });
@@ -9906,7 +9929,7 @@ class FieldShellComponent {
9906
9929
  }
9907
9930
  catch {
9908
9931
  return formatDisplayValue(this.field, raw, {
9909
- appLocale: this.hostLocale,
9932
+ appLocale: this.resolvePresentationAppLocale(),
9910
9933
  surfaceLocale: this.field?.localization?.locale ?? this.field?.locale ?? undefined,
9911
9934
  localization: this.field?.localization ?? undefined,
9912
9935
  });
@@ -9924,14 +9947,14 @@ class FieldShellComponent {
9924
9947
  : null;
9925
9948
  const val = formatted == null ? raw : formatted;
9926
9949
  return formatDisplayHtml(this.field, val, {
9927
- appLocale: this.hostLocale,
9950
+ appLocale: this.resolvePresentationAppLocale(),
9928
9951
  surfaceLocale: this.field?.localization?.locale ?? this.field?.locale ?? undefined,
9929
9952
  localization: this.field?.localization ?? undefined,
9930
9953
  });
9931
9954
  }
9932
9955
  catch {
9933
9956
  return formatDisplayHtml(this.field, raw, {
9934
- appLocale: this.hostLocale,
9957
+ appLocale: this.resolvePresentationAppLocale(),
9935
9958
  surfaceLocale: this.field?.localization?.locale ?? this.field?.locale ?? undefined,
9936
9959
  localization: this.field?.localization ?? undefined,
9937
9960
  });
@@ -10029,6 +10052,14 @@ class FieldShellComponent {
10029
10052
  const value = String(position ?? '').trim().toLowerCase();
10030
10053
  return ['end', 'right', 'after', 'suffix', 'trailing'].includes(value);
10031
10054
  }
10055
+ resolvePresentationAppLocale() {
10056
+ const praxisLocale = this.i18n?.getLocale()?.trim();
10057
+ if (praxisLocale) {
10058
+ return praxisLocale;
10059
+ }
10060
+ const angularLocale = this.angularLocale?.trim();
10061
+ return angularLocale || 'en-US';
10062
+ }
10032
10063
  refreshPresentationDisplayValue() {
10033
10064
  this.resolveSub?.unsubscribe();
10034
10065
  this.resolveSub = undefined;
@@ -13108,7 +13139,7 @@ class InlineEntityLookupComponent extends MaterialAsyncSelectComponent {
13108
13139
  }
13109
13140
  selectionLimitReachedText(maxSelections) {
13110
13141
  const max = Number(maxSelections ?? 0);
13111
- const template = this.tDynamicFields('entityLookup.selectionLimitReached', 'Limite de {max} selecoes atingido.', { max: Number.isFinite(max) && max > 0 ? max : 0 });
13142
+ const template = this.tDynamicFields('entityLookup.selectionLimitReached', 'Limite de {max} seleções atingido.', { max: Number.isFinite(max) && max > 0 ? max : 0 });
13112
13143
  return template.replace('{max}', String(Number.isFinite(max) && max > 0 ? max : 0));
13113
13144
  }
13114
13145
  onDetailAction(event, item) {
@@ -13278,7 +13309,7 @@ class InlineEntityLookupComponent extends MaterialAsyncSelectComponent {
13278
13309
  }
13279
13310
  resetOptionText() {
13280
13311
  const custom = String(this.metadata()?.resetLabel || '').trim();
13281
- return custom || 'Limpar selecao';
13312
+ return custom || 'Limpar seleção';
13282
13313
  }
13283
13314
  showQuickClear() {
13284
13315
  const clearCfg = this.currentMetadata().clearButton;
@@ -13382,7 +13413,7 @@ class InlineEntityLookupComponent extends MaterialAsyncSelectComponent {
13382
13413
  this.retry();
13383
13414
  }
13384
13415
  errorStateLabel() {
13385
- return this.error() || this.tDynamicFields('select.loadOptionsError', 'Erro ao carregar opcoes.');
13416
+ return this.error() || this.tDynamicFields('select.loadOptionsError', 'Erro ao carregar opções.');
13386
13417
  }
13387
13418
  dependencyStateLabel() {
13388
13419
  const dependencyLabels = this.missingDependencyLabels();
@@ -13453,7 +13484,7 @@ class InlineEntityLookupComponent extends MaterialAsyncSelectComponent {
13453
13484
  return this.tDynamicFields('entityLookup.sort.label', 'Ordenar por');
13454
13485
  }
13455
13486
  sortDefaultOptionText() {
13456
- return this.tDynamicFields('entityLookup.sort.default', 'Padrao');
13487
+ return this.tDynamicFields('entityLookup.sort.default', 'Padrão');
13457
13488
  }
13458
13489
  clearAllFiltersText() {
13459
13490
  return this.tDynamicFields('entityLookup.filter.clearAll', 'Limpar filtros');
@@ -17626,11 +17657,11 @@ function createInlinePhoneComponentMetadata(locale = 'en-US') {
17626
17657
  }
17627
17658
 
17628
17659
  /**
17629
- * Placeholder component for file upload fields.
17660
+ * Baseline file upload field.
17630
17661
  *
17631
- * Currently provides a simple `<input type="file">` element and basic
17632
- * ControlValueAccessor integration. A full-featured implementation will
17633
- * be provided in future iterations.
17662
+ * This component owns local file selection, validation and preview. Persisting
17663
+ * the selected file remains a host/backend concern; use @praxisui/files-upload
17664
+ * when the flow needs presign, progress, conflict policy or storage events.
17634
17665
  */
17635
17666
  class MaterialFileUploadComponent extends SimpleBaseInputComponent {
17636
17667
  // Praxis Field States
@@ -17638,13 +17669,55 @@ class MaterialFileUploadComponent extends SimpleBaseInputComponent {
17638
17669
  disabledMode = false;
17639
17670
  visible = true;
17640
17671
  presentationMode = false;
17672
+ selectedFiles = signal([], ...(ngDevMode ? [{ debugName: "selectedFiles" }] : /* istanbul ignore next */ []));
17673
+ previewUrl = signal(null, ...(ngDevMode ? [{ debugName: "previewUrl" }] : /* istanbul ignore next */ []));
17674
+ externalValueSyncSubscription = new Subscription();
17675
+ selectedSummaryText = computed(() => {
17676
+ const files = this.selectedFiles();
17677
+ if (!files.length) {
17678
+ return this.dynamicFieldsText('fileUpload.emptySummary', 'No file selected');
17679
+ }
17680
+ if (files.length === 1) {
17681
+ return files[0].name;
17682
+ }
17683
+ return this.dynamicFieldsText('fileUpload.multipleSummary', '{{count}} files selected', {
17684
+ count: files.length,
17685
+ });
17686
+ }, ...(ngDevMode ? [{ debugName: "selectedSummaryText" }] : /* istanbul ignore next */ []));
17687
+ selectedDetailText = computed(() => {
17688
+ const files = this.selectedFiles();
17689
+ if (!files.length)
17690
+ return '';
17691
+ const total = files.reduce((sum, file) => sum + file.size, 0);
17692
+ return this.formatBytes(total);
17693
+ }, ...(ngDevMode ? [{ debugName: "selectedDetailText" }] : /* istanbul ignore next */ []));
17641
17694
  /**
17642
17695
  * Updates the control value when the user selects a file.
17643
17696
  */
17644
17697
  onFileSelected(event) {
17645
17698
  const input = event.target;
17646
- const file = input.files?.[0] ?? null;
17647
- this.setValue(file);
17699
+ const files = Array.from(input.files ?? []);
17700
+ if (!files.length) {
17701
+ this.clearSelectedFiles({ resetInput: false });
17702
+ return;
17703
+ }
17704
+ const selected = this.allowsMultiple() ? files : files.slice(0, 1);
17705
+ const validationError = this.validateSelectedFiles(selected);
17706
+ if (validationError) {
17707
+ this.clearPreview();
17708
+ this.selectedFiles.set([]);
17709
+ this.setValue(this.allowsMultiple() ? [] : null, { emitEvent: true });
17710
+ this.applyFileError(validationError);
17711
+ input.value = '';
17712
+ return;
17713
+ }
17714
+ this.clearFileErrors();
17715
+ this.selectedFiles.set(selected);
17716
+ this.updatePreview(selected);
17717
+ this.setValue(this.allowsMultiple() ? selected : selected[0], { emitEvent: true });
17718
+ const control = this.control();
17719
+ control.markAsDirty();
17720
+ control.markAsTouched();
17648
17721
  }
17649
17722
  /**
17650
17723
  * Exposes metadata setter for integration with dynamic form loader.
@@ -17660,8 +17733,71 @@ class MaterialFileUploadComponent extends SimpleBaseInputComponent {
17660
17733
  allowsMultiple() {
17661
17734
  return this.metadataRecord()?.['multiple'] === true;
17662
17735
  }
17736
+ imagePreviewEnabled() {
17737
+ const metadata = this.metadataRecord();
17738
+ if (metadata?.['imagePreview'] === true)
17739
+ return true;
17740
+ if (metadata?.['preview'] === 'image')
17741
+ return true;
17742
+ const accept = this.fileAccept()?.toLowerCase() ?? '';
17743
+ return accept.includes('image/');
17744
+ }
17663
17745
  isInteractionDisabled() {
17664
- return this.disabledMode || this.readonlyMode || this.presentationMode;
17746
+ return this.isInteractionBlockedByState();
17747
+ }
17748
+ clearSelectedFiles(options = {}) {
17749
+ this.clearPreview();
17750
+ this.selectedFiles.set([]);
17751
+ this.clearFileErrors();
17752
+ this.setValue(this.allowsMultiple() ? [] : null, { emitEvent: true });
17753
+ const control = this.control();
17754
+ control.markAsDirty();
17755
+ control.markAsTouched();
17756
+ if (options.resetInput !== false) {
17757
+ const input = this.elementRef.nativeElement.querySelector('input[type="file"]');
17758
+ if (input)
17759
+ input.value = '';
17760
+ }
17761
+ }
17762
+ clearLabel() {
17763
+ return this.dynamicFieldsText('fileUpload.clearSelection', 'Clear');
17764
+ }
17765
+ clearAriaLabel() {
17766
+ return this.dynamicFieldsText('fileUpload.clearSelectionAriaLabel', 'Clear selected file');
17767
+ }
17768
+ selectedSummaryAriaLabel() {
17769
+ return this.dynamicFieldsText('fileUpload.selectedSummaryAriaLabel', 'Selected file');
17770
+ }
17771
+ fileInputDescribedBy() {
17772
+ const ids = [
17773
+ this.metadata()?.ariaDescribedby,
17774
+ this.hasValidationError() ? this.errorId() : null,
17775
+ this.metadata()?.hint && !this.hasValidationError() ? this.hintId() : null,
17776
+ ]
17777
+ .map((id) => (typeof id === 'string' ? id.trim() : ''))
17778
+ .filter(Boolean);
17779
+ return ids.length ? ids.join(' ') : null;
17780
+ }
17781
+ hintId() {
17782
+ return `${this.componentId()}-hint`;
17783
+ }
17784
+ errorId() {
17785
+ return `${this.componentId()}-error`;
17786
+ }
17787
+ previewAltText() {
17788
+ const label = this.metadataRecord()?.['previewAlt'];
17789
+ if (typeof label === 'string' && label.trim())
17790
+ return label.trim();
17791
+ return this.dynamicFieldsText('fileUpload.previewAlt', 'Selected image preview');
17792
+ }
17793
+ writeValue(value) {
17794
+ super.writeValue(value);
17795
+ this.syncSelectedFilesFromValue(value);
17796
+ }
17797
+ ngOnDestroy() {
17798
+ this.externalValueSyncSubscription.unsubscribe();
17799
+ this.clearPreview();
17800
+ super.ngOnDestroy();
17665
17801
  }
17666
17802
  /**
17667
17803
  * CSS classes specific to the file upload wrapper.
@@ -17669,12 +17805,168 @@ class MaterialFileUploadComponent extends SimpleBaseInputComponent {
17669
17805
  getSpecificCssClasses() {
17670
17806
  return ['pdx-material-file-upload'];
17671
17807
  }
17808
+ onActiveControlChanged(previousControl, currentControl) {
17809
+ super.onActiveControlChanged(previousControl, currentControl);
17810
+ this.externalValueSyncSubscription.unsubscribe();
17811
+ this.externalValueSyncSubscription = currentControl.valueChanges.subscribe((value) => {
17812
+ this.syncSelectedFilesFromValue(value);
17813
+ });
17814
+ this.syncSelectedFilesFromValue(currentControl.value);
17815
+ }
17672
17816
  metadataRecord() {
17673
17817
  const metadata = this.metadata();
17674
17818
  return metadata && typeof metadata === 'object'
17675
17819
  ? metadata
17676
17820
  : null;
17677
17821
  }
17822
+ updatePreview(files) {
17823
+ this.clearPreview();
17824
+ if (!this.imagePreviewEnabled())
17825
+ return;
17826
+ const image = files.find((file) => this.isImageFile(file));
17827
+ if (!image || typeof URL === 'undefined' || typeof URL.createObjectURL !== 'function') {
17828
+ return;
17829
+ }
17830
+ this.previewUrl.set(URL.createObjectURL(image));
17831
+ }
17832
+ clearPreview() {
17833
+ const current = this.previewUrl();
17834
+ if (current && typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function') {
17835
+ URL.revokeObjectURL(current);
17836
+ }
17837
+ this.previewUrl.set(null);
17838
+ }
17839
+ validateSelectedFiles(files) {
17840
+ const maxFileSize = this.maxFileSize();
17841
+ const maxFiles = this.maxFiles();
17842
+ if (maxFiles !== null && files.length > maxFiles) {
17843
+ return {
17844
+ key: 'maxFiles',
17845
+ message: this.dynamicFieldsText('validation.maxFiles', 'Too many files selected', {
17846
+ count: maxFiles,
17847
+ }),
17848
+ };
17849
+ }
17850
+ for (const file of files) {
17851
+ if (!this.isAcceptedFile(file)) {
17852
+ return {
17853
+ key: 'fileType',
17854
+ message: this.dynamicFieldsText('validation.fileType', 'File type is not allowed'),
17855
+ };
17856
+ }
17857
+ if (maxFileSize !== null && file.size > maxFileSize) {
17858
+ return {
17859
+ key: 'maxFileSize',
17860
+ message: this.dynamicFieldsText('validation.maxFileSize', 'File exceeds the maximum size'),
17861
+ };
17862
+ }
17863
+ }
17864
+ return null;
17865
+ }
17866
+ isAcceptedFile(file) {
17867
+ const accept = this.fileAccept();
17868
+ if (!accept)
17869
+ return true;
17870
+ const fileName = file.name.toLowerCase();
17871
+ const fileType = file.type.toLowerCase();
17872
+ return accept
17873
+ .split(',')
17874
+ .map((token) => token.trim().toLowerCase())
17875
+ .filter(Boolean)
17876
+ .some((token) => {
17877
+ if (token === '*/*')
17878
+ return true;
17879
+ if (token === 'image/*')
17880
+ return fileType.startsWith('image/') || this.hasKnownImageExtension(fileName);
17881
+ if (token.endsWith('/*'))
17882
+ return fileType.startsWith(token.slice(0, -1));
17883
+ if (token.startsWith('.'))
17884
+ return fileName.endsWith(token);
17885
+ return fileType === token;
17886
+ });
17887
+ }
17888
+ maxFileSize() {
17889
+ const raw = this.metadataRecord()?.['maxFileSize'];
17890
+ const value = typeof raw === 'number' ? raw : Number(raw);
17891
+ return Number.isFinite(value) && value > 0 ? value : null;
17892
+ }
17893
+ maxFiles() {
17894
+ const raw = this.metadataRecord()?.['maxFiles'];
17895
+ const value = typeof raw === 'number' ? raw : Number(raw);
17896
+ return Number.isInteger(value) && value > 0 ? value : null;
17897
+ }
17898
+ applyFileError(error) {
17899
+ const control = this.control();
17900
+ const current = control.errors ?? {};
17901
+ control.setErrors({
17902
+ ...current,
17903
+ [error.key]: { message: error.message },
17904
+ });
17905
+ control.markAsDirty();
17906
+ control.markAsTouched();
17907
+ }
17908
+ clearFileErrors() {
17909
+ const control = this.control();
17910
+ const current = control.errors;
17911
+ if (!current)
17912
+ return;
17913
+ const { fileType: _fileType, maxFileSize: _maxFileSize, maxFiles: _maxFiles, ...rest } = current;
17914
+ control.setErrors(Object.keys(rest).length ? rest : null);
17915
+ }
17916
+ syncSelectedFilesFromValue(value) {
17917
+ const files = this.extractFilesFromValue(value);
17918
+ this.selectedFiles.set(files);
17919
+ this.updatePreview(files);
17920
+ }
17921
+ extractFilesFromValue(value) {
17922
+ if (!value)
17923
+ return [];
17924
+ if (this.isFile(value))
17925
+ return [value];
17926
+ if (Array.isArray(value))
17927
+ return value.filter((item) => this.isFile(item));
17928
+ return [];
17929
+ }
17930
+ isFile(value) {
17931
+ return typeof File !== 'undefined' && value instanceof File;
17932
+ }
17933
+ isImageFile(file) {
17934
+ return file.type.toLowerCase().startsWith('image/') || this.hasKnownImageExtension(file.name.toLowerCase());
17935
+ }
17936
+ hasKnownImageExtension(fileName) {
17937
+ return [
17938
+ '.apng',
17939
+ '.avif',
17940
+ '.bmp',
17941
+ '.gif',
17942
+ '.jpeg',
17943
+ '.jpg',
17944
+ '.png',
17945
+ '.svg',
17946
+ '.webp',
17947
+ ].some((extension) => fileName.endsWith(extension));
17948
+ }
17949
+ formatBytes(bytes) {
17950
+ const units = [
17951
+ this.dynamicFieldsText('fileUpload.sizeUnitBytes', 'B'),
17952
+ this.dynamicFieldsText('fileUpload.sizeUnitKb', 'KB'),
17953
+ this.dynamicFieldsText('fileUpload.sizeUnitMb', 'MB'),
17954
+ this.dynamicFieldsText('fileUpload.sizeUnitGb', 'GB'),
17955
+ ];
17956
+ if (!Number.isFinite(bytes) || bytes <= 0)
17957
+ return `0 ${units[0]}`;
17958
+ let value = bytes;
17959
+ let index = 0;
17960
+ while (value >= 1024 && index < units.length - 1) {
17961
+ value /= 1024;
17962
+ index += 1;
17963
+ }
17964
+ const rounded = value >= 10 || index === 0 ? Math.round(value) : Math.round(value * 10) / 10;
17965
+ return `${rounded} ${units[index]}`;
17966
+ }
17967
+ dynamicFieldsText(keySuffix, fallback, params) {
17968
+ return this.i18n.t(`praxis.dynamicFields.${keySuffix}`, params, fallback, 'dynamicFields');
17969
+ }
17678
17970
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: MaterialFileUploadComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
17679
17971
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: MaterialFileUploadComponent, isStandalone: true, selector: "pdx-material-file-upload", inputs: { readonlyMode: "readonlyMode", disabledMode: "disabledMode", visible: "visible", presentationMode: "presentationMode" }, host: { properties: { "class": "componentCssClasses()", "class.praxis-disabled": "disabledMode", "style.display": "visible ? null : \"none\"", "attr.aria-hidden": "visible ? null : \"true\"", "attr.data-field-type": "\"file-upload\"", "attr.data-field-name": "metadata()?.name", "attr.data-component-id": "componentId()" } }, providers: [
17680
17972
  {
@@ -17697,24 +17989,51 @@ class MaterialFileUploadComponent extends SimpleBaseInputComponent {
17697
17989
  [attr.accept]="fileAccept()"
17698
17990
  [attr.multiple]="allowsMultiple() ? '' : null"
17699
17991
  [disabled]="isInteractionDisabled()"
17700
- [attr.aria-disabled]="disabledMode ? 'true' : null"
17992
+ [attr.aria-disabled]="isInteractionDisabled() ? 'true' : null"
17993
+ [attr.aria-describedby]="fileInputDescribedBy()"
17994
+ [attr.aria-invalid]="hasValidationError() ? 'true' : null"
17995
+ [attr.aria-label]="inputAriaLabel()"
17701
17996
  />
17997
+ @if (selectedFiles().length) {
17998
+ <div
17999
+ class="pdx-file-upload__selection"
18000
+ aria-live="polite"
18001
+ [attr.aria-label]="selectedSummaryAriaLabel()"
18002
+ >
18003
+ @if (previewUrl()) {
18004
+ <img
18005
+ class="pdx-file-upload__preview"
18006
+ [src]="previewUrl()!"
18007
+ [attr.alt]="previewAltText()"
18008
+ />
18009
+ }
18010
+ <div class="pdx-file-upload__summary">
18011
+ <span class="pdx-file-upload__summary-title">{{ selectedSummaryText() }}</span>
18012
+ <span class="pdx-file-upload__summary-detail">{{ selectedDetailText() }}</span>
18013
+ </div>
18014
+ <button
18015
+ class="pdx-file-upload__clear"
18016
+ type="button"
18017
+ [disabled]="isInteractionDisabled()"
18018
+ [attr.aria-label]="clearAriaLabel()"
18019
+ (click)="clearSelectedFiles()"
18020
+ >
18021
+ {{ clearLabel() }}
18022
+ </button>
18023
+ </div>
18024
+ }
17702
18025
  @if (hasValidationError()) {
17703
- <div class="error">{{ errorMessage() }}</div>
18026
+ <div class="error" [attr.id]="errorId()">{{ errorMessage() }}</div>
17704
18027
  }
17705
18028
  @if (metadata()?.hint && !hasValidationError()) {
17706
- <div class="hint">{{ metadata()!.hint }}</div>
18029
+ <div class="hint" [attr.id]="hintId()">{{ metadata()!.hint }}</div>
17707
18030
  }
17708
18031
  </div>
17709
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
18032
+ `, isInline: true, styles: [".pdx-file-upload{display:flex;flex-direction:column;gap:8px}.pdx-file-upload label{font-size:12px;font-weight:500;line-height:1.25;color:var(--md-sys-color-on-surface, rgba(0, 0, 0, .87))}.pdx-file-upload input[type=file]{max-width:100%}.pdx-file-upload__selection{display:flex;align-items:center;gap:10px;min-width:0;padding:8px;border:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .16));border-radius:8px;background:var(--md-sys-color-surface-container-low, rgba(0, 0, 0, .03))}.pdx-file-upload__preview{width:48px;height:48px;border-radius:9999px;object-fit:cover;flex:0 0 48px;border:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .16))}.pdx-file-upload__summary{display:flex;min-width:0;flex:1 1 auto;flex-direction:column;gap:2px}.pdx-file-upload__summary-title,.pdx-file-upload__summary-detail{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pdx-file-upload__summary-title{font-size:13px;font-weight:600;color:var(--md-sys-color-on-surface, rgba(0, 0, 0, .87))}.pdx-file-upload__summary-detail,.hint{font-size:12px;color:var(--md-sys-color-on-surface-variant, rgba(0, 0, 0, .6))}.pdx-file-upload__clear{border:0;background:transparent;color:var(--md-sys-color-primary, #6750a4);font:inherit;font-size:12px;font-weight:600;cursor:pointer;padding:6px}.pdx-file-upload__clear:disabled{cursor:default;opacity:.5}.error{color:var(--md-sys-color-error, #b3261e);font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
17710
18033
  }
17711
18034
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: MaterialFileUploadComponent, decorators: [{
17712
18035
  type: Component,
17713
- args: [{
17714
- selector: 'pdx-material-file-upload',
17715
- standalone: true,
17716
- imports: [ReactiveFormsModule, MatTooltipModule],
17717
- template: `
18036
+ args: [{ selector: 'pdx-material-file-upload', standalone: true, imports: [ReactiveFormsModule, MatTooltipModule], template: `
17718
18037
  <div
17719
18038
  class="pdx-file-upload"
17720
18039
  [matTooltip]="tooltipEnabled() ? errorMessage() : null"
@@ -17729,24 +18048,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
17729
18048
  [attr.accept]="fileAccept()"
17730
18049
  [attr.multiple]="allowsMultiple() ? '' : null"
17731
18050
  [disabled]="isInteractionDisabled()"
17732
- [attr.aria-disabled]="disabledMode ? 'true' : null"
18051
+ [attr.aria-disabled]="isInteractionDisabled() ? 'true' : null"
18052
+ [attr.aria-describedby]="fileInputDescribedBy()"
18053
+ [attr.aria-invalid]="hasValidationError() ? 'true' : null"
18054
+ [attr.aria-label]="inputAriaLabel()"
17733
18055
  />
18056
+ @if (selectedFiles().length) {
18057
+ <div
18058
+ class="pdx-file-upload__selection"
18059
+ aria-live="polite"
18060
+ [attr.aria-label]="selectedSummaryAriaLabel()"
18061
+ >
18062
+ @if (previewUrl()) {
18063
+ <img
18064
+ class="pdx-file-upload__preview"
18065
+ [src]="previewUrl()!"
18066
+ [attr.alt]="previewAltText()"
18067
+ />
18068
+ }
18069
+ <div class="pdx-file-upload__summary">
18070
+ <span class="pdx-file-upload__summary-title">{{ selectedSummaryText() }}</span>
18071
+ <span class="pdx-file-upload__summary-detail">{{ selectedDetailText() }}</span>
18072
+ </div>
18073
+ <button
18074
+ class="pdx-file-upload__clear"
18075
+ type="button"
18076
+ [disabled]="isInteractionDisabled()"
18077
+ [attr.aria-label]="clearAriaLabel()"
18078
+ (click)="clearSelectedFiles()"
18079
+ >
18080
+ {{ clearLabel() }}
18081
+ </button>
18082
+ </div>
18083
+ }
17734
18084
  @if (hasValidationError()) {
17735
- <div class="error">{{ errorMessage() }}</div>
18085
+ <div class="error" [attr.id]="errorId()">{{ errorMessage() }}</div>
17736
18086
  }
17737
18087
  @if (metadata()?.hint && !hasValidationError()) {
17738
- <div class="hint">{{ metadata()!.hint }}</div>
18088
+ <div class="hint" [attr.id]="hintId()">{{ metadata()!.hint }}</div>
17739
18089
  }
17740
18090
  </div>
17741
- `,
17742
- providers: [
18091
+ `, providers: [
17743
18092
  {
17744
18093
  provide: NG_VALUE_ACCESSOR,
17745
18094
  useExisting: forwardRef(() => MaterialFileUploadComponent),
17746
18095
  multi: true,
17747
18096
  },
17748
- ],
17749
- host: {
18097
+ ], host: {
17750
18098
  '[class]': 'componentCssClasses()',
17751
18099
  '[class.praxis-disabled]': 'disabledMode',
17752
18100
  '[style.display]': 'visible ? null : "none"',
@@ -17754,8 +18102,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
17754
18102
  '[attr.data-field-type]': '"file-upload"',
17755
18103
  '[attr.data-field-name]': 'metadata()?.name',
17756
18104
  '[attr.data-component-id]': 'componentId()',
17757
- },
17758
- }]
18105
+ }, styles: [".pdx-file-upload{display:flex;flex-direction:column;gap:8px}.pdx-file-upload label{font-size:12px;font-weight:500;line-height:1.25;color:var(--md-sys-color-on-surface, rgba(0, 0, 0, .87))}.pdx-file-upload input[type=file]{max-width:100%}.pdx-file-upload__selection{display:flex;align-items:center;gap:10px;min-width:0;padding:8px;border:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .16));border-radius:8px;background:var(--md-sys-color-surface-container-low, rgba(0, 0, 0, .03))}.pdx-file-upload__preview{width:48px;height:48px;border-radius:9999px;object-fit:cover;flex:0 0 48px;border:1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, .16))}.pdx-file-upload__summary{display:flex;min-width:0;flex:1 1 auto;flex-direction:column;gap:2px}.pdx-file-upload__summary-title,.pdx-file-upload__summary-detail{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pdx-file-upload__summary-title{font-size:13px;font-weight:600;color:var(--md-sys-color-on-surface, rgba(0, 0, 0, .87))}.pdx-file-upload__summary-detail,.hint{font-size:12px;color:var(--md-sys-color-on-surface-variant, rgba(0, 0, 0, .6))}.pdx-file-upload__clear{border:0;background:transparent;color:var(--md-sys-color-primary, #6750a4);font:inherit;font-size:12px;font-weight:600;cursor:pointer;padding:6px}.pdx-file-upload__clear:disabled{cursor:default;opacity:.5}.error{color:var(--md-sys-color-error, #b3261e);font-size:12px}\n"] }]
17759
18106
  }], propDecorators: { readonlyMode: [{
17760
18107
  type: Input
17761
18108
  }], disabledMode: [{
@@ -43610,7 +43957,7 @@ class InlineDistanceRadiusComponent extends InlineRangeSliderComponent {
43610
43957
  const subtitle = String(md.distanceSubtitle ?? md.inlinePanelSubtitle ?? md.inlineSubtitle ?? md.hint ?? '').trim();
43611
43958
  if (subtitle.length)
43612
43959
  return subtitle;
43613
- return this.distanceText('inlineDistance.panelSubtitle', 'Visualizacao radial com controle de raio e unidade');
43960
+ return this.distanceText('inlineDistance.panelSubtitle', 'Visualização radial com controle de raio e unidade');
43614
43961
  }
43615
43962
  unitToggleAriaLabel() {
43616
43963
  return this.distanceText('inlineDistance.unitToggleAriaLabel', 'Unidade de distancia atual: {{unit}}', {
@@ -59405,7 +59752,7 @@ const PDX_INLINE_DATE_RANGE_COMPONENT_METADATA = {
59405
59752
  selector: 'pdx-inline-date-range',
59406
59753
  component: InlineDateRangeComponent,
59407
59754
  friendlyName: 'Inline Date Range',
59408
- description: 'Intervalo de datas compacto para barras de filtro dinamico, com autosize e presets rápidos no overlay do calendário.',
59755
+ description: 'Intervalo de datas compacto para barras de filtro dinâmico, com autosize e presets rápidos no overlay do calendário.',
59409
59756
  icon: 'date_range',
59410
59757
  inputs: [
59411
59758
  { name: 'metadata', type: 'MaterialDateRangeMetadata', description: 'Configuração do campo' },
@@ -59423,10 +59770,10 @@ const PDX_INLINE_TIME_COMPONENT_METADATA = {
59423
59770
  selector: 'pdx-inline-time',
59424
59771
  component: InlineTimeComponent,
59425
59772
  friendlyName: 'Inline Time',
59426
- description: 'Timepicker compacto para barras de filtro dinamico, com largura por conteudo e selecao inline em lista ou colunas Hor/Min/Seg.',
59773
+ description: 'Timepicker compacto para barras de filtro dinâmico, com largura por conteúdo e seleção inline em lista ou colunas Hor/Min/Seg.',
59427
59774
  icon: 'schedule',
59428
59775
  inputs: [
59429
- { name: 'metadata', type: 'MaterialTimepickerMetadata', description: 'Configuracao do campo' },
59776
+ { name: 'metadata', type: 'MaterialTimepickerMetadata', description: 'Configuração do campo' },
59430
59777
  { name: 'readonlyMode', type: 'boolean', default: false, description: 'Define modo somente leitura' },
59431
59778
  { name: 'disabledMode', type: 'boolean', default: false, description: 'Desabilita o campo' },
59432
59779
  { name: 'visible', type: 'boolean', default: true, description: 'Controla a visibilidade' },
@@ -59444,7 +59791,7 @@ const PDX_INLINE_TIME_RANGE_COMPONENT_METADATA = {
59444
59791
  description: 'Intervalo de horário compacto para barras de filtro dinâmico, com pill responsiva, slider de faixa, atalhos rápidos e popover não-modal com foco de teclado tratado para uso corporativo.',
59445
59792
  icon: 'schedule',
59446
59793
  inputs: [
59447
- { name: 'metadata', type: 'MaterialTimeRangeMetadata', description: 'Configuracao do campo' },
59794
+ { name: 'metadata', type: 'MaterialTimeRangeMetadata', description: 'Configuração do campo' },
59448
59795
  { name: 'readonlyMode', type: 'boolean', default: false, description: 'Define modo somente leitura' },
59449
59796
  { name: 'disabledMode', type: 'boolean', default: false, description: 'Desabilita o campo' },
59450
59797
  { name: 'visible', type: 'boolean', default: true, description: 'Controla a visibilidade' },
@@ -59459,10 +59806,10 @@ const PDX_INLINE_TREE_SELECT_COMPONENT_METADATA = {
59459
59806
  selector: 'pdx-inline-tree-select',
59460
59807
  component: InlineTreeSelectComponent,
59461
59808
  friendlyName: 'Inline Tree Select',
59462
- description: 'Selecao hierarquica compacta para barras de filtro dinamico, com busca no popover e resumo em pill.',
59809
+ description: 'Seleção hierárquica compacta para barras de filtro dinâmico, com busca no popover e resumo em pill.',
59463
59810
  icon: 'account_tree',
59464
59811
  inputs: [
59465
- { name: 'metadata', type: 'MaterialTreeSelectMetadata', description: 'Configuracao do campo' },
59812
+ { name: 'metadata', type: 'MaterialTreeSelectMetadata', description: 'Configuração do campo' },
59466
59813
  { name: 'readonlyMode', type: 'boolean', default: false, description: 'Define modo somente leitura' },
59467
59814
  { name: 'disabledMode', type: 'boolean', default: false, description: 'Desabilita o campo' },
59468
59815
  { name: 'visible', type: 'boolean', default: true, description: 'Controla a visibilidade' },
@@ -59495,14 +59842,14 @@ const PDX_INLINE_DISTANCE_RADIUS_COMPONENT_METADATA = {
59495
59842
  selector: 'pdx-inline-distance-radius',
59496
59843
  component: InlineDistanceRadiusComponent,
59497
59844
  friendlyName: 'Inline Distance Radius',
59498
- description: 'Filtro inline de raio de distancia com visual radial, slider single/range, toggle de unidade (km/mi) e presets.',
59845
+ description: 'Filtro inline de raio de distância com visual radial, slider single/range, toggle de unidade (km/mi) e presets.',
59499
59846
  icon: 'place',
59500
59847
  inputs: [
59501
- { name: 'metadata', type: 'MaterialRangeSliderMetadata', description: 'Configuracao do campo' },
59848
+ { name: 'metadata', type: 'MaterialRangeSliderMetadata', description: 'Configuração do campo' },
59502
59849
  { name: 'readonlyMode', type: 'boolean', default: false, description: 'Define modo somente leitura' },
59503
59850
  { name: 'disabledMode', type: 'boolean', default: false, description: 'Desabilita o campo' },
59504
59851
  { name: 'visible', type: 'boolean', default: true, description: 'Controla a visibilidade' },
59505
- { name: 'presentationMode', type: 'boolean', default: false, description: 'Modo de apresentacao' },
59852
+ { name: 'presentationMode', type: 'boolean', default: false, description: 'Modo de apresentação' },
59506
59853
  ],
59507
59854
  tags: ['widget', 'field', 'distance', 'radius', 'filter', 'inline'],
59508
59855
  lib: '@praxisui/dynamic-fields',
@@ -59513,14 +59860,14 @@ const PDX_INLINE_PIPELINE_STATUS_COMPONENT_METADATA = {
59513
59860
  selector: 'pdx-inline-pipeline-status',
59514
59861
  component: InlinePipelineStatusComponent,
59515
59862
  friendlyName: 'Inline Pipeline Status',
59516
- description: 'Filtro inline de status/etapas com popover, barra segmentada e selecao single ou multipla totalmente configuravel por metadados.',
59863
+ description: 'Filtro inline de status/etapas com popover, barra segmentada e seleção single ou múltipla totalmente configurável por metadados.',
59517
59864
  icon: 'timeline',
59518
59865
  inputs: [
59519
- { name: 'metadata', type: 'MaterialSelectMetadata', description: 'Configuracao do campo' },
59866
+ { name: 'metadata', type: 'MaterialSelectMetadata', description: 'Configuração do campo' },
59520
59867
  { name: 'readonlyMode', type: 'boolean', default: false, description: 'Define modo somente leitura' },
59521
59868
  { name: 'disabledMode', type: 'boolean', default: false, description: 'Desabilita o campo' },
59522
59869
  { name: 'visible', type: 'boolean', default: true, description: 'Controla a visibilidade' },
59523
- { name: 'presentationMode', type: 'boolean', default: false, description: 'Modo de apresentacao' },
59870
+ { name: 'presentationMode', type: 'boolean', default: false, description: 'Modo de apresentação' },
59524
59871
  ],
59525
59872
  tags: ['widget', 'field', 'pipeline', 'status', 'filter', 'inline', 'multiselect'],
59526
59873
  lib: '@praxisui/dynamic-fields',
@@ -59534,7 +59881,7 @@ const PDX_INLINE_SCORE_PRIORITY_COMPONENT_METADATA = {
59534
59881
  description: 'Filtro inline de score/prioridade com faixa colorida, slider single/range, bandas configuráveis por metadados preservadas integralmente para semântica decisória, rótulos de escala com autoajuste de densidade e painel em popover não-modal.',
59535
59882
  icon: 'speed',
59536
59883
  inputs: [
59537
- { name: 'metadata', type: 'MaterialRangeSliderMetadata', description: 'Configuracao do campo' },
59884
+ { name: 'metadata', type: 'MaterialRangeSliderMetadata', description: 'Configuração do campo' },
59538
59885
  { name: 'readonlyMode', type: 'boolean', default: false, description: 'Define modo somente leitura' },
59539
59886
  { name: 'disabledMode', type: 'boolean', default: false, description: 'Desabilita o campo' },
59540
59887
  { name: 'visible', type: 'boolean', default: true, description: 'Controla a visibilidade' },
@@ -59585,10 +59932,10 @@ const PDX_INLINE_COLOR_LABEL_COMPONENT_METADATA = {
59585
59932
  selector: 'pdx-inline-color-label',
59586
59933
  component: InlineColorLabelComponent,
59587
59934
  friendlyName: 'Inline Color Label',
59588
- description: 'Filtro inline de cor/label com grid de cores, selecao visual e badges removiveis.',
59935
+ description: 'Filtro inline de cor/label com grid de cores, seleção visual e badges removíveis.',
59589
59936
  icon: 'palette',
59590
59937
  inputs: [
59591
- { name: 'metadata', type: 'MaterialSelectMetadata', description: 'Configuracao do campo' },
59938
+ { name: 'metadata', type: 'MaterialSelectMetadata', description: 'Configuração do campo' },
59592
59939
  { name: 'readonlyMode', type: 'boolean', default: false, description: 'Define modo somente leitura' },
59593
59940
  { name: 'disabledMode', type: 'boolean', default: false, description: 'Desabilita o campo' },
59594
59941
  { name: 'visible', type: 'boolean', default: true, description: 'Controla a visibilidade' },
@@ -61256,7 +61603,7 @@ const INLINE_DISTANCE_RADIUS_PREVIEW_METADATA = {
61256
61603
  distanceUnit: 'km',
61257
61604
  distanceBaseUnit: 'km',
61258
61605
  distanceAllowUnitToggle: true,
61259
- distanceSubtitle: 'Visualizacao radial com controle de raio e unidade',
61606
+ distanceSubtitle: 'Visualização radial com controle de raio e unidade',
61260
61607
  distanceRadialRings: 5,
61261
61608
  distanceAccentColor: '#3f5be6',
61262
61609
  inlineAutoSize: {
@@ -62361,10 +62708,13 @@ const DYNAMIC_FIELDS_PLAYGROUND_CATALOG = [
62361
62708
  apiPath: jsonApiPath('material-file-upload/pdx-material-file-upload.json-api.md'),
62362
62709
  metadata: {
62363
62710
  accept: '.pdf,.png,.jpg',
62364
- hint: 'Current runtime is a lightweight placeholder based on native file input.',
62711
+ maxFileSize: 4194304,
62712
+ maxFiles: 1,
62713
+ imagePreview: true,
62714
+ hint: 'Select a file locally; image flows show a preview before the host persists the file.',
62365
62715
  },
62366
- snippet: `{ "name": "attachment", "controlType": "upload", "accept": ".pdf,.png,.jpg", "hint": "Current runtime is a lightweight placeholder based on native file input." }`,
62367
- snippetNote: 'This preview intentionally demonstrates the current placeholder runtime, not a full asset-management experience.',
62716
+ snippet: `{ "name": "attachment", "controlType": "upload", "accept": ".pdf,.png,.jpg", "maxFileSize": 4194304, "maxFiles": 1, "imagePreview": true, "hint": "Select a file locally; image flows show a preview before the host persists the file." }`,
62717
+ snippetNote: 'This preview demonstrates local selection, validation and image preview. Use @praxisui/files-upload for presign, progress and persisted storage.',
62368
62718
  }),
62369
62719
  createEntry({
62370
62720
  id: 'color-input',
@@ -62470,7 +62820,7 @@ const DYNAMIC_FIELDS_PLAYGROUND_CATALOG = [
62470
62820
  metadataPatch: {
62471
62821
  extra: {
62472
62822
  icon: 'person',
62473
- ariaLabel: 'Avatar com icone de pessoa',
62823
+ ariaLabel: 'Avatar com ícone de pessoa',
62474
62824
  size: 'medium',
62475
62825
  fillMode: 'outline',
62476
62826
  themeColor: 'primary',
@@ -64059,7 +64409,7 @@ const DISPLAY_ACTION_AI_CAPABILITIES = {
64059
64409
  category: 'appearance',
64060
64410
  valueKind: 'enum',
64061
64411
  allowedValues: ['primary', 'secondary', 'tertiary', 'base', 'info', 'success', 'warning', 'error', 'dark', 'light', 'inverse', 'none'],
64062
- description: 'Cor semantica do avatar baseada nos tokens de tema Praxis.',
64412
+ description: 'Cor semântica do avatar baseada nos tokens de tema Praxis.',
64063
64413
  },
64064
64414
  {
64065
64415
  path: 'rounded',
@@ -64203,23 +64553,26 @@ const FILE_UPLOAD_AI_CAPABILITIES = {
64203
64553
  uploadMethod: ['POST', 'PUT', 'PATCH'],
64204
64554
  },
64205
64555
  notes: [
64206
- 'Capabilities for file upload controls.',
64556
+ 'Capabilities for file upload controls. The baseline pdx-material-file-upload runtime owns local selection, local validation and optional local image preview.',
64207
64557
  'Inherits basic capabilities from FieldMetadata (label, hint, validators).',
64558
+ 'Persistence capabilities such as uploadUrl, autoUpload, chunkSize, headers, credentials and method must be grounded by @praxisui/files-upload or a backend upload capability; they are not executed by the baseline local selector alone.',
64208
64559
  ],
64209
64560
  capabilities: [
64210
64561
  {
64211
64562
  path: 'uploadUrl',
64212
64563
  category: 'data',
64213
64564
  valueKind: 'string',
64214
- description: 'URL do endpoint para upload de arquivos.',
64565
+ description: 'URL do endpoint para upload de arquivos em fluxo persistente governado.',
64215
64566
  intentExamples: ['endpoint de upload de imagens', 'salvar arquivos em /api/docs'],
64567
+ safetyNotes: 'Requer runtime/capability de persistencia como @praxisui/files-upload; o seletor baseline nao executa upload HTTP.',
64216
64568
  },
64217
64569
  {
64218
64570
  path: 'autoUpload',
64219
64571
  category: 'behavior',
64220
64572
  valueKind: 'boolean',
64221
- description: 'Se true, inicia o upload automaticamente após a seleção do arquivo.',
64573
+ description: 'Se true, inicia o upload automaticamente após a seleção do arquivo em fluxo persistente governado.',
64222
64574
  intentExamples: ['upload automático', 'não iniciar upload imediatamente'],
64575
+ safetyNotes: 'Requer runtime/capability de persistencia; o seletor baseline apenas emite File/File[].',
64223
64576
  },
64224
64577
  {
64225
64578
  path: 'multiple',
@@ -64235,6 +64588,21 @@ const FILE_UPLOAD_AI_CAPABILITIES = {
64235
64588
  description: 'Tipos de arquivo aceitos (ex: "image/*", ".pdf").',
64236
64589
  intentExamples: ['aceitar apenas imagens', 'apenas PDF'],
64237
64590
  },
64591
+ {
64592
+ path: 'imagePreview',
64593
+ category: 'appearance',
64594
+ valueKind: 'boolean',
64595
+ description: 'Exibe preview local para imagens selecionadas antes da persistencia pelo host.',
64596
+ intentExamples: ['mostrar preview da foto escolhida', 'pré-visualizar avatar antes de salvar'],
64597
+ },
64598
+ {
64599
+ path: 'preview',
64600
+ category: 'appearance',
64601
+ valueKind: 'string',
64602
+ allowedValues: ['image'],
64603
+ description: 'Modo de preview local. Use "image" para fluxos de foto/avatar.',
64604
+ safetyNotes: 'Preview local não substitui upload, presign, crop persistido ou validação backend.',
64605
+ },
64238
64606
  {
64239
64607
  path: 'maxFileSize',
64240
64608
  category: 'validation',
@@ -64261,18 +64629,21 @@ const FILE_UPLOAD_AI_CAPABILITIES = {
64261
64629
  category: 'behavior',
64262
64630
  valueKind: 'number',
64263
64631
  description: 'Tamanho dos chunks em bytes para uploads grandes (file chunking).',
64632
+ safetyNotes: 'Aplicavel ao fluxo @praxisui/files-upload ou backend equivalente, nao ao seletor local baseline.',
64264
64633
  },
64265
64634
  {
64266
64635
  path: 'withCredentials',
64267
64636
  category: 'data',
64268
64637
  valueKind: 'boolean',
64269
64638
  description: 'Envia cookies e headers de autorização com a requisição de upload.',
64639
+ safetyNotes: 'Aplicavel ao fluxo @praxisui/files-upload ou backend equivalente, nao ao seletor local baseline.',
64270
64640
  },
64271
64641
  {
64272
64642
  path: 'headers',
64273
64643
  category: 'data',
64274
64644
  valueKind: 'object',
64275
64645
  description: 'Cabeçalhos HTTP personalizados para a requisição de upload.',
64646
+ safetyNotes: 'Aplicavel ao fluxo @praxisui/files-upload ou backend equivalente, nao ao seletor local baseline.',
64276
64647
  },
64277
64648
  {
64278
64649
  path: 'method',
@@ -64280,6 +64651,7 @@ const FILE_UPLOAD_AI_CAPABILITIES = {
64280
64651
  valueKind: 'enum',
64281
64652
  allowedValues: ['POST', 'PUT', 'PATCH'],
64282
64653
  description: 'Método HTTP para a requisição de upload.',
64654
+ safetyNotes: 'Aplicavel ao fluxo @praxisui/files-upload ou backend equivalente, nao ao seletor local baseline.',
64283
64655
  },
64284
64656
  {
64285
64657
  path: 'saveField',
@@ -66160,7 +66532,7 @@ function mapValuePresentation(value) {
66160
66532
  const typeToken = String(value.type ?? '').trim();
66161
66533
  if (!typeToken || !VALUE_PRESENTATION_TYPES.has(typeToken)) {
66162
66534
  if (typeToken) {
66163
- logger.warn(`[JsonSchemaMapper] Ignorando x-ui.valuePresentation.type invalido: ${typeToken}`);
66535
+ logger.warn(`[JsonSchemaMapper] Ignorando x-ui.valuePresentation.type inválido: ${typeToken}`);
66164
66536
  }
66165
66537
  return undefined;
66166
66538
  }
@@ -66173,7 +66545,7 @@ function mapValuePresentation(value) {
66173
66545
  presentation.style = styleToken;
66174
66546
  }
66175
66547
  else {
66176
- logger.warn(`[JsonSchemaMapper] Ignorando x-ui.valuePresentation.style invalido: ${styleToken}`);
66548
+ logger.warn(`[JsonSchemaMapper] Ignorando x-ui.valuePresentation.style inválido: ${styleToken}`);
66177
66549
  }
66178
66550
  }
66179
66551
  const formatToken = String(value.format ?? '').trim();