ng-hub-ui-forms 22.4.0 → 22.6.0

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, model, HostBinding, Directive, inject, TemplateRef, InjectionToken, makeEnvironmentProviders, ElementRef, ChangeDetectorRef, contentChild, contentChildren, signal, HostListener, computed, effect, numberAttribute, booleanAttribute, output, DestroyRef, viewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, viewChildren, PLATFORM_ID, afterRenderEffect, afterNextRender, Injectable, Renderer2, NgZone, afterEveryRender, HostAttributeToken, linkedSignal, forwardRef, Injector, LOCALE_ID, Pipe } from '@angular/core';
2
+ import { input, model, Directive, inject, TemplateRef, InjectionToken, makeEnvironmentProviders, ElementRef, ChangeDetectorRef, contentChild, contentChildren, signal, computed, effect, numberAttribute, booleanAttribute, output, DestroyRef, viewChild, isDevMode, ViewEncapsulation, ChangeDetectionStrategy, Component, viewChildren, PLATFORM_ID, afterRenderEffect, afterNextRender, Injectable, Renderer2, NgZone, afterEveryRender, HostAttributeToken, linkedSignal, forwardRef, Injector, HostListener, LOCALE_ID, Pipe } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { FormControl, Validators, NgControl, ControlContainer, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
5
5
  import { Observable, Subject, takeUntil, tap, animationFrameScheduler, asapScheduler, fromEvent } from 'rxjs';
@@ -258,14 +258,16 @@ class HubFormControl {
258
258
  control['toggle'] = this.toggle;
259
259
  }
260
260
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubFormControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
261
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: HubFormControl, isStandalone: true, inputs: { formControlName: { classPropertyName: "formControlName", publicName: "formControlName", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { required: "requiredChange" }, host: { properties: { "class.hidden-control": "this.hidden" } }, ngImport: i0 });
261
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: HubFormControl, isStandalone: true, inputs: { formControlName: { classPropertyName: "formControlName", publicName: "formControlName", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { required: "requiredChange" }, host: { properties: { "class.hidden-control": "hidden" } }, ngImport: i0 });
262
262
  }
263
263
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubFormControl, decorators: [{
264
- type: Directive
265
- }], propDecorators: { formControlName: [{ type: i0.Input, args: [{ isSignal: true, alias: "formControlName", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }, { type: i0.Output, args: ["requiredChange"] }], hidden: [{
266
- type: HostBinding,
267
- args: ['class.hidden-control']
268
- }] } });
264
+ type: Directive,
265
+ args: [{
266
+ host: {
267
+ '[class.hidden-control]': 'hidden'
268
+ }
269
+ }]
270
+ }], propDecorators: { formControlName: [{ type: i0.Input, args: [{ isSignal: true, alias: "formControlName", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }, { type: i0.Output, args: ["requiredChange"] }] } });
269
271
 
270
272
  /**
271
273
  * Marks a template as form helper text projected into a form field.
@@ -338,6 +340,78 @@ const defaultHubDatepickerConfig = {
338
340
  labels: defaultHubDatepickerLabels
339
341
  };
340
342
 
343
+ /** Binary size units, ascending. */
344
+ const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];
345
+ /**
346
+ * Formats a byte count as human-readable text using binary multiples (1 KB = 1024 B).
347
+ *
348
+ * Values below 10 in their unit keep one decimal (`1.4 MB`); larger ones are rounded (`24 MB`).
349
+ * The decimal separator follows the given locale, or the runtime's default when omitted.
350
+ *
351
+ * @param bytes - The size in bytes.
352
+ * @param locale - BCP 47 locale used to format the number. Defaults to the runtime locale.
353
+ * @returns The formatted size, or an empty string when `bytes` is not a finite, non-negative number.
354
+ */
355
+ function formatFileSize(bytes, locale) {
356
+ if (!Number.isFinite(bytes) || bytes < 0) {
357
+ return '';
358
+ }
359
+ if (bytes < 1024) {
360
+ return `${bytes} ${FILE_SIZE_UNITS[0]}`;
361
+ }
362
+ let value = bytes;
363
+ let unit = 0;
364
+ while (value >= 1024 && unit < FILE_SIZE_UNITS.length - 1) {
365
+ value /= 1024;
366
+ unit++;
367
+ }
368
+ const formatted = new Intl.NumberFormat(locale, { maximumFractionDigits: value < 10 ? 1 : 0 }).format(value);
369
+ return `${formatted} ${FILE_SIZE_UNITS[unit]}`;
370
+ }
371
+
372
+ /** Built-in (English) file-input labels. */
373
+ const defaultHubFileInputLabels = {
374
+ browse: 'Browse files',
375
+ dropHere: 'Drag files here, or',
376
+ remove: 'Remove file',
377
+ clear: 'Remove all',
378
+ retry: 'Retry upload',
379
+ cancel: 'Cancel upload',
380
+ filesSelected: (count) => (count === 1 ? '1 file selected' : `${count} files selected`),
381
+ formatSize: (bytes) => formatFileSize(bytes),
382
+ rejection: ({ file, reason, limit }) => {
383
+ switch (reason) {
384
+ case 'accept':
385
+ return `${file.name}: file type not allowed.`;
386
+ case 'maxSize':
387
+ return `${file.name}: exceeds the maximum size of ${formatFileSize(Number(limit))}.`;
388
+ case 'minSize':
389
+ return `${file.name}: below the minimum size of ${formatFileSize(Number(limit))}.`;
390
+ case 'maxFiles':
391
+ return `${file.name}: at most ${limit} files can be selected.`;
392
+ case 'maxTotalSize':
393
+ return `${file.name}: the total size would exceed ${formatFileSize(Number(limit))}.`;
394
+ case 'duplicate':
395
+ return `${file.name}: already selected.`;
396
+ default:
397
+ return `${file.name}: rejected.`;
398
+ }
399
+ },
400
+ constraints: ({ accept, maxSize, maxFiles }) => {
401
+ const parts = [];
402
+ if (accept) {
403
+ parts.push(accept);
404
+ }
405
+ if (maxSize != null) {
406
+ parts.push(`up to ${formatFileSize(maxSize)} each`);
407
+ }
408
+ if (maxFiles != null) {
409
+ parts.push(maxFiles === 1 ? '1 file max' : `${maxFiles} files max`);
410
+ }
411
+ return parts.join(' · ');
412
+ }
413
+ };
414
+
341
415
  /**
342
416
  * Default, human-readable messages for the most common Angular validation errors.
343
417
  *
@@ -351,6 +425,18 @@ const defaultHubDatepickerConfig = {
351
425
  */
352
426
  function defaultInvalidFeedback(key, value) {
353
427
  switch (key) {
428
+ case 'fileAccept':
429
+ return `Only ${value?.accept ?? 'certain'} files are allowed.`;
430
+ case 'fileMaxSize':
431
+ return `Each file must be at most ${formatFileSize(value?.max)}.`;
432
+ case 'fileMinSize':
433
+ return `Each file must be at least ${formatFileSize(value?.min)}.`;
434
+ case 'fileMaxTotalSize':
435
+ return `The files must add up to at most ${formatFileSize(value?.max)}.`;
436
+ case 'fileMaxFiles':
437
+ return `Select at most ${value?.max ?? ''} files.`;
438
+ case 'fileMinFiles':
439
+ return `Select at least ${value?.min ?? ''} files.`;
354
440
  case 'required':
355
441
  return 'This field is required.';
356
442
  case 'email':
@@ -380,7 +466,8 @@ function defaultInvalidFeedback(key, value) {
380
466
  const defaultHubFormsConfig = {
381
467
  invalidFeedbackTemplateFn: defaultInvalidFeedback,
382
468
  showValid: false,
383
- datepicker: defaultHubDatepickerConfig
469
+ datepicker: defaultHubDatepickerConfig,
470
+ fileInput: defaultHubFileInputLabels
384
471
  };
385
472
  /**
386
473
  * Injection token holding the resolved {@link HubFormsConfig}.
@@ -415,10 +502,14 @@ function provideHubForms(config) {
415
502
  ...config?.datepicker,
416
503
  labels: { ...defaultHubFormsConfig.datepicker.labels, ...config?.datepicker?.labels }
417
504
  };
505
+ const fileInput = {
506
+ ...defaultHubFormsConfig.fileInput,
507
+ ...config?.fileInput
508
+ };
418
509
  return makeEnvironmentProviders([
419
510
  {
420
511
  provide: HUB_FORMS_CONFIG,
421
- useValue: { ...defaultHubFormsConfig, ...config, datepicker }
512
+ useValue: { ...defaultHubFormsConfig, ...config, datepicker, fileInput }
422
513
  }
423
514
  ]);
424
515
  }
@@ -596,11 +687,13 @@ class HubFieldControl extends HubFormControl {
596
687
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.0.1", type: HubFieldControl, isStandalone: true, inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalidFeedbackTemplateFn: { classPropertyName: "invalidFeedbackTemplateFn", publicName: "invalidFeedbackTemplateFn", isSignal: true, isRequired: false, transformFunction: null }, showValid: { classPropertyName: "showValid", publicName: "showValid", isSignal: true, isRequired: false, transformFunction: null }, validFeedback: { classPropertyName: "validFeedback", publicName: "validFeedback", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange" }, host: { listeners: { "focusout": "handleBlur($event)" } }, queries: [{ propertyName: "formTextTmp", first: true, predicate: HubFormTextDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "errorTpts", predicate: HubValidationErrorDirective, isSignal: true }], usesInheritance: true, ngImport: i0 });
597
688
  }
598
689
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubFieldControl, decorators: [{
599
- type: Directive
600
- }], ctorParameters: () => [], propDecorators: { formTextTmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => HubFormTextDirective), { ...{ read: TemplateRef }, isSignal: true }] }], errorTpts: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => HubValidationErrorDirective), { isSignal: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], invalidFeedbackTemplateFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidFeedbackTemplateFn", required: false }] }], showValid: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValid", required: false }] }], validFeedback: [{ type: i0.Input, args: [{ isSignal: true, alias: "validFeedback", required: false }] }], handleBlur: [{
601
- type: HostListener,
602
- args: ['focusout', ['$event']]
603
- }] } });
690
+ type: Directive,
691
+ args: [{
692
+ host: {
693
+ '(focusout)': 'handleBlur($event)'
694
+ }
695
+ }]
696
+ }], ctorParameters: () => [], propDecorators: { formTextTmp: [{ type: i0.ContentChild, args: [i0.forwardRef(() => HubFormTextDirective), { ...{ read: TemplateRef }, isSignal: true }] }], errorTpts: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => HubValidationErrorDirective), { isSignal: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], invalidFeedbackTemplateFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidFeedbackTemplateFn", required: false }] }], showValid: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValid", required: false }] }], validFeedback: [{ type: i0.Input, args: [{ isSignal: true, alias: "validFeedback", required: false }] }] } });
604
697
 
605
698
  /**
606
699
  * Shared base for form *containers* (`hub-form`, `hub-fieldset`, `hub-legend`).
@@ -719,6 +812,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
719
812
  type: Directive
720
813
  }], ctorParameters: () => [], propDecorators: { group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: false }] }], groupName: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupName", required: false }] }], errorTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTrigger", required: false }] }], invalidFeedbackTemplateFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidFeedbackTemplateFn", required: false }] }], errorTpts: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => HubValidationErrorDirective), { isSignal: true }] }] } });
721
814
 
815
+ /**
816
+ * Injection token holding the {@link HubFileUploader} that `<hub-file-input>` delegates uploads to.
817
+ *
818
+ * It has **no default**: when nothing is registered, the component resolves it as `null` and behaves
819
+ * as a pure picker — no automatic upload, no progress bar, no cancel or retry. Register one with
820
+ * {@link provideHubFileUploader}.
821
+ */
822
+ const HUB_FILE_UPLOADER = new InjectionToken('HUB_FILE_UPLOADER');
823
+ /**
824
+ * Registers the uploader that `<hub-file-input>` uses to send files.
825
+ *
826
+ * The library defines the contract, not the transport: an uploader that knows your endpoint, field
827
+ * name, headers and retry policy is application code. Implement {@link HubFileUploader} there and
828
+ * hand it over here.
829
+ *
830
+ * Passing a class provides it in the same injector and aliases the token to that single instance, so
831
+ * the uploader can inject `HttpClient` and hold its own state.
832
+ *
833
+ * @param uploader - The uploader class (dependency-injected) or a ready-made instance.
834
+ * @returns Environment providers to add to `bootstrapApplication` (or a route's `providers`).
835
+ *
836
+ * @example
837
+ * ```ts
838
+ * bootstrapApplication(App, {
839
+ * providers: [provideHttpClient(), provideHubFileUploader(ApiFileUploader)]
840
+ * });
841
+ * ```
842
+ */
843
+ function provideHubFileUploader(uploader) {
844
+ if (typeof uploader === 'function') {
845
+ const uploaderType = uploader;
846
+ return makeEnvironmentProviders([uploaderType, { provide: HUB_FILE_UPLOADER, useExisting: uploaderType }]);
847
+ }
848
+ return makeEnvironmentProviders([{ provide: HUB_FILE_UPLOADER, useValue: uploader }]);
849
+ }
850
+
722
851
  /**
723
852
  * Enum for form helper text display types.
724
853
  */
@@ -892,9 +1021,13 @@ function applyMask(value, mask) {
892
1021
  * `invalidFeedbackTemplateFn` input, or projected `hubValidationError` templates.
893
1022
  *
894
1023
  * Formats: `text`, `number`, `password`, `email`, `tel`, `url`, `color`, `checkbox`, `switch`,
895
- * `counter`. Label types: `stacked`, `floating`, `horizontal`. Text-like formats support prepend /
896
- * append addons (input groups). Numeric formats auto-attach `min`/`max` validators when bound to a
897
- * reactive control.
1024
+ * `counter` and `file`. Label types: `stacked`, `floating`, `horizontal`. Text-like formats support
1025
+ * prepend / append addons (input groups). Numeric formats auto-attach `min`/`max` validators when
1026
+ * bound to a reactive control.
1027
+ *
1028
+ * The `file` format is **deprecated since 22.6.0** in favour of `<hub-file-input>`: it is a bare
1029
+ * picker that cannot enforce `accept` on a drop, and has no size limits, no preview and no per-file
1030
+ * removal. It keeps working until the next major.
898
1031
  *
899
1032
  * @example
900
1033
  * ```html
@@ -1001,12 +1134,25 @@ class HubInputComponent extends HubFieldControl {
1001
1134
  #lastSearchTerm = null;
1002
1135
  /** Clears any pending debounce timer when the component is destroyed. */
1003
1136
  _searchCleanup = inject(DestroyRef).onDestroy(() => clearTimeout(this.#searchTimer));
1004
- /** Accepted file types (file format), e.g. `image/*,.pdf`. */
1137
+ /**
1138
+ * Accepted file types (file format), e.g. `image/*,.pdf`.
1139
+ *
1140
+ * @deprecated Since 22.6.0. Use `<hub-file-input>`, which enforces `accept` on drops and pastes
1141
+ * too. Removed in the next major.
1142
+ */
1005
1143
  accept = input('*', /* @ts-ignore */
1006
1144
  ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
1007
- /** Whether multiple files can be selected (file format). */
1145
+ /**
1146
+ * Whether multiple files can be selected (file format).
1147
+ *
1148
+ * @deprecated Since 22.6.0. Use `<hub-file-input>`. Removed in the next major.
1149
+ */
1008
1150
  multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1009
- /** Text of the file-picker button (file format). */
1151
+ /**
1152
+ * Text of the file-picker button (file format).
1153
+ *
1154
+ * @deprecated Since 22.6.0. Use `<hub-file-input>` and its `buttonLabel`. Removed in the next major.
1155
+ */
1010
1156
  buttonLabel = input('Choose file', /* @ts-ignore */
1011
1157
  ...(ngDevMode ? [{ debugName: "buttonLabel" }] : /* istanbul ignore next */ []));
1012
1158
  /** Reference to the hidden native file input. */
@@ -1063,6 +1209,7 @@ class HubInputComponent extends HubFieldControl {
1063
1209
  ngAfterContentInit() {
1064
1210
  super.ngAfterContentInit();
1065
1211
  this.#attachNumericValidators();
1212
+ this.#warnDeprecatedFileFormat();
1066
1213
  }
1067
1214
  writeValue(value) {
1068
1215
  if (this.isCheckable()) {
@@ -1197,6 +1344,17 @@ class HubInputComponent extends HubFieldControl {
1197
1344
  this.onChange?.(null);
1198
1345
  this.valueChange.emit(null);
1199
1346
  }
1347
+ /**
1348
+ * Warns once, in development only, that the `file` format is deprecated.
1349
+ *
1350
+ * It still works, but it cannot enforce `accept` on a drop, has no size limits, no preview and
1351
+ * no per-file removal. `<hub-file-input>` supersedes it.
1352
+ */
1353
+ #warnDeprecatedFileFormat() {
1354
+ if (isDevMode() && this.type() === this._inputFormats.File) {
1355
+ console.warn('[ng-hub-ui-forms] <hub-input type="file"> is deprecated since 22.6.0 and will be removed in the next major. Use <hub-file-input> instead.');
1356
+ }
1357
+ }
1200
1358
  /**
1201
1359
  * Auto-attaches `Validators.min` / `Validators.max` to the bound reactive control when `min` /
1202
1360
  * `max` are set and not already present, so the inline bounds become real validation.
@@ -1593,18 +1751,14 @@ class HubAutoresizeDirective {
1593
1751
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubAutoresizeDirective, decorators: [{
1594
1752
  type: Directive,
1595
1753
  args: [{
1596
- selector: 'textarea[hubAutoresize]'
1754
+ selector: 'textarea[hubAutoresize]',
1755
+ host: {
1756
+ '(:focusin)': 'onChange()',
1757
+ '(:focusout)': 'onChange()',
1758
+ '(:input)': 'onChange()'
1759
+ }
1597
1760
  }]
1598
- }], propDecorators: { hubAutoresize: [{ type: i0.Input, args: [{ isSignal: true, alias: "hubAutoresize", required: false }] }], onChange: [{
1599
- type: HostListener,
1600
- args: [':focusin']
1601
- }, {
1602
- type: HostListener,
1603
- args: [':focusout']
1604
- }, {
1605
- type: HostListener,
1606
- args: [':input']
1607
- }] } });
1761
+ }], propDecorators: { hubAutoresize: [{ type: i0.Input, args: [{ isSignal: true, alias: "hubAutoresize", required: false }] }] } });
1608
1762
 
1609
1763
  /**
1610
1764
  * Accessible multi-line text field with optional auto-resize, character counter and automatic
@@ -1816,6 +1970,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
1816
1970
  }, template: "<div\n\tclass=\"hub-field hub-slider\"\n\t[class.hub-field--horizontal]=\"label() && labelType() === _labelTypes.Horizontal\"\n\t[class.hub-field--disabled]=\"disabled()\"\n\t[class.hub-field--invalid]=\"isInvalid\"\n\t[class.hub-field--valid]=\"showsValid\"\n\t[style.--hub-slider-percent]=\"percent()\"\n\t[style.--hub-slider-from]=\"lowerPercent()\"\n\t[style.--hub-slider-to]=\"upperPercent()\"\n>\n\t@if (label() || required()) {\n\t\t<label [for]=\"id\" class=\"hub-field__label\">\n\t\t\t{{ label() }}\n\t\t\t@if (required()) {\n\t\t\t\t<span class=\"hub-field__required\" aria-hidden=\"true\">*</span>\n\t\t\t}\n\t\t</label>\n\t}\n\n\t<div\n\t\tclass=\"hub-field__body hub-slider__range\"\n\t\t[class.hub-slider__range--dual]=\"range()\"\n\t\t[class.hub-slider__range--flush]=\"!showValue()\"\n\t>\n\t\t@if (range()) {\n\t\t\t<div class=\"hub-slider__rail\" aria-hidden=\"true\"></div>\n\n\t\t\t<input\n\t\t\t\ttype=\"range\"\n\t\t\t\tclass=\"hub-slider__track hub-slider__track--lower\"\n\t\t\t\t[id]=\"id\"\n\t\t\t\t[min]=\"min()\"\n\t\t\t\t[max]=\"max()\"\n\t\t\t\t[step]=\"step()\"\n\t\t\t\t[value]=\"lower()\"\n\t\t\t\t(input)=\"setLower($any($event.target).value)\"\n\t\t\t\t[disabled]=\"disabled()\"\n\t\t\t\taria-label=\"Range start\"\n\t\t\t\t[attr.aria-valuetext]=\"lower()\"\n\t\t\t/>\n\t\t\t<input\n\t\t\t\ttype=\"range\"\n\t\t\t\tclass=\"hub-slider__track hub-slider__track--upper\"\n\t\t\t\t[min]=\"min()\"\n\t\t\t\t[max]=\"max()\"\n\t\t\t\t[step]=\"step()\"\n\t\t\t\t[value]=\"upper()\"\n\t\t\t\t(input)=\"setUpper($any($event.target).value)\"\n\t\t\t\t[disabled]=\"disabled()\"\n\t\t\t\taria-label=\"Range end\"\n\t\t\t\t[attr.aria-valuetext]=\"upper()\"\n\t\t\t/>\n\n\t\t\t@if (showValue()) {\n\t\t\t\t<span class=\"hub-slider__bubble hub-slider__bubble--lower\" aria-hidden=\"true\">{{ lower() }}</span>\n\t\t\t\t<span class=\"hub-slider__bubble hub-slider__bubble--upper\" aria-hidden=\"true\">{{ upper() }}</span>\n\t\t\t}\n\t\t} @else {\n\t\t\t<input\n\t\t\t\ttype=\"range\"\n\t\t\t\tclass=\"hub-slider__track\"\n\t\t\t\t[id]=\"id\"\n\t\t\t\t[min]=\"min()\"\n\t\t\t\t[max]=\"max()\"\n\t\t\t\t[step]=\"step()\"\n\t\t\t\t[value]=\"single()\"\n\t\t\t\t(input)=\"setValue($any($event.target).value)\"\n\t\t\t\t[disabled]=\"disabled()\"\n\t\t\t\t[required]=\"!!required()\"\n\t\t\t\t[attr.aria-valuetext]=\"single()\"\n\t\t\t/>\n\n\t\t\t@if (showValue()) {\n\t\t\t\t<span class=\"hub-slider__bubble\" aria-hidden=\"true\">{{ single() }}</span>\n\t\t\t}\n\t\t}\n\t</div>\n\n\t@if (formText() || formTextTmp()) {\n\t\t<div class=\"hub-field__form-text\" [class.hub-field__form-text--disabled]=\"disabled()\">\n\t\t\t@if (formTextTmp()) {\n\t\t\t\t<ng-container [ngTemplateOutlet]=\"formTextTmp()!\"></ng-container>\n\t\t\t} @else {\n\t\t\t\t{{ formText() }}\n\t\t\t}\n\t\t</div>\n\t}\n\n\t@if (isInvalid) {\n\t\t<div class=\"hub-field__feedback\" role=\"alert\">\n\t\t\t<ul>\n\t\t\t\t@for (error of errors | keyvalue; track error.key) {\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"_errorTemplates[error.key] ?? defaultErrorTpt\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: error.value, value: error.value }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t<ng-template #defaultErrorTpt>\n\t\t\t\t\t\t\t<span class=\"hub-field__feedback-text\" [innerHTML]=\"getInvalidFeedbackTemplate(error.key, error.value)\"></span>\n\t\t\t\t\t\t</ng-template>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\t\t</div>\n\t}\n\n\t@if (showsValid && validFeedback()) {\n\t\t<div class=\"hub-field__feedback hub-field__feedback--valid\" role=\"status\">\n\t\t\t<span class=\"hub-field__feedback-text\">{{ validFeedback() }}</span>\n\t\t</div>\n\t}\n</div>\n", styles: ["hub-slider{display:block}hub-slider.hidden-control{display:none!important}.hub-slider__range{position:relative;padding-top:var(--hub-slider-value-space)}.hub-slider__range--flush{--hub-slider-value-space: 0}.hub-slider__track{width:var(--hub-slider-track-width);height:var(--hub-slider-track-height);margin:0;appearance:none;-webkit-appearance:none;background:var(--hub-slider-track-fill) no-repeat,var(--hub-slider-track-bg);background-size:calc(var(--hub-slider-percent) * 1%) 100%,100% 100%;border-radius:var(--hub-slider-track-border-radius);cursor:pointer}.hub-slider__track:disabled{opacity:var(--hub-form-disabled-opacity);cursor:not-allowed}.hub-slider__track::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;width:var(--hub-slider-thumb-width);height:var(--hub-slider-thumb-height);border-radius:var(--hub-slider-thumb-border-radius);background:var(--hub-slider-thumb-bg);border:var(--hub-slider-thumb-border);box-shadow:var(--hub-slider-thumb-shadow);cursor:pointer}.hub-slider__track::-moz-range-thumb{width:var(--hub-slider-thumb-width);height:var(--hub-slider-thumb-height);border-radius:var(--hub-slider-thumb-border-radius);background:var(--hub-slider-thumb-bg);border:var(--hub-slider-thumb-border);box-shadow:var(--hub-slider-thumb-shadow);cursor:pointer}.hub-slider__track::-moz-range-track{height:var(--hub-slider-track-height);background:transparent;border-radius:var(--hub-slider-track-border-radius)}.hub-slider__track:focus-visible{outline:0}.hub-slider__track:focus-visible::-webkit-slider-thumb{box-shadow:var(--hub-slider-thumb-shadow),var(--hub-input-focus-box-shadow)}.hub-slider__track:focus-visible::-moz-range-thumb{box-shadow:var(--hub-slider-thumb-shadow),var(--hub-input-focus-box-shadow)}.hub-slider__bubble{position:absolute;top:0;left:calc(var(--hub-slider-percent) * 1%);transform:translate(-50%);padding:.1rem .4rem;color:var(--hub-slider-tooltip-color);background:var(--hub-slider-tooltip-bg);font-size:var(--hub-ref-font-size-sm, .875rem);line-height:1.4;border-radius:var(--hub-slider-tooltip-border-radius);white-space:nowrap;pointer-events:none}.hub-slider__bubble--lower{left:calc(var(--hub-slider-from) * 1%)}.hub-slider__bubble--upper{left:calc(var(--hub-slider-to) * 1%)}.hub-slider__range--dual{min-height:calc(var(--hub-slider-value-space) + var(--hub-slider-thumb-height))}.hub-slider__range--dual .hub-slider__rail,.hub-slider__range--dual .hub-slider__track--lower,.hub-slider__range--dual .hub-slider__track--upper{position:absolute;left:0;right:0;top:calc(var(--hub-slider-value-space) + var(--hub-slider-thumb-height) / 2)}.hub-slider__range--dual .hub-slider__rail{height:var(--hub-slider-track-height);transform:translateY(-50%);border-radius:var(--hub-slider-track-border-radius);background:var(--hub-slider-track-fill) no-repeat,var(--hub-slider-track-bg);background-size:calc((var(--hub-slider-to) - var(--hub-slider-from)) * 1%) 100%,100% 100%;background-position:calc(var(--hub-slider-from) / max(100 - var(--hub-slider-to) + var(--hub-slider-from),1) * 100%) center,0 0;pointer-events:none}.hub-slider__range--dual .hub-slider__track--lower,.hub-slider__range--dual .hub-slider__track--upper{width:100%;height:var(--hub-slider-thumb-height);margin:0;transform:translateY(-50%);background:transparent;pointer-events:none}.hub-slider__range--dual .hub-slider__track--lower::-webkit-slider-thumb,.hub-slider__range--dual .hub-slider__track--upper::-webkit-slider-thumb{pointer-events:auto;position:relative;z-index:2}.hub-slider__range--dual .hub-slider__track--lower::-moz-range-thumb,.hub-slider__range--dual .hub-slider__track--upper::-moz-range-thumb{pointer-events:auto}.hub-slider__range--dual .hub-slider__track--lower::-moz-range-track,.hub-slider__range--dual .hub-slider__track--upper::-moz-range-track{background:transparent}\n"] }]
1817
1971
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], labelType: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelType", required: false }] }], range: [{ type: i0.Input, args: [{ isSignal: true, alias: "range", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], showValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValue", required: false }] }], formText: [{ type: i0.Input, args: [{ isSignal: true, alias: "formText", required: false }] }], formTextType: [{ type: i0.Input, args: [{ isSignal: true, alias: "formTextType", required: false }] }], classlist: [{ type: i0.Input, args: [{ isSignal: true, alias: "classlist", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
1818
1972
 
1973
+ /**
1974
+ * Resolves a user-supplied accent value to a paintable CSS colour for a `--hub-*-accent` slot.
1975
+ *
1976
+ * A bareword (a built-in semantic name such as `primary` / `success`, or any registered accent /
1977
+ * named CSS colour) becomes a `--hub-sys-color-*` design-system token with the word itself as the
1978
+ * raw fallback, so named CSS colours keep working when no token is registered. A literal colour
1979
+ * (`#ff0000`, `rgb(...)`, `oklch(...)`, `var(...)`) is passed through unchanged. Empty / whitespace
1980
+ * input resolves to `null` so the caller can keep its neutral default.
1981
+ *
1982
+ * @param value - The raw accent value (e.g. from a `color` input).
1983
+ * @returns The resolved CSS colour, or `null` when no accent is provided.
1984
+ */
1985
+ function resolveHubAccent(value) {
1986
+ const color = value?.trim();
1987
+ if (!color)
1988
+ return null;
1989
+ return /^[a-zA-Z][\w-]*$/.test(color) ? `var(--hub-sys-color-${color}, ${color})` : color;
1990
+ }
1991
+
1819
1992
  /**
1820
1993
  * Segmented button bar (`hub-segmented`): a compact group of 2..n options rendered as an inline
1821
1994
  * (or vertical) segmented control. A full `ng-hub-ui-forms` field — it extends
@@ -1848,15 +2021,22 @@ class HubSegmentedComponent extends HubFieldControl {
1848
2021
  size = input('md', /* @ts-ignore */
1849
2022
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1850
2023
  /**
1851
- * Semantic accent for the selected pill. Drives `data-variant` on the bar, which re-bases the
1852
- * `--hub-segmented-selected-*` tokens. Built-in values map to the design-system palette
1853
- * (`primary`, `secondary`, `success`, `danger`, `warning`, `info`, `neutral`), but the set is
1854
- * OPEN — any custom string works as long as the consumer supplies the matching
1855
- * `--hub-segmented-selected-bg` / `--hub-segmented-selected-color` (or `--hub-segmented-accent`)
1856
- * for that `data-variant`. Empty string (default) keeps the neutral white-pill look.
2024
+ * Accent for the selected pill. Accepts ANY colour value:
2025
+ * - a built-in semantic name (`primary`, `success`, …) or any registered accent → resolves to
2026
+ * the matching `--hub-sys-color-*` design-system token;
2027
+ * - a literal colour (`#ff0000`, `rgb(...)`, `oklch(...)`, `rebeccapurple`) → used as-is.
2028
+ * Both the sliding indicator and the multiple-mode pill follow it, and the selected label gets a
2029
+ * derived contrast colour automatically. Empty string (default) keeps the neutral white-pill look.
1857
2030
  */
1858
2031
  color = input('', /* @ts-ignore */
1859
2032
  ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
2033
+ /**
2034
+ * Resolves {@link color} to a paintable accent for the `--hub-segmented-accent` slot: a bareword
2035
+ * becomes a `--hub-sys-color-*` token with the word as raw fallback (so named CSS colours work
2036
+ * too), while a literal `#hex` / `rgb()` / `oklch()` / `var()` is passed through unchanged.
2037
+ */
2038
+ accentVar = computed(() => resolveHubAccent(this.color()), /* @ts-ignore */
2039
+ ...(ngDevMode ? [{ debugName: "accentVar" }] : /* istanbul ignore next */ []));
1860
2040
  /** Label text. */
1861
2041
  label = input('', /* @ts-ignore */
1862
2042
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
@@ -2051,13 +2231,13 @@ class HubSegmentedComponent extends HubFieldControl {
2051
2231
  return Object.is(a, b);
2052
2232
  }
2053
2233
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSegmentedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2054
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: HubSegmentedComponent, isStandalone: true, selector: "hub-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, labelType: { classPropertyName: "labelType", publicName: "labelType", isSignal: true, isRequired: false, transformFunction: null }, formText: { classPropertyName: "formText", publicName: "formText", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, vertical: { classPropertyName: "vertical", publicName: "vertical", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { properties: { "class.hub-segmented-host": "true" } }, viewQueries: [{ propertyName: "_buttons", predicate: ["optionButton"], descendants: true, isSignal: true }, { propertyName: "_bar", first: true, predicate: ["segmentedBar"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n\tclass=\"hub-field hub-segmented-field\"\n\t[class.hub-field--horizontal]=\"label() && labelType() === _labelTypes.Horizontal\"\n\t[class.hub-field--disabled]=\"disabled()\"\n\t[class.hub-field--invalid]=\"isInvalid\"\n\t[class.hub-field--valid]=\"showsValid\"\n>\n\t@if (label() || required()) {\n\t\t<label [for]=\"id\" class=\"hub-field__label\">\n\t\t\t{{ label() }}\n\t\t\t@if (required()) {\n\t\t\t\t<span class=\"hub-field__required\" aria-hidden=\"true\">*</span>\n\t\t\t}\n\t\t</label>\n\t}\n\n\t<div class=\"hub-field__body\">\n\t\t<div\n\t\t\t#segmentedBar\n\t\t\t[id]=\"id\"\n\t\t\tclass=\"hub-segmented\"\n\t\t\t[class.hub-segmented--sm]=\"size() === 'sm'\"\n\t\t\t[class.hub-segmented--lg]=\"size() === 'lg'\"\n\t\t\t[class.hub-segmented--vertical]=\"vertical()\"\n\t\t\t[class.hub-segmented--multiple]=\"multiple()\"\n\t\t\t[class.hub-segmented--disabled]=\"disabled()\"\n\t\t\t[attr.data-variant]=\"color() || null\"\n\t\t\t[attr.role]=\"multiple() ? 'group' : 'radiogroup'\"\n\t\t\t[attr.aria-disabled]=\"disabled() ? 'true' : null\"\n\t\t\t(keydown)=\"onKeydown($event)\"\n\t\t>\n\t\t\t@if (!multiple()) {\n\t\t\t\t<span class=\"hub-segmented__indicator\" aria-hidden=\"true\"></span>\n\t\t\t}\n\n\t\t\t@for (option of options(); track option.value; let index = $index) {\n\t\t\t\t<button\n\t\t\t\t\t#optionButton\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"hub-segmented__option\"\n\t\t\t\t\t[class.hub-segmented__option--selected]=\"isSelected(option)\"\n\t\t\t\t\t[attr.role]=\"multiple() ? null : 'radio'\"\n\t\t\t\t\t[attr.aria-checked]=\"multiple() ? null : isSelected(option)\"\n\t\t\t\t\t[attr.aria-pressed]=\"multiple() ? isSelected(option) : null\"\n\t\t\t\t\t[disabled]=\"disabled() || !!option.disabled\"\n\t\t\t\t\t[tabindex]=\"rovingIndex() === index ? 0 : -1\"\n\t\t\t\t\t(click)=\"select(option)\"\n\t\t\t\t>\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</button>\n\t\t\t}\n\t\t</div>\n\t</div>\n\n\t@if (formText() || formTextTmp()) {\n\t\t<div class=\"hub-field__form-text\" [class.hub-field__form-text--disabled]=\"disabled()\">\n\t\t\t@if (formTextTmp()) {\n\t\t\t\t<ng-container [ngTemplateOutlet]=\"formTextTmp()!\"></ng-container>\n\t\t\t} @else {\n\t\t\t\t{{ formText() }}\n\t\t\t}\n\t\t</div>\n\t}\n\n\t@if (isInvalid) {\n\t\t<div class=\"hub-field__feedback\" role=\"alert\">\n\t\t\t<ul>\n\t\t\t\t@for (error of errors | keyvalue; track error.key) {\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"_errorTemplates[error.key] ?? defaultErrorTpt\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: error.value, value: error.value }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t<ng-template #defaultErrorTpt>\n\t\t\t\t\t\t\t<span class=\"hub-field__feedback-text\" [innerHTML]=\"getInvalidFeedbackTemplate(error.key, error.value)\"></span>\n\t\t\t\t\t\t</ng-template>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\t\t</div>\n\t}\n\n\t@if (showsValid && validFeedback()) {\n\t\t<div class=\"hub-field__feedback hub-field__feedback--valid\" role=\"status\">\n\t\t\t<span class=\"hub-field__feedback-text\">{{ validFeedback() }}</span>\n\t\t</div>\n\t}\n</div>\n", styles: ["hub-segmented{display:block}hub-segmented.hidden-control{display:none!important}.hub-segmented{position:relative;display:inline-flex;gap:var(--hub-segmented-gap);padding:var(--hub-segmented-gap);background:var(--hub-segmented-bg);border-radius:var(--hub-segmented-radius)}.hub-segmented__indicator{position:absolute;z-index:0;left:var(--hub-segmented-indicator-x, 0);top:var(--hub-segmented-indicator-y, 0);width:var(--hub-segmented-indicator-width, 0);height:var(--hub-segmented-indicator-height, 0);background:var(--hub-segmented-selected-bg);border-radius:calc(var(--hub-segmented-radius) - var(--hub-segmented-gap));box-shadow:var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075));pointer-events:none;transition:left var(--hub-segmented-indicator-transition),top var(--hub-segmented-indicator-transition),width var(--hub-segmented-indicator-transition),height var(--hub-segmented-indicator-transition)}@media(prefers-reduced-motion:reduce){.hub-segmented .hub-segmented__indicator{transition:none}}.hub-segmented--disabled{opacity:var(--hub-form-disabled-opacity);cursor:not-allowed}.hub-segmented--vertical{flex-direction:column}.hub-segmented--vertical .hub-segmented__option{width:100%}.hub-segmented--multiple .hub-segmented__option--selected{background:var(--hub-segmented-selected-bg);box-shadow:var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075))}.hub-segmented__option{appearance:none;border:0;margin:0;position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;padding:var(--hub-segmented-padding-y) var(--hub-segmented-padding-x);color:var(--hub-sys-text-muted, #6c757d);background:transparent;font:inherit;line-height:1.2;white-space:nowrap;border-radius:calc(var(--hub-segmented-radius) - var(--hub-segmented-gap));cursor:pointer;transition:var(--hub-form-transition),color .15s ease-in-out,background-color .15s ease-in-out}.hub-segmented__option:focus-visible{outline:0;box-shadow:0 0 0 var(--hub-form-focus-ring-width) var(--hub-form-focus-ring-color)}.hub-segmented__option--selected{color:var(--hub-segmented-selected-color)}.hub-segmented__option:disabled{cursor:not-allowed;opacity:var(--hub-form-disabled-opacity)}.hub-segmented--sm .hub-segmented__option{padding:calc(var(--hub-segmented-padding-y) * .5) calc(var(--hub-segmented-padding-x) * .65);font-size:var(--hub-ref-font-size-sm, .875rem)}.hub-segmented--lg .hub-segmented__option{padding:calc(var(--hub-segmented-padding-y) * 1.35) calc(var(--hub-segmented-padding-x) * 1.35);font-size:var(--hub-ref-font-size-lg, 1.125rem)}:where(.hub-segmented[data-variant=primary]){--hub-segmented-selected-bg: var(--hub-sys-color-primary);--hub-segmented-selected-color: var(--hub-sys-color-primary-on, #fff)}:where(.hub-segmented[data-variant=secondary]){--hub-segmented-selected-bg: var(--hub-sys-color-secondary);--hub-segmented-selected-color: var(--hub-sys-color-secondary-on, #fff)}:where(.hub-segmented[data-variant=success]){--hub-segmented-selected-bg: var(--hub-sys-color-success);--hub-segmented-selected-color: var(--hub-sys-color-success-on, #fff)}:where(.hub-segmented[data-variant=danger]){--hub-segmented-selected-bg: var(--hub-sys-color-danger);--hub-segmented-selected-color: var(--hub-sys-color-danger-on, #fff)}:where(.hub-segmented[data-variant=warning]){--hub-segmented-selected-bg: var(--hub-sys-color-warning);--hub-segmented-selected-color: var(--hub-sys-color-warning-on, #fff)}:where(.hub-segmented[data-variant=info]){--hub-segmented-selected-bg: var(--hub-sys-color-info);--hub-segmented-selected-color: var(--hub-sys-color-info-on, #fff)}:where(.hub-segmented[data-variant=neutral]){--hub-segmented-selected-bg: var(--hub-sys-color-neutral);--hub-segmented-selected-color: var(--hub-sys-color-neutral-on, #fff)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2234
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: HubSegmentedComponent, isStandalone: true, selector: "hub-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, labelType: { classPropertyName: "labelType", publicName: "labelType", isSignal: true, isRequired: false, transformFunction: null }, formText: { classPropertyName: "formText", publicName: "formText", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, vertical: { classPropertyName: "vertical", publicName: "vertical", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { properties: { "class.hub-segmented-host": "true" } }, viewQueries: [{ propertyName: "_buttons", predicate: ["optionButton"], descendants: true, isSignal: true }, { propertyName: "_bar", first: true, predicate: ["segmentedBar"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n\tclass=\"hub-field hub-segmented-field\"\n\t[class.hub-field--horizontal]=\"label() && labelType() === _labelTypes.Horizontal\"\n\t[class.hub-field--disabled]=\"disabled()\"\n\t[class.hub-field--invalid]=\"isInvalid\"\n\t[class.hub-field--valid]=\"showsValid\"\n>\n\t@if (label() || required()) {\n\t\t<label [for]=\"id\" class=\"hub-field__label\">\n\t\t\t{{ label() }}\n\t\t\t@if (required()) {\n\t\t\t\t<span class=\"hub-field__required\" aria-hidden=\"true\">*</span>\n\t\t\t}\n\t\t</label>\n\t}\n\n\t<div class=\"hub-field__body\">\n\t\t<div\n\t\t\t#segmentedBar\n\t\t\t[id]=\"id\"\n\t\t\tclass=\"hub-segmented\"\n\t\t\t[class.hub-segmented--sm]=\"size() === 'sm'\"\n\t\t\t[class.hub-segmented--lg]=\"size() === 'lg'\"\n\t\t\t[class.hub-segmented--vertical]=\"vertical()\"\n\t\t\t[class.hub-segmented--multiple]=\"multiple()\"\n\t\t\t[class.hub-segmented--disabled]=\"disabled()\"\n\t\t\t[attr.data-variant]=\"color() || null\"\n\t\t\t[style.--hub-segmented-accent]=\"accentVar()\"\n\t\t\t[attr.role]=\"multiple() ? 'group' : 'radiogroup'\"\n\t\t\t[attr.aria-disabled]=\"disabled() ? 'true' : null\"\n\t\t\t(keydown)=\"onKeydown($event)\"\n\t\t>\n\t\t\t@if (!multiple()) {\n\t\t\t\t<span class=\"hub-segmented__indicator\" aria-hidden=\"true\"></span>\n\t\t\t}\n\n\t\t\t@for (option of options(); track option.value; let index = $index) {\n\t\t\t\t<button\n\t\t\t\t\t#optionButton\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"hub-segmented__option\"\n\t\t\t\t\t[class.hub-segmented__option--selected]=\"isSelected(option)\"\n\t\t\t\t\t[attr.role]=\"multiple() ? null : 'radio'\"\n\t\t\t\t\t[attr.aria-checked]=\"multiple() ? null : isSelected(option)\"\n\t\t\t\t\t[attr.aria-pressed]=\"multiple() ? isSelected(option) : null\"\n\t\t\t\t\t[disabled]=\"disabled() || !!option.disabled\"\n\t\t\t\t\t[tabindex]=\"rovingIndex() === index ? 0 : -1\"\n\t\t\t\t\t(click)=\"select(option)\"\n\t\t\t\t>\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</button>\n\t\t\t}\n\t\t</div>\n\t</div>\n\n\t@if (formText() || formTextTmp()) {\n\t\t<div class=\"hub-field__form-text\" [class.hub-field__form-text--disabled]=\"disabled()\">\n\t\t\t@if (formTextTmp()) {\n\t\t\t\t<ng-container [ngTemplateOutlet]=\"formTextTmp()!\"></ng-container>\n\t\t\t} @else {\n\t\t\t\t{{ formText() }}\n\t\t\t}\n\t\t</div>\n\t}\n\n\t@if (isInvalid) {\n\t\t<div class=\"hub-field__feedback\" role=\"alert\">\n\t\t\t<ul>\n\t\t\t\t@for (error of errors | keyvalue; track error.key) {\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"_errorTemplates[error.key] ?? defaultErrorTpt\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: error.value, value: error.value }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t<ng-template #defaultErrorTpt>\n\t\t\t\t\t\t\t<span class=\"hub-field__feedback-text\" [innerHTML]=\"getInvalidFeedbackTemplate(error.key, error.value)\"></span>\n\t\t\t\t\t\t</ng-template>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\t\t</div>\n\t}\n\n\t@if (showsValid && validFeedback()) {\n\t\t<div class=\"hub-field__feedback hub-field__feedback--valid\" role=\"status\">\n\t\t\t<span class=\"hub-field__feedback-text\">{{ validFeedback() }}</span>\n\t\t</div>\n\t}\n</div>\n", styles: ["hub-segmented{display:block}hub-segmented.hidden-control{display:none!important}.hub-segmented{position:relative;display:inline-flex;gap:var(--hub-segmented-gap);padding:var(--hub-segmented-gap);background:var(--hub-segmented-bg);border-radius:var(--hub-segmented-radius)}.hub-segmented__indicator{position:absolute;z-index:0;left:var(--hub-segmented-indicator-x, 0);top:var(--hub-segmented-indicator-y, 0);width:var(--hub-segmented-indicator-width, 0);height:var(--hub-segmented-indicator-height, 0);background:var(--hub-segmented-selected-bg);border-radius:calc(var(--hub-segmented-radius) - var(--hub-segmented-gap));box-shadow:var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075));pointer-events:none;transition:left var(--hub-segmented-indicator-transition),top var(--hub-segmented-indicator-transition),width var(--hub-segmented-indicator-transition),height var(--hub-segmented-indicator-transition)}@media(prefers-reduced-motion:reduce){.hub-segmented .hub-segmented__indicator{transition:none}}.hub-segmented--disabled{opacity:var(--hub-form-disabled-opacity);cursor:not-allowed}.hub-segmented--vertical{flex-direction:column}.hub-segmented--vertical .hub-segmented__option{width:100%}.hub-segmented--multiple .hub-segmented__option--selected{background:var(--hub-segmented-selected-bg);box-shadow:var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075))}.hub-segmented__option{appearance:none;border:0;margin:0;position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;padding:var(--hub-segmented-padding-y) var(--hub-segmented-padding-x);color:var(--hub-sys-text-muted, #6c757d);background:transparent;font:inherit;line-height:1.2;white-space:nowrap;border-radius:calc(var(--hub-segmented-radius) - var(--hub-segmented-gap));cursor:pointer;transition:var(--hub-form-transition),color .15s ease-in-out,background-color .15s ease-in-out}.hub-segmented__option:focus-visible{outline:0;box-shadow:0 0 0 var(--hub-form-focus-ring-width) var(--hub-form-focus-ring-color)}.hub-segmented__option--selected{color:var(--hub-segmented-selected-color)}.hub-segmented__option:disabled{cursor:not-allowed;opacity:var(--hub-form-disabled-opacity)}.hub-segmented--sm .hub-segmented__option{padding:calc(var(--hub-segmented-padding-y) * .5) calc(var(--hub-segmented-padding-x) * .65);font-size:var(--hub-ref-font-size-sm, .875rem)}.hub-segmented--lg .hub-segmented__option{padding:calc(var(--hub-segmented-padding-y) * 1.35) calc(var(--hub-segmented-padding-x) * 1.35);font-size:var(--hub-ref-font-size-lg, 1.125rem)}:where(.hub-segmented[data-variant]){--hub-segmented-selected-bg: var(--hub-segmented-accent);--hub-segmented-selected-color: oklch(from var(--hub-segmented-accent) clamp(0, (.62 - l) * 1000, 1) 0 h)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2055
2235
  }
2056
2236
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSegmentedComponent, decorators: [{
2057
2237
  type: Component,
2058
2238
  args: [{ selector: 'hub-segmented', standalone: true, imports: [NgTemplateOutlet, KeyValuePipe], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
2059
2239
  '[class.hub-segmented-host]': 'true'
2060
- }, template: "<div\n\tclass=\"hub-field hub-segmented-field\"\n\t[class.hub-field--horizontal]=\"label() && labelType() === _labelTypes.Horizontal\"\n\t[class.hub-field--disabled]=\"disabled()\"\n\t[class.hub-field--invalid]=\"isInvalid\"\n\t[class.hub-field--valid]=\"showsValid\"\n>\n\t@if (label() || required()) {\n\t\t<label [for]=\"id\" class=\"hub-field__label\">\n\t\t\t{{ label() }}\n\t\t\t@if (required()) {\n\t\t\t\t<span class=\"hub-field__required\" aria-hidden=\"true\">*</span>\n\t\t\t}\n\t\t</label>\n\t}\n\n\t<div class=\"hub-field__body\">\n\t\t<div\n\t\t\t#segmentedBar\n\t\t\t[id]=\"id\"\n\t\t\tclass=\"hub-segmented\"\n\t\t\t[class.hub-segmented--sm]=\"size() === 'sm'\"\n\t\t\t[class.hub-segmented--lg]=\"size() === 'lg'\"\n\t\t\t[class.hub-segmented--vertical]=\"vertical()\"\n\t\t\t[class.hub-segmented--multiple]=\"multiple()\"\n\t\t\t[class.hub-segmented--disabled]=\"disabled()\"\n\t\t\t[attr.data-variant]=\"color() || null\"\n\t\t\t[attr.role]=\"multiple() ? 'group' : 'radiogroup'\"\n\t\t\t[attr.aria-disabled]=\"disabled() ? 'true' : null\"\n\t\t\t(keydown)=\"onKeydown($event)\"\n\t\t>\n\t\t\t@if (!multiple()) {\n\t\t\t\t<span class=\"hub-segmented__indicator\" aria-hidden=\"true\"></span>\n\t\t\t}\n\n\t\t\t@for (option of options(); track option.value; let index = $index) {\n\t\t\t\t<button\n\t\t\t\t\t#optionButton\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"hub-segmented__option\"\n\t\t\t\t\t[class.hub-segmented__option--selected]=\"isSelected(option)\"\n\t\t\t\t\t[attr.role]=\"multiple() ? null : 'radio'\"\n\t\t\t\t\t[attr.aria-checked]=\"multiple() ? null : isSelected(option)\"\n\t\t\t\t\t[attr.aria-pressed]=\"multiple() ? isSelected(option) : null\"\n\t\t\t\t\t[disabled]=\"disabled() || !!option.disabled\"\n\t\t\t\t\t[tabindex]=\"rovingIndex() === index ? 0 : -1\"\n\t\t\t\t\t(click)=\"select(option)\"\n\t\t\t\t>\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</button>\n\t\t\t}\n\t\t</div>\n\t</div>\n\n\t@if (formText() || formTextTmp()) {\n\t\t<div class=\"hub-field__form-text\" [class.hub-field__form-text--disabled]=\"disabled()\">\n\t\t\t@if (formTextTmp()) {\n\t\t\t\t<ng-container [ngTemplateOutlet]=\"formTextTmp()!\"></ng-container>\n\t\t\t} @else {\n\t\t\t\t{{ formText() }}\n\t\t\t}\n\t\t</div>\n\t}\n\n\t@if (isInvalid) {\n\t\t<div class=\"hub-field__feedback\" role=\"alert\">\n\t\t\t<ul>\n\t\t\t\t@for (error of errors | keyvalue; track error.key) {\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"_errorTemplates[error.key] ?? defaultErrorTpt\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: error.value, value: error.value }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t<ng-template #defaultErrorTpt>\n\t\t\t\t\t\t\t<span class=\"hub-field__feedback-text\" [innerHTML]=\"getInvalidFeedbackTemplate(error.key, error.value)\"></span>\n\t\t\t\t\t\t</ng-template>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\t\t</div>\n\t}\n\n\t@if (showsValid && validFeedback()) {\n\t\t<div class=\"hub-field__feedback hub-field__feedback--valid\" role=\"status\">\n\t\t\t<span class=\"hub-field__feedback-text\">{{ validFeedback() }}</span>\n\t\t</div>\n\t}\n</div>\n", styles: ["hub-segmented{display:block}hub-segmented.hidden-control{display:none!important}.hub-segmented{position:relative;display:inline-flex;gap:var(--hub-segmented-gap);padding:var(--hub-segmented-gap);background:var(--hub-segmented-bg);border-radius:var(--hub-segmented-radius)}.hub-segmented__indicator{position:absolute;z-index:0;left:var(--hub-segmented-indicator-x, 0);top:var(--hub-segmented-indicator-y, 0);width:var(--hub-segmented-indicator-width, 0);height:var(--hub-segmented-indicator-height, 0);background:var(--hub-segmented-selected-bg);border-radius:calc(var(--hub-segmented-radius) - var(--hub-segmented-gap));box-shadow:var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075));pointer-events:none;transition:left var(--hub-segmented-indicator-transition),top var(--hub-segmented-indicator-transition),width var(--hub-segmented-indicator-transition),height var(--hub-segmented-indicator-transition)}@media(prefers-reduced-motion:reduce){.hub-segmented .hub-segmented__indicator{transition:none}}.hub-segmented--disabled{opacity:var(--hub-form-disabled-opacity);cursor:not-allowed}.hub-segmented--vertical{flex-direction:column}.hub-segmented--vertical .hub-segmented__option{width:100%}.hub-segmented--multiple .hub-segmented__option--selected{background:var(--hub-segmented-selected-bg);box-shadow:var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075))}.hub-segmented__option{appearance:none;border:0;margin:0;position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;padding:var(--hub-segmented-padding-y) var(--hub-segmented-padding-x);color:var(--hub-sys-text-muted, #6c757d);background:transparent;font:inherit;line-height:1.2;white-space:nowrap;border-radius:calc(var(--hub-segmented-radius) - var(--hub-segmented-gap));cursor:pointer;transition:var(--hub-form-transition),color .15s ease-in-out,background-color .15s ease-in-out}.hub-segmented__option:focus-visible{outline:0;box-shadow:0 0 0 var(--hub-form-focus-ring-width) var(--hub-form-focus-ring-color)}.hub-segmented__option--selected{color:var(--hub-segmented-selected-color)}.hub-segmented__option:disabled{cursor:not-allowed;opacity:var(--hub-form-disabled-opacity)}.hub-segmented--sm .hub-segmented__option{padding:calc(var(--hub-segmented-padding-y) * .5) calc(var(--hub-segmented-padding-x) * .65);font-size:var(--hub-ref-font-size-sm, .875rem)}.hub-segmented--lg .hub-segmented__option{padding:calc(var(--hub-segmented-padding-y) * 1.35) calc(var(--hub-segmented-padding-x) * 1.35);font-size:var(--hub-ref-font-size-lg, 1.125rem)}:where(.hub-segmented[data-variant=primary]){--hub-segmented-selected-bg: var(--hub-sys-color-primary);--hub-segmented-selected-color: var(--hub-sys-color-primary-on, #fff)}:where(.hub-segmented[data-variant=secondary]){--hub-segmented-selected-bg: var(--hub-sys-color-secondary);--hub-segmented-selected-color: var(--hub-sys-color-secondary-on, #fff)}:where(.hub-segmented[data-variant=success]){--hub-segmented-selected-bg: var(--hub-sys-color-success);--hub-segmented-selected-color: var(--hub-sys-color-success-on, #fff)}:where(.hub-segmented[data-variant=danger]){--hub-segmented-selected-bg: var(--hub-sys-color-danger);--hub-segmented-selected-color: var(--hub-sys-color-danger-on, #fff)}:where(.hub-segmented[data-variant=warning]){--hub-segmented-selected-bg: var(--hub-sys-color-warning);--hub-segmented-selected-color: var(--hub-sys-color-warning-on, #fff)}:where(.hub-segmented[data-variant=info]){--hub-segmented-selected-bg: var(--hub-sys-color-info);--hub-segmented-selected-color: var(--hub-sys-color-info-on, #fff)}:where(.hub-segmented[data-variant=neutral]){--hub-segmented-selected-bg: var(--hub-sys-color-neutral);--hub-segmented-selected-color: var(--hub-sys-color-neutral-on, #fff)}\n"] }]
2240
+ }, template: "<div\n\tclass=\"hub-field hub-segmented-field\"\n\t[class.hub-field--horizontal]=\"label() && labelType() === _labelTypes.Horizontal\"\n\t[class.hub-field--disabled]=\"disabled()\"\n\t[class.hub-field--invalid]=\"isInvalid\"\n\t[class.hub-field--valid]=\"showsValid\"\n>\n\t@if (label() || required()) {\n\t\t<label [for]=\"id\" class=\"hub-field__label\">\n\t\t\t{{ label() }}\n\t\t\t@if (required()) {\n\t\t\t\t<span class=\"hub-field__required\" aria-hidden=\"true\">*</span>\n\t\t\t}\n\t\t</label>\n\t}\n\n\t<div class=\"hub-field__body\">\n\t\t<div\n\t\t\t#segmentedBar\n\t\t\t[id]=\"id\"\n\t\t\tclass=\"hub-segmented\"\n\t\t\t[class.hub-segmented--sm]=\"size() === 'sm'\"\n\t\t\t[class.hub-segmented--lg]=\"size() === 'lg'\"\n\t\t\t[class.hub-segmented--vertical]=\"vertical()\"\n\t\t\t[class.hub-segmented--multiple]=\"multiple()\"\n\t\t\t[class.hub-segmented--disabled]=\"disabled()\"\n\t\t\t[attr.data-variant]=\"color() || null\"\n\t\t\t[style.--hub-segmented-accent]=\"accentVar()\"\n\t\t\t[attr.role]=\"multiple() ? 'group' : 'radiogroup'\"\n\t\t\t[attr.aria-disabled]=\"disabled() ? 'true' : null\"\n\t\t\t(keydown)=\"onKeydown($event)\"\n\t\t>\n\t\t\t@if (!multiple()) {\n\t\t\t\t<span class=\"hub-segmented__indicator\" aria-hidden=\"true\"></span>\n\t\t\t}\n\n\t\t\t@for (option of options(); track option.value; let index = $index) {\n\t\t\t\t<button\n\t\t\t\t\t#optionButton\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"hub-segmented__option\"\n\t\t\t\t\t[class.hub-segmented__option--selected]=\"isSelected(option)\"\n\t\t\t\t\t[attr.role]=\"multiple() ? null : 'radio'\"\n\t\t\t\t\t[attr.aria-checked]=\"multiple() ? null : isSelected(option)\"\n\t\t\t\t\t[attr.aria-pressed]=\"multiple() ? isSelected(option) : null\"\n\t\t\t\t\t[disabled]=\"disabled() || !!option.disabled\"\n\t\t\t\t\t[tabindex]=\"rovingIndex() === index ? 0 : -1\"\n\t\t\t\t\t(click)=\"select(option)\"\n\t\t\t\t>\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</button>\n\t\t\t}\n\t\t</div>\n\t</div>\n\n\t@if (formText() || formTextTmp()) {\n\t\t<div class=\"hub-field__form-text\" [class.hub-field__form-text--disabled]=\"disabled()\">\n\t\t\t@if (formTextTmp()) {\n\t\t\t\t<ng-container [ngTemplateOutlet]=\"formTextTmp()!\"></ng-container>\n\t\t\t} @else {\n\t\t\t\t{{ formText() }}\n\t\t\t}\n\t\t</div>\n\t}\n\n\t@if (isInvalid) {\n\t\t<div class=\"hub-field__feedback\" role=\"alert\">\n\t\t\t<ul>\n\t\t\t\t@for (error of errors | keyvalue; track error.key) {\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"_errorTemplates[error.key] ?? defaultErrorTpt\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: error.value, value: error.value }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t<ng-template #defaultErrorTpt>\n\t\t\t\t\t\t\t<span class=\"hub-field__feedback-text\" [innerHTML]=\"getInvalidFeedbackTemplate(error.key, error.value)\"></span>\n\t\t\t\t\t\t</ng-template>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\t\t</div>\n\t}\n\n\t@if (showsValid && validFeedback()) {\n\t\t<div class=\"hub-field__feedback hub-field__feedback--valid\" role=\"status\">\n\t\t\t<span class=\"hub-field__feedback-text\">{{ validFeedback() }}</span>\n\t\t</div>\n\t}\n</div>\n", styles: ["hub-segmented{display:block}hub-segmented.hidden-control{display:none!important}.hub-segmented{position:relative;display:inline-flex;gap:var(--hub-segmented-gap);padding:var(--hub-segmented-gap);background:var(--hub-segmented-bg);border-radius:var(--hub-segmented-radius)}.hub-segmented__indicator{position:absolute;z-index:0;left:var(--hub-segmented-indicator-x, 0);top:var(--hub-segmented-indicator-y, 0);width:var(--hub-segmented-indicator-width, 0);height:var(--hub-segmented-indicator-height, 0);background:var(--hub-segmented-selected-bg);border-radius:calc(var(--hub-segmented-radius) - var(--hub-segmented-gap));box-shadow:var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075));pointer-events:none;transition:left var(--hub-segmented-indicator-transition),top var(--hub-segmented-indicator-transition),width var(--hub-segmented-indicator-transition),height var(--hub-segmented-indicator-transition)}@media(prefers-reduced-motion:reduce){.hub-segmented .hub-segmented__indicator{transition:none}}.hub-segmented--disabled{opacity:var(--hub-form-disabled-opacity);cursor:not-allowed}.hub-segmented--vertical{flex-direction:column}.hub-segmented--vertical .hub-segmented__option{width:100%}.hub-segmented--multiple .hub-segmented__option--selected{background:var(--hub-segmented-selected-bg);box-shadow:var(--hub-sys-shadow-sm, 0 .125rem .25rem rgba(0, 0, 0, .075))}.hub-segmented__option{appearance:none;border:0;margin:0;position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;padding:var(--hub-segmented-padding-y) var(--hub-segmented-padding-x);color:var(--hub-sys-text-muted, #6c757d);background:transparent;font:inherit;line-height:1.2;white-space:nowrap;border-radius:calc(var(--hub-segmented-radius) - var(--hub-segmented-gap));cursor:pointer;transition:var(--hub-form-transition),color .15s ease-in-out,background-color .15s ease-in-out}.hub-segmented__option:focus-visible{outline:0;box-shadow:0 0 0 var(--hub-form-focus-ring-width) var(--hub-form-focus-ring-color)}.hub-segmented__option--selected{color:var(--hub-segmented-selected-color)}.hub-segmented__option:disabled{cursor:not-allowed;opacity:var(--hub-form-disabled-opacity)}.hub-segmented--sm .hub-segmented__option{padding:calc(var(--hub-segmented-padding-y) * .5) calc(var(--hub-segmented-padding-x) * .65);font-size:var(--hub-ref-font-size-sm, .875rem)}.hub-segmented--lg .hub-segmented__option{padding:calc(var(--hub-segmented-padding-y) * 1.35) calc(var(--hub-segmented-padding-x) * 1.35);font-size:var(--hub-ref-font-size-lg, 1.125rem)}:where(.hub-segmented[data-variant]){--hub-segmented-selected-bg: var(--hub-segmented-accent);--hub-segmented-selected-color: oklch(from var(--hub-segmented-accent) clamp(0, (.62 - l) * 1000, 1) 0 h)}\n"] }]
2061
2241
  }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], labelType: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelType", required: false }] }], formText: [{ type: i0.Input, args: [{ isSignal: true, alias: "formText", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], vertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "vertical", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], _buttons: [{ type: i0.ViewChildren, args: ['optionButton', { isSignal: true }] }], _bar: [{ type: i0.ViewChild, args: ['segmentedBar', { isSignal: true }] }] } });
2062
2242
 
2063
2243
  /**
@@ -2158,12 +2338,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
2158
2338
  type: Component,
2159
2339
  args: [{ selector: 'form[hubForm]', imports: [NgTemplateOutlet, KeyValuePipe], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
2160
2340
  '[class.hub-form-host]': 'true',
2161
- '[class.hub-form--invalid]': 'isInvalid'
2341
+ '[class.hub-form--invalid]': 'isInvalid',
2342
+ '(submit)': 'handleSubmit($event)'
2162
2343
  }, template: "<ng-content></ng-content>\n\n@if (isInvalid) {\n\t<div class=\"hub-field__feedback\" role=\"alert\">\n\t\t<ul>\n\t\t\t@for (error of errors | keyvalue; track error.key) {\n\t\t\t\t<li>\n\t\t\t\t\t<ng-container\n\t\t\t\t\t\t[ngTemplateOutlet]=\"_errorTemplates[error.key] ?? defaultErrorTpt\"\n\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: error.value, value: error.value }\"\n\t\t\t\t\t></ng-container>\n\t\t\t\t\t<ng-template #defaultErrorTpt>\n\t\t\t\t\t\t<span class=\"hub-field__feedback-text\" [innerHTML]=\"getInvalidFeedbackTemplate(error.key, error.value)\"></span>\n\t\t\t\t\t</ng-template>\n\t\t\t\t</li>\n\t\t\t}\n\t\t</ul>\n\t</div>\n}\n" }]
2163
- }], propDecorators: { handleSubmit: [{
2164
- type: HostListener,
2165
- args: ['submit', ['$event']]
2166
- }] } });
2344
+ }] });
2167
2345
 
2168
2346
  /**
2169
2347
  * Presentational legend for a `<hub-fieldset>` (or any grouped section), with optional
@@ -6093,6 +6271,780 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
6093
6271
  }, template: "<div\n\tclass=\"hub-field hub-datepicker\"\n\t[class.hub-field--horizontal]=\"labelType() === _labelTypes.Horizontal || (!label() && required())\"\n\t[class.hub-field--disabled]=\"disabled()\"\n\t[class.hub-field--invalid]=\"isInvalid\"\n\t[class.hub-field--valid]=\"showsValid\"\n>\n\t@if (label() || required()) {\n\t\t<label [for]=\"id\" class=\"hub-field__label\">\n\t\t\t{{ label() }}\n\t\t\t@if (required()) {\n\t\t\t\t<span class=\"hub-field__required\" aria-hidden=\"true\">*</span>\n\t\t\t}\n\t\t</label>\n\t}\n\n\t<div class=\"hub-field__body hub-datepicker__trigger\" cdkOverlayOrigin #trigger=\"cdkOverlayOrigin\">\n\t\t<input\n\t\t\tclass=\"hub-field__control hub-datepicker__input\"\n\t\t\t[class.hub-field__control--invalid]=\"isInvalid\"\n\t\t\t[class.hub-field__control--valid]=\"showsValid\"\n\t\t\t[class.hub-datepicker__input--clearable]=\"clearable() && displayValue()\"\n\t\t\t[id]=\"id\"\n\t\t\ttype=\"text\"\n\t\t\treadonly\n\t\t\t[value]=\"displayValue()\"\n\t\t\t[placeholder]=\"placeholder()\"\n\t\t\t[disabled]=\"disabled()\"\n\t\t\t[required]=\"!!required()\"\n\t\t\t[attr.aria-invalid]=\"isInvalid\"\n\t\t\t[attr.aria-expanded]=\"_open()\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\t(click)=\"toggleCalendar()\"\n\t\t\t(blur)=\"handleBlur($event)\"\n\t\t/>\n\t\t@if (clearable() && displayValue() && !disabled() && !readonly()) {\n\t\t\t<button type=\"button\" class=\"hub-datepicker__clear\" [attr.aria-label]=\"_labels().clear\" (click)=\"clear()\">&times;</button>\n\t\t}\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\tclass=\"hub-datepicker__icon\"\n\t\t\ttabindex=\"-1\"\n\t\t\t[attr.aria-label]=\"_labels().openCalendar\"\n\t\t\t[disabled]=\"disabled() || readonly()\"\n\t\t\t(click)=\"toggleCalendar()\"\n\t\t></button>\n\t</div>\n\n\t@if (formText() || formTextTmp()) {\n\t\t<div class=\"hub-field__form-text\" [class.hub-field__form-text--disabled]=\"disabled()\">\n\t\t\t@if (formTextTmp()) {\n\t\t\t\t<ng-container [ngTemplateOutlet]=\"formTextTmp()!\"></ng-container>\n\t\t\t} @else {\n\t\t\t\t{{ formText() }}\n\t\t\t}\n\t\t</div>\n\t}\n\n\t@if (isInvalid) {\n\t\t<div class=\"hub-field__feedback\" role=\"alert\">\n\t\t\t<ul>\n\t\t\t\t@for (error of errors | keyvalue; track error.key) {\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"_errorTemplates[error.key] ?? defaultErrorTpt\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: error.value, value: error.value }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t<ng-template #defaultErrorTpt>\n\t\t\t\t\t\t\t<span class=\"hub-field__feedback-text\" [innerHTML]=\"getInvalidFeedbackTemplate(error.key, error.value)\"></span>\n\t\t\t\t\t\t</ng-template>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\t\t</div>\n\t}\n\n\t@if (showsValid && validFeedback()) {\n\t\t<div class=\"hub-field__feedback hub-field__feedback--valid\" role=\"status\">\n\t\t\t<span class=\"hub-field__feedback-text\">{{ validFeedback() }}</span>\n\t\t</div>\n\t}\n</div>\n\n<ng-template\n\tcdkConnectedOverlay\n\t[cdkConnectedOverlayOrigin]=\"trigger\"\n\t[cdkConnectedOverlayOpen]=\"_open()\"\n\t[cdkConnectedOverlayHasBackdrop]=\"true\"\n\tcdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n\t[cdkConnectedOverlayOffsetY]=\"4\"\n\t(backdropClick)=\"close()\"\n\t(detach)=\"close()\"\n>\n\t<div class=\"hub-datepicker__panel\" role=\"dialog\" [attr.aria-label]=\"monthYearLabel()\">\n\t\t<header class=\"hub-datepicker__header\">\n\t\t\t<div class=\"hub-datepicker__nav-group\">\n\t\t\t\t<button type=\"button\" class=\"hub-datepicker__nav\" [attr.aria-label]=\"_labels().previousYear\" (click)=\"shiftMonth(-12)\">\u00AB</button>\n\t\t\t\t<button type=\"button\" class=\"hub-datepicker__nav\" [attr.aria-label]=\"_labels().previousMonth\" (click)=\"shiftMonth(-1)\">\u2039</button>\n\t\t\t</div>\n\t\t\t<span class=\"hub-datepicker__title\" aria-live=\"polite\">{{ monthYearLabel() }}</span>\n\t\t\t<div class=\"hub-datepicker__nav-group\">\n\t\t\t\t<button type=\"button\" class=\"hub-datepicker__nav\" [attr.aria-label]=\"_labels().nextMonth\" (click)=\"shiftMonth(1)\">\u203A</button>\n\t\t\t\t<button type=\"button\" class=\"hub-datepicker__nav\" [attr.aria-label]=\"_labels().nextYear\" (click)=\"shiftMonth(12)\">\u00BB</button>\n\t\t\t</div>\n\t\t</header>\n\n\t\t<div class=\"hub-datepicker__weekdays\" aria-hidden=\"true\">\n\t\t\t@for (wd of weekdays(); track $index) {\n\t\t\t\t<span class=\"hub-datepicker__weekday\">{{ wd }}</span>\n\t\t\t}\n\t\t</div>\n\n\t\t<div class=\"hub-datepicker__grid\" role=\"grid\" (keydown)=\"onGridKeydown($event)\">\n\t\t\t@for (cell of grid(); track cell.date.getTime()) {\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\trole=\"gridcell\"\n\t\t\t\t\tclass=\"hub-datepicker__cell\"\n\t\t\t\t\t[class.hub-datepicker__cell--outside]=\"!cell.currentMonth\"\n\t\t\t\t\t[class.hub-datepicker__cell--today]=\"cell.today\"\n\t\t\t\t\t[class.hub-datepicker__cell--selected]=\"cell.selected || cell.rangeStart || cell.rangeEnd\"\n\t\t\t\t\t[class.hub-datepicker__cell--range-start]=\"cell.rangeStart\"\n\t\t\t\t\t[class.hub-datepicker__cell--range-end]=\"cell.rangeEnd\"\n\t\t\t\t\t[class.hub-datepicker__cell--in-range]=\"cell.inRange\"\n\t\t\t\t\t[attr.data-time]=\"cell.date.getTime()\"\n\t\t\t\t\t[attr.tabindex]=\"cell.focused ? 0 : -1\"\n\t\t\t\t\t[attr.aria-selected]=\"cell.selected || cell.rangeStart || cell.rangeEnd\"\n\t\t\t\t\t[disabled]=\"cell.disabled\"\n\t\t\t\t\t(click)=\"selectDay(cell)\"\n\t\t\t\t>\n\t\t\t\t\t{{ cell.day }}\n\t\t\t\t</button>\n\t\t\t}\n\t\t</div>\n\n\t\t@if (showToday() || clearable()) {\n\t\t\t<footer class=\"hub-datepicker__footer\">\n\t\t\t\t@if (showToday()) {\n\t\t\t\t\t<button type=\"button\" class=\"hub-datepicker__action\" (click)=\"goToToday()\">{{ _labels().today }}</button>\n\t\t\t\t}\n\t\t\t\t@if (clearable()) {\n\t\t\t\t\t<button type=\"button\" class=\"hub-datepicker__action\" (click)=\"clear()\">{{ _labels().clear }}</button>\n\t\t\t\t}\n\t\t\t</footer>\n\t\t}\n\t</div>\n</ng-template>\n", styles: ["hub-datepicker{display:block}hub-datepicker.hidden-control{display:none!important}.hub-datepicker__trigger{position:relative}.hub-datepicker__input{padding-right:var(--hub-datepicker-icon-width);cursor:pointer}.hub-datepicker__input--clearable{padding-right:calc(var(--hub-datepicker-icon-width) + 1.5rem)}.hub-datepicker__clear{position:absolute;top:0;right:var(--hub-datepicker-icon-width);height:100%;width:1.5rem;padding:0;color:var(--hub-daterangepicker-nav-arrow-color);font-size:1.1rem;line-height:1;background:transparent;border:0;cursor:pointer}.hub-datepicker__clear:hover{color:var(--hub-form-invalid-color)}.hub-datepicker__icon{position:absolute;top:0;right:0;width:var(--hub-datepicker-icon-width);height:100%;padding:0;background:transparent;border:0;cursor:pointer}.hub-datepicker__icon:after{content:\"\";display:block;width:var(--hub-datepicker-icon-size);height:var(--hub-datepicker-icon-size);margin:0 auto;background:var(--hub-datepicker-icon) center/contain no-repeat}.hub-datepicker__icon:disabled{cursor:not-allowed;opacity:var(--hub-form-disabled-opacity)}.hub-datepicker__panel{width:max-content;padding:var(--hub-daterangepicker-padding-y, var(--hub-ref-space-3, 1rem)) var(--hub-daterangepicker-padding-x, var(--hub-ref-space-3, 1rem));color:var(--hub-daterangepicker-color);background:var(--hub-daterangepicker-bg);border:var(--hub-input-border-width) solid var(--hub-daterangepicker-border-color);border-radius:var(--hub-daterangepicker-border-radius);box-shadow:var(--hub-daterangepicker-box-shadow);font-family:var(--hub-input-font-family)}.hub-datepicker__panel .hub-datepicker__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--hub-ref-space-2, .5rem)}.hub-datepicker__panel .hub-datepicker__nav-group{display:inline-flex;gap:.125rem}.hub-datepicker__panel .hub-datepicker__nav{display:inline-flex;align-items:center;justify-content:center;width:var(--hub-daterangepicker-cell-size);height:var(--hub-daterangepicker-cell-size);font-size:1.25rem;line-height:1;color:var(--hub-daterangepicker-nav-arrow-color);background:transparent;border:0;border-radius:var(--hub-daterangepicker-cell-border-radius);cursor:pointer}.hub-datepicker__panel .hub-datepicker__nav:hover{color:var(--hub-daterangepicker-nav-arrow-hover-color);background:var(--hub-daterangepicker-cell-hover-bg)}.hub-datepicker__panel .hub-datepicker__title{font-weight:var(--hub-ref-font-weight-medium, 500);text-transform:capitalize}.hub-datepicker__panel .hub-datepicker__weekdays,.hub-datepicker__panel .hub-datepicker__grid{display:grid;grid-template-columns:repeat(7,var(--hub-daterangepicker-cell-size));gap:.125rem}.hub-datepicker__panel .hub-datepicker__weekday{display:inline-flex;align-items:center;justify-content:center;height:1.5rem;font-size:var(--hub-ref-font-size-sm, .875rem);color:var(--hub-daterangepicker-off-color);text-transform:capitalize}.hub-datepicker__panel .hub-datepicker__cell{display:inline-flex;align-items:center;justify-content:center;width:var(--hub-daterangepicker-cell-size);height:var(--hub-daterangepicker-cell-size);color:var(--hub-daterangepicker-cell-color);background:transparent;border:0;border-radius:var(--hub-daterangepicker-cell-border-radius);font-size:var(--hub-input-font-size);cursor:pointer}.hub-datepicker__panel .hub-datepicker__cell:hover{background:var(--hub-daterangepicker-cell-hover-bg)}.hub-datepicker__panel .hub-datepicker__cell--outside{color:var(--hub-daterangepicker-off-color)}.hub-datepicker__panel .hub-datepicker__cell--today{font-weight:var(--hub-ref-font-weight-bold, 700);box-shadow:inset 0 0 0 1px var(--hub-daterangepicker-border-color)}.hub-datepicker__panel .hub-datepicker__cell--in-range{background:var(--hub-daterangepicker-in-range-bg);border-radius:0}.hub-datepicker__panel .hub-datepicker__cell--range-start{border-top-right-radius:0;border-bottom-right-radius:0}.hub-datepicker__panel .hub-datepicker__cell--range-end{border-top-left-radius:0;border-bottom-left-radius:0}.hub-datepicker__panel .hub-datepicker__cell--selected,.hub-datepicker__panel .hub-datepicker__cell--selected:hover{color:var(--hub-daterangepicker-active-color);background:var(--hub-daterangepicker-active-bg)}.hub-datepicker__panel .hub-datepicker__cell:disabled{color:var(--hub-daterangepicker-off-color);background:transparent;cursor:not-allowed;opacity:.6}.hub-datepicker__panel .hub-datepicker__cell:focus-visible{outline:0;box-shadow:var(--hub-input-focus-box-shadow)}.hub-datepicker__panel .hub-datepicker__footer{display:flex;justify-content:flex-end;gap:var(--hub-ref-space-2, .5rem);margin-top:var(--hub-ref-space-2, .5rem);padding-top:var(--hub-ref-space-2, .5rem);border-top:var(--hub-input-border-width) solid var(--hub-daterangepicker-border-color)}.hub-datepicker__panel .hub-datepicker__action{padding:.2rem .5rem;color:var(--hub-sys-color-primary, #0d6efd);font-size:var(--hub-ref-font-size-sm, .875rem);background:transparent;border:0;border-radius:var(--hub-daterangepicker-cell-border-radius);cursor:pointer}.hub-datepicker__panel .hub-datepicker__action:hover{background:var(--hub-daterangepicker-cell-hover-bg)}\n"] }]
6094
6272
  }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], labelType: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelType", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], weekdayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekdayFormat", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], rangeSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeSeparator", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], showToday: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToday", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], formText: [{ type: i0.Input, args: [{ isSignal: true, alias: "formText", required: false }] }], formTextType: [{ type: i0.Input, args: [{ isSignal: true, alias: "formTextType", required: false }] }], classlist: [{ type: i0.Input, args: [{ isSignal: true, alias: "classlist", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], viewChange: [{ type: i0.Output, args: ["viewChange"] }], cleared: [{ type: i0.Output, args: ["cleared"] }] } });
6095
6273
 
6274
+ /**
6275
+ * Overrides the glyph `<hub-file-input>` renders next to a non-image file.
6276
+ *
6277
+ * The library never depends on `ng-hub-ui-icons`: it draws a neutral document glyph from the
6278
+ * `--hub-file-input-file-icon` CSS token and lets you project anything richer — a `<hub-icon>` from
6279
+ * any pack, an inline `<svg>`, an emoji — through this template. The item is the implicit context,
6280
+ * so the glyph can vary with `file.type`.
6281
+ *
6282
+ * @example
6283
+ * ```html
6284
+ * <hub-file-input formControlName="attachments" [multiple]="true">
6285
+ * <ng-template hubFileIcon let-item>
6286
+ * <hub-icon [name]="item.file.type === 'application/pdf' ? 'fa:solid:file-pdf' : 'fa:solid:file'" />
6287
+ * </ng-template>
6288
+ * </hub-file-input>
6289
+ * ```
6290
+ */
6291
+ class HubFileIconDirective {
6292
+ /** Template reference for the icon. */
6293
+ template = inject(TemplateRef);
6294
+ /**
6295
+ * Narrows the implicit context for the template type-checker.
6296
+ *
6297
+ * @param _directive - The directive instance.
6298
+ * @param _context - The candidate context.
6299
+ * @returns Always `true`; exists only to type `let-item`.
6300
+ */
6301
+ static ngTemplateContextGuard(_directive, _context) {
6302
+ return true;
6303
+ }
6304
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubFileIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
6305
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: HubFileIconDirective, isStandalone: true, selector: "[hubFileIcon]", ngImport: i0 });
6306
+ }
6307
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubFileIconDirective, decorators: [{
6308
+ type: Directive,
6309
+ args: [{
6310
+ selector: '[hubFileIcon]'
6311
+ }]
6312
+ }] });
6313
+
6314
+ /**
6315
+ * Replaces the entire rendering of a selected file in `<hub-file-input>`.
6316
+ *
6317
+ * Where `hubFileIcon` swaps a glyph, this takes over the whole row (or grid tile): thumbnail, name,
6318
+ * size, progress bar and buttons. The template receives the item plus the actions it needs, so a
6319
+ * custom preview keeps working removal, retry and cancel without reaching into the component.
6320
+ *
6321
+ * @example
6322
+ * ```html
6323
+ * <hub-file-input formControlName="attachments" [multiple]="true">
6324
+ * <ng-template hubFilePreview let-item let-remove="remove">
6325
+ * <article class="attachment">
6326
+ * <img *ngIf="item.previewUrl" [src]="item.previewUrl" alt="" />
6327
+ * <span>{{ item.file.name }}</span>
6328
+ * <button type="button" (click)="remove()">Remove</button>
6329
+ * </article>
6330
+ * </ng-template>
6331
+ * </hub-file-input>
6332
+ * ```
6333
+ */
6334
+ class HubFilePreviewDirective {
6335
+ /** Template reference for the item. */
6336
+ template = inject(TemplateRef);
6337
+ /**
6338
+ * Narrows the context for the template type-checker.
6339
+ *
6340
+ * @param _directive - The directive instance.
6341
+ * @param _context - The candidate context.
6342
+ * @returns Always `true`; exists only to type `let-item` and the action bindings.
6343
+ */
6344
+ static ngTemplateContextGuard(_directive, _context) {
6345
+ return true;
6346
+ }
6347
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubFilePreviewDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
6348
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: HubFilePreviewDirective, isStandalone: true, selector: "[hubFilePreview]", ngImport: i0 });
6349
+ }
6350
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubFilePreviewDirective, decorators: [{
6351
+ type: Directive,
6352
+ args: [{
6353
+ selector: '[hubFilePreview]'
6354
+ }]
6355
+ }] });
6356
+
6357
+ /**
6358
+ * Tests a file against an `accept` specification.
6359
+ *
6360
+ * The native `accept` attribute only filters the operating-system file dialog: files arriving by
6361
+ * drag-and-drop or paste bypass it entirely. This function applies the same rules by hand, so a
6362
+ * dropped file is held to the constraint the consumer declared.
6363
+ *
6364
+ * Supported tokens, comma-separated, matched case-insensitively:
6365
+ * - an extension — `.pdf`
6366
+ * - a wildcard MIME type — `image/*`
6367
+ * - an exact MIME type — `application/pdf`
6368
+ * - the catch-alls `*` and `*​/*`
6369
+ *
6370
+ * Note that a MIME token can never match a file the browser could not type (`file.type === ''`,
6371
+ * common for unusual extensions); use an extension token for those.
6372
+ *
6373
+ * @param file - The file to test.
6374
+ * @param accept - The `accept` specification. Empty or `*` accepts everything.
6375
+ * @returns `true` when the file satisfies at least one token.
6376
+ */
6377
+ function matchesAccept(file, accept) {
6378
+ const specification = (accept ?? '').trim();
6379
+ if (!specification || specification === '*' || specification === '*/*') {
6380
+ return true;
6381
+ }
6382
+ const name = file.name.toLowerCase();
6383
+ const type = (file.type ?? '').toLowerCase();
6384
+ return specification
6385
+ .split(',')
6386
+ .map((token) => token.trim().toLowerCase())
6387
+ .filter((token) => token.length > 0)
6388
+ .some((token) => {
6389
+ if (token === '*' || token === '*/*') {
6390
+ return true;
6391
+ }
6392
+ // An extension, e.g. `.pdf`
6393
+ if (token.startsWith('.')) {
6394
+ return name.endsWith(token);
6395
+ }
6396
+ // A wildcard MIME type, e.g. `image/*` — the file's type must start with `image/`
6397
+ if (token.endsWith('/*')) {
6398
+ return type.length > 0 && type.startsWith(token.slice(0, -1));
6399
+ }
6400
+ // An exact MIME type
6401
+ return type === token;
6402
+ });
6403
+ }
6404
+
6405
+ /**
6406
+ * Builds the identity key used to detect duplicate selections.
6407
+ *
6408
+ * Two `File` objects are never referentially equal across separate drops, so identity is derived
6409
+ * from the tuple the browser exposes: name, byte size and last-modified timestamp. Collisions are
6410
+ * possible in theory (same name, size and mtime) but describe the same file in every practical case.
6411
+ *
6412
+ * @param file - The file to key.
6413
+ * @returns A stable string key for the file.
6414
+ */
6415
+ function fileKey(file) {
6416
+ return `${file.name}:${file.size}:${file.lastModified}`;
6417
+ }
6418
+
6419
+ /**
6420
+ * Normalizes any file-shaped control value to a plain array.
6421
+ *
6422
+ * Accepts the shapes a control can realistically hold: a single `File` (single mode), an array of
6423
+ * files (multiple mode), a native `FileList` (a control fed straight from a DOM event or from the
6424
+ * deprecated `<hub-input type="file">`), or `null`/`undefined`.
6425
+ *
6426
+ * @param value - The raw control value.
6427
+ * @returns The files it contains, in order; an empty array when it holds none.
6428
+ */
6429
+ function toFileArray(value) {
6430
+ if (value == null) {
6431
+ return [];
6432
+ }
6433
+ if (value instanceof File) {
6434
+ return [value];
6435
+ }
6436
+ if (Array.isArray(value)) {
6437
+ return value.filter((entry) => entry instanceof File);
6438
+ }
6439
+ if (typeof FileList !== 'undefined' && value instanceof FileList) {
6440
+ return Array.from(value);
6441
+ }
6442
+ return [];
6443
+ }
6444
+
6445
+ /**
6446
+ * Accessible file field with drag-and-drop, clipboard paste, per-file constraints, previews and
6447
+ * optional upload progress.
6448
+ *
6449
+ * A full `ng-hub-ui-forms` field: it extends {@link HubFieldControl}, so it binds with
6450
+ * `formControlName` / `ngModel` and shares the label, helper text and validation chrome with every
6451
+ * other control.
6452
+ *
6453
+ * The **form value stays native** — a `File` in single mode, a `File[]` in multiple mode, `null`
6454
+ * when empty — so it can be handed straight to a `FormData`. The rich per-file state (preview URL,
6455
+ * upload status, progress, error) lives in the {@link files} signal instead of contaminating the
6456
+ * control.
6457
+ *
6458
+ * Constraints declared as inputs (`accept`, `maxSize`, `minSize`, `maxTotalSize`, `maxFiles`) act as
6459
+ * a **filter**: an offending file never reaches the value and surfaces through {@link rejected}.
6460
+ * They are enforced by hand rather than delegated to the native `accept` attribute, which only
6461
+ * filters the operating-system dialog and is bypassed entirely by a drop or a paste. To make the
6462
+ * *control itself* invalid — the right choice when a value can also be patched in programmatically —
6463
+ * add the matching validators (`hubAcceptedFiles`, `hubMaxFileSize`, …).
6464
+ *
6465
+ * Uploading is opt-in and transport-agnostic: register a {@link HubFileUploader} with
6466
+ * `provideHubFileUploader()` and the component drives progress, cancel and retry. With no uploader
6467
+ * registered it is a pure picker.
6468
+ *
6469
+ * @example
6470
+ * ```html
6471
+ * <hub-file-input
6472
+ * formControlName="attachments"
6473
+ * label="Attachments"
6474
+ * [multiple]="true"
6475
+ * accept="image/*,.pdf"
6476
+ * [maxSize]="5 * 1024 * 1024"
6477
+ * [maxFiles]="3"
6478
+ * preview="grid"
6479
+ * (rejected)="notify($event)"
6480
+ * />
6481
+ * ```
6482
+ */
6483
+ class HubFileInputComponent extends HubFieldControl {
6484
+ #config = inject(HUB_FORMS_CONFIG);
6485
+ #uploader = inject(HUB_FILE_UPLOADER, { optional: true });
6486
+ #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
6487
+ #subscriptions = new Map();
6488
+ _formTextTypes = FormTextTypes;
6489
+ _items = signal([], /* @ts-ignore */
6490
+ ...(ngDevMode ? [{ debugName: "_items" }] : /* istanbul ignore next */ []));
6491
+ /** Depth counter for `dragenter`/`dragleave`: children of the dropzone fire their own events. */
6492
+ #dragDepth = 0;
6493
+ /** Releases every object URL and aborts every in-flight upload when the field goes away. */
6494
+ _cleanup = inject(DestroyRef).onDestroy(() => {
6495
+ this.#subscriptions.forEach((subscription) => subscription.unsubscribe());
6496
+ this.#subscriptions.clear();
6497
+ this._items().forEach((item) => this.#revokePreview(item));
6498
+ });
6499
+ /** Label text. */
6500
+ label = input('', /* @ts-ignore */
6501
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
6502
+ /** Whether more than one file can be held. Switches the control value to `File[]`. */
6503
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
6504
+ /** Accepted file types, e.g. `image/*,.pdf`. Enforced on drop and paste, not just in the dialog. */
6505
+ accept = input('*', /* @ts-ignore */
6506
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
6507
+ /** Maximum size per file, in bytes. */
6508
+ maxSize = input(null, /* @ts-ignore */
6509
+ ...(ngDevMode ? [{ debugName: "maxSize" }] : /* istanbul ignore next */ []));
6510
+ /** Minimum size per file, in bytes. Rejects the 0-byte files a failed export produces. */
6511
+ minSize = input(null, /* @ts-ignore */
6512
+ ...(ngDevMode ? [{ debugName: "minSize" }] : /* istanbul ignore next */ []));
6513
+ /** Maximum combined size of every held file, in bytes. */
6514
+ maxTotalSize = input(null, /* @ts-ignore */
6515
+ ...(ngDevMode ? [{ debugName: "maxTotalSize" }] : /* istanbul ignore next */ []));
6516
+ /** Maximum number of files. Only meaningful with `multiple`. */
6517
+ maxFiles = input(null, /* @ts-ignore */
6518
+ ...(ngDevMode ? [{ debugName: "maxFiles" }] : /* istanbul ignore next */ []));
6519
+ /** Whether files can be dropped onto the field. */
6520
+ dragDrop = input(true, { ...(ngDevMode ? { debugName: "dragDrop" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
6521
+ /** Whether files can be pasted into the focused field (e.g. a screenshot). */
6522
+ paste = input(true, { ...(ngDevMode ? { debugName: "paste" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
6523
+ /** How the held files are rendered: not at all, as a list, or as a thumbnail grid. */
6524
+ preview = input('list', /* @ts-ignore */
6525
+ ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
6526
+ /** Whether the same file can be selected twice (keyed on name, size and last-modified date). */
6527
+ allowDuplicates = input(false, { ...(ngDevMode ? { debugName: "allowDuplicates" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
6528
+ /** Opens the device camera instead of the file browser, on the platforms that support it. */
6529
+ capture = input(null, /* @ts-ignore */
6530
+ ...(ngDevMode ? [{ debugName: "capture" }] : /* istanbul ignore next */ []));
6531
+ /** Whether a newly accepted file starts uploading immediately. Ignored with no uploader registered. */
6532
+ autoUpload = input(true, { ...(ngDevMode ? { debugName: "autoUpload" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
6533
+ /** Overrides the "browse" button text coming from the global labels. */
6534
+ buttonLabel = input(null, /* @ts-ignore */
6535
+ ...(ngDevMode ? [{ debugName: "buttonLabel" }] : /* istanbul ignore next */ []));
6536
+ /** Overrides the auto-generated hint that summarizes the active constraints. Pass `''` to hide it. */
6537
+ hint = input(null, /* @ts-ignore */
6538
+ ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
6539
+ /** Helper text shown below the control. */
6540
+ formText = input('', /* @ts-ignore */
6541
+ ...(ngDevMode ? [{ debugName: "formText" }] : /* istanbul ignore next */ []));
6542
+ /** Helper text placement. Only `bottom` is supported. */
6543
+ formTextType = input(FormTextTypes.Bottom, /* @ts-ignore */
6544
+ ...(ngDevMode ? [{ debugName: "formTextType" }] : /* istanbul ignore next */ []));
6545
+ /** Extra CSS classes applied to the host element. */
6546
+ classlist = input('', /* @ts-ignore */
6547
+ ...(ngDevMode ? [{ debugName: "classlist" }] : /* istanbul ignore next */ []));
6548
+ /** Emits the native value whenever the selection changes. */
6549
+ valueChange = output();
6550
+ /** Emits the files refused by the declared constraints, with the reason for each. */
6551
+ rejected = output();
6552
+ /** Emits the file removed from the selection. */
6553
+ fileRemoved = output();
6554
+ /** Emits the full item list whenever an upload changes status or progress. */
6555
+ uploadStateChange = output();
6556
+ /** The hidden native file input, kept focusable so the dropzone label activates it. */
6557
+ nativeInput = viewChild('nativeInput', /* @ts-ignore */
6558
+ ...(ngDevMode ? [{ debugName: "nativeInput" }] : /* istanbul ignore next */ []));
6559
+ /** Projected per-file icon template. */
6560
+ iconTpt = contentChild(HubFileIconDirective, /* @ts-ignore */
6561
+ ...(ngDevMode ? [{ debugName: "iconTpt" }] : /* istanbul ignore next */ []));
6562
+ /** Projected per-file preview template, replacing the built-in item rendering. */
6563
+ previewTpt = contentChild(HubFilePreviewDirective, /* @ts-ignore */
6564
+ ...(ngDevMode ? [{ debugName: "previewTpt" }] : /* istanbul ignore next */ []));
6565
+ /** Whether a drag is currently hovering the dropzone. */
6566
+ isDragging = signal(false, /* @ts-ignore */
6567
+ ...(ngDevMode ? [{ debugName: "isDragging" }] : /* istanbul ignore next */ []));
6568
+ /** Latest message for the screen-reader live region. */
6569
+ announcement = signal('', /* @ts-ignore */
6570
+ ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
6571
+ /** The rich, per-file state of the current selection. */
6572
+ files = this._items.asReadonly();
6573
+ /** Whether at least one file is currently uploading. Gate a submit button on this. */
6574
+ uploading = computed(() => this._items().some((item) => item.status === 'uploading'), /* @ts-ignore */
6575
+ ...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
6576
+ /** Whether an uploader is registered, which is what enables progress, cancel and retry. */
6577
+ hasUploader = !!this.#uploader;
6578
+ /** The resolved, localizable labels. */
6579
+ labels = computed(() => this.#config.fileInput, /* @ts-ignore */
6580
+ ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
6581
+ /** Text of the browse action. */
6582
+ browseLabel = computed(() => this.buttonLabel() ?? this.labels().browse, /* @ts-ignore */
6583
+ ...(ngDevMode ? [{ debugName: "browseLabel" }] : /* istanbul ignore next */ []));
6584
+ /** The hint summarizing the active constraints. */
6585
+ constraintsHint = computed(() => {
6586
+ const override = this.hint();
6587
+ if (override != null) {
6588
+ return override;
6589
+ }
6590
+ const accept = this.accept();
6591
+ return this.labels().constraints({
6592
+ accept: !accept || accept === '*' || accept === '*/*' ? null : accept,
6593
+ maxSize: this.maxSize(),
6594
+ maxFiles: this.multiple() ? this.maxFiles() : null
6595
+ });
6596
+ }, /* @ts-ignore */
6597
+ ...(ngDevMode ? [{ debugName: "constraintsHint" }] : /* istanbul ignore next */ []));
6598
+ /** Whether the dropzone currently accepts a drop. */
6599
+ dropEnabled = computed(() => this.dragDrop() && !this.disabled(), /* @ts-ignore */
6600
+ ...(ngDevMode ? [{ debugName: "dropEnabled" }] : /* istanbul ignore next */ []));
6601
+ /**
6602
+ * Derives the native validation errors from the held files rather than from the DOM.
6603
+ *
6604
+ * The hidden native input is emptied after every `change` so the same file can be picked twice,
6605
+ * and it never sees a dropped or pasted file at all. Its `validity` therefore reports
6606
+ * `valueMissing` even when files are held — reading it would mark a satisfied field as required.
6607
+ *
6608
+ * @param _target - Ignored; the file state is the source of truth.
6609
+ */
6610
+ updateNativeErrors(_target) {
6611
+ if (this._control) {
6612
+ return;
6613
+ }
6614
+ this._nativeErrors.set(this.required() && this._items().length === 0 ? { required: true } : null);
6615
+ }
6616
+ /**
6617
+ * Resolves a human-readable message for a failed upload.
6618
+ *
6619
+ * @param item - The item in the `error` state.
6620
+ * @returns The error message.
6621
+ */
6622
+ errorText(item) {
6623
+ const error = item.error;
6624
+ return error?.message ?? String(error ?? '');
6625
+ }
6626
+ writeValue(value) {
6627
+ this._items().forEach((item) => this.#revokePreview(item));
6628
+ this.#subscriptions.forEach((subscription) => subscription.unsubscribe());
6629
+ this.#subscriptions.clear();
6630
+ this._items.set(toFileArray(value).map((file) => this.#createItem(file)));
6631
+ }
6632
+ /** Opens the native file dialog. */
6633
+ open() {
6634
+ if (!this.disabled()) {
6635
+ this.nativeInput()?.nativeElement.click();
6636
+ }
6637
+ }
6638
+ /**
6639
+ * Removes a file from the selection, aborting its upload and releasing its preview.
6640
+ *
6641
+ * @param id - The id of the item to remove.
6642
+ */
6643
+ remove(id) {
6644
+ const item = this._items().find((candidate) => candidate.id === id);
6645
+ if (!item || this.disabled()) {
6646
+ return;
6647
+ }
6648
+ this.#abort(id);
6649
+ this.#revokePreview(item);
6650
+ this._items.update((items) => items.filter((candidate) => candidate.id !== id));
6651
+ this.#emitValue();
6652
+ this.fileRemoved.emit(item.file);
6653
+ this.#announce(this.labels().filesSelected(this._items().length));
6654
+ }
6655
+ /** Removes every file, aborting any upload in flight. */
6656
+ clear() {
6657
+ if (this.disabled()) {
6658
+ return;
6659
+ }
6660
+ this.#subscriptions.forEach((subscription) => subscription.unsubscribe());
6661
+ this.#subscriptions.clear();
6662
+ this._items().forEach((item) => this.#revokePreview(item));
6663
+ this._items.set([]);
6664
+ this.#emitValue();
6665
+ this.#announce(this.labels().filesSelected(0));
6666
+ }
6667
+ /** Uploads every file that has not been uploaded yet. No-op without a registered uploader. */
6668
+ upload() {
6669
+ this._items()
6670
+ .filter((item) => item.status === 'ready' || item.status === 'error')
6671
+ .forEach((item) => this.#startUpload(item.id));
6672
+ }
6673
+ /**
6674
+ * Aborts an in-flight upload, returning the file to the `ready` state.
6675
+ *
6676
+ * @param id - The id of the uploading item.
6677
+ */
6678
+ cancel(id) {
6679
+ if (!this.#subscriptions.has(id)) {
6680
+ return;
6681
+ }
6682
+ this.#abort(id);
6683
+ this.#patch(id, { status: 'ready', progress: null, error: null });
6684
+ }
6685
+ /**
6686
+ * Re-runs the upload of a file, typically after a failure.
6687
+ *
6688
+ * @param id - The id of the item to upload again.
6689
+ */
6690
+ retry(id) {
6691
+ this.#startUpload(id);
6692
+ }
6693
+ /**
6694
+ * Reads the files chosen through the native dialog.
6695
+ *
6696
+ * @param event - The `change` event of the hidden native input.
6697
+ */
6698
+ handleNativeChange(event) {
6699
+ const native = event.target;
6700
+ this.#addFiles(Array.from(native.files ?? []));
6701
+ // Without this, re-picking the very same file fires no second `change` event.
6702
+ native.value = '';
6703
+ }
6704
+ /**
6705
+ * Accepts files dropped onto the dropzone.
6706
+ *
6707
+ * @param event - The drop event.
6708
+ */
6709
+ handleDrop(event) {
6710
+ if (!this.dropEnabled()) {
6711
+ return;
6712
+ }
6713
+ event.preventDefault();
6714
+ this.#dragDepth = 0;
6715
+ this.isDragging.set(false);
6716
+ this.#addFiles(this.#extractFiles(event.dataTransfer));
6717
+ this.handleBlur();
6718
+ }
6719
+ /**
6720
+ * Highlights the dropzone once a drag carrying files enters it.
6721
+ *
6722
+ * @param event - The dragenter event.
6723
+ */
6724
+ handleDragEnter(event) {
6725
+ if (!this.dropEnabled() || !this.#carriesFiles(event)) {
6726
+ return;
6727
+ }
6728
+ event.preventDefault();
6729
+ this.#dragDepth++;
6730
+ this.isDragging.set(true);
6731
+ }
6732
+ /**
6733
+ * Keeps the drop target alive. A `dragover` that does not call `preventDefault` refuses the drop.
6734
+ *
6735
+ * @param event - The dragover event.
6736
+ */
6737
+ handleDragOver(event) {
6738
+ if (!this.dropEnabled() || !this.#carriesFiles(event)) {
6739
+ return;
6740
+ }
6741
+ event.preventDefault();
6742
+ if (event.dataTransfer) {
6743
+ event.dataTransfer.dropEffect = 'copy';
6744
+ }
6745
+ }
6746
+ /**
6747
+ * Un-highlights the dropzone, but only once the drag has left it *and* all of its children.
6748
+ *
6749
+ * @param event - The dragleave event.
6750
+ */
6751
+ handleDragLeave(event) {
6752
+ if (!this.dropEnabled()) {
6753
+ return;
6754
+ }
6755
+ event.preventDefault();
6756
+ this.#dragDepth = Math.max(0, this.#dragDepth - 1);
6757
+ if (this.#dragDepth === 0) {
6758
+ this.isDragging.set(false);
6759
+ }
6760
+ }
6761
+ /**
6762
+ * Accepts files pasted into the focused field, e.g. a screenshot from the clipboard.
6763
+ *
6764
+ * @param event - The paste event.
6765
+ */
6766
+ handlePaste(event) {
6767
+ if (!this.paste() || this.disabled()) {
6768
+ return;
6769
+ }
6770
+ const files = this.#extractFiles(event.clipboardData);
6771
+ if (files.length > 0) {
6772
+ event.preventDefault();
6773
+ this.#addFiles(files);
6774
+ }
6775
+ }
6776
+ /**
6777
+ * Builds the context handed to a projected `hubFilePreview` template.
6778
+ *
6779
+ * @param item - The item being rendered.
6780
+ * @returns The template context, complete with the actions the item needs.
6781
+ */
6782
+ previewContext(item) {
6783
+ return {
6784
+ $implicit: item,
6785
+ remove: () => this.remove(item.id),
6786
+ retry: () => this.retry(item.id),
6787
+ cancel: () => this.cancel(item.id)
6788
+ };
6789
+ }
6790
+ /**
6791
+ * Validates and appends the incoming files, emitting the new value and any rejections.
6792
+ *
6793
+ * In single mode the incoming file replaces the current one, so the constraint checks run against
6794
+ * an empty baseline rather than against the file about to be discarded.
6795
+ *
6796
+ * @param incoming - The files to add.
6797
+ */
6798
+ #addFiles(incoming) {
6799
+ if (this.disabled() || incoming.length === 0) {
6800
+ return;
6801
+ }
6802
+ const multiple = this.multiple();
6803
+ const current = this._items();
6804
+ const baseline = multiple ? current : [];
6805
+ const accepted = [];
6806
+ const rejections = [];
6807
+ const keys = new Set(baseline.map((item) => fileKey(item.file)));
6808
+ let count = baseline.length;
6809
+ let total = baseline.reduce((sum, item) => sum + item.file.size, 0);
6810
+ for (const file of incoming) {
6811
+ const rejection = this.#reject(file, count, total, keys);
6812
+ if (rejection) {
6813
+ rejections.push(rejection);
6814
+ continue;
6815
+ }
6816
+ keys.add(fileKey(file));
6817
+ count++;
6818
+ total += file.size;
6819
+ accepted.push(this.#createItem(file));
6820
+ }
6821
+ if (accepted.length > 0) {
6822
+ if (!multiple) {
6823
+ current.forEach((item) => this.#abort(item.id));
6824
+ current.forEach((item) => this.#revokePreview(item));
6825
+ }
6826
+ this._items.set([...baseline, ...accepted]);
6827
+ this.#emitValue();
6828
+ if (this.#uploader && this.autoUpload()) {
6829
+ accepted.forEach((item) => this.#startUpload(item.id));
6830
+ }
6831
+ }
6832
+ if (rejections.length > 0) {
6833
+ this.rejected.emit(rejections);
6834
+ }
6835
+ this.#announce(this.#summarize(accepted.length, rejections));
6836
+ }
6837
+ /**
6838
+ * Applies the declared constraints to a single candidate file.
6839
+ *
6840
+ * @param file - The candidate.
6841
+ * @param count - How many files are already accepted.
6842
+ * @param total - The combined size of the already-accepted files.
6843
+ * @param keys - Identity keys of the already-accepted files, for duplicate detection.
6844
+ * @returns The rejection, or `null` when the file passes every constraint.
6845
+ */
6846
+ #reject(file, count, total, keys) {
6847
+ const build = (reason, limit) => ({
6848
+ file,
6849
+ reason,
6850
+ limit
6851
+ });
6852
+ if (!this.allowDuplicates() && keys.has(fileKey(file))) {
6853
+ return build('duplicate', null);
6854
+ }
6855
+ if (!matchesAccept(file, this.accept())) {
6856
+ return build('accept', this.accept());
6857
+ }
6858
+ const minSize = this.minSize();
6859
+ if (minSize != null && file.size < minSize) {
6860
+ return build('minSize', minSize);
6861
+ }
6862
+ const maxSize = this.maxSize();
6863
+ if (maxSize != null && file.size > maxSize) {
6864
+ return build('maxSize', maxSize);
6865
+ }
6866
+ const maxFiles = this.multiple() ? this.maxFiles() : 1;
6867
+ if (maxFiles != null && count + 1 > maxFiles) {
6868
+ return build('maxFiles', maxFiles);
6869
+ }
6870
+ const maxTotalSize = this.maxTotalSize();
6871
+ if (maxTotalSize != null && total + file.size > maxTotalSize) {
6872
+ return build('maxTotalSize', maxTotalSize);
6873
+ }
6874
+ return null;
6875
+ }
6876
+ /**
6877
+ * Wraps a file in an item, minting its preview URL when it is a previewable image.
6878
+ *
6879
+ * @param file - The accepted file.
6880
+ * @returns The new item, in the `ready` state.
6881
+ */
6882
+ #createItem(file) {
6883
+ const previewable = this.#isBrowser && this.preview() !== 'none' && file.type.startsWith('image/');
6884
+ return {
6885
+ id: uuid(),
6886
+ file,
6887
+ previewUrl: previewable ? URL.createObjectURL(file) : null,
6888
+ status: 'ready',
6889
+ progress: null,
6890
+ error: null
6891
+ };
6892
+ }
6893
+ /**
6894
+ * Releases the object URL of an item. Skipping this leaks the whole file for the page's lifetime.
6895
+ *
6896
+ * @param item - The item whose preview should be released.
6897
+ */
6898
+ #revokePreview(item) {
6899
+ if (item.previewUrl && this.#isBrowser) {
6900
+ URL.revokeObjectURL(item.previewUrl);
6901
+ }
6902
+ }
6903
+ /** Writes the native value to the form control and emits it. */
6904
+ #emitValue() {
6905
+ const files = this._items().map((item) => item.file);
6906
+ const value = this.multiple() ? files : (files[0] ?? null);
6907
+ this.onChange?.(value);
6908
+ this.valueChange.emit(value);
6909
+ }
6910
+ /**
6911
+ * Subscribes to the registered uploader for one item, mapping its events onto the item's status.
6912
+ *
6913
+ * @param id - The id of the item to upload.
6914
+ */
6915
+ #startUpload(id) {
6916
+ const uploader = this.#uploader;
6917
+ const item = this._items().find((candidate) => candidate.id === id);
6918
+ if (!uploader || !item) {
6919
+ return;
6920
+ }
6921
+ this.#abort(id);
6922
+ this.#patch(id, { status: 'uploading', progress: null, error: null });
6923
+ const subscription = uploader.upload(item.file, { id }).subscribe({
6924
+ next: (event) => {
6925
+ if (event.status === 'progress') {
6926
+ // A transport that cannot report a total leaves `progress` null, which the template
6927
+ // renders as an indeterminate bar rather than a bar frozen at 0%.
6928
+ const progress = event.total ? Math.round((event.loaded / event.total) * 100) : null;
6929
+ this.#patch(id, { status: 'uploading', progress });
6930
+ return;
6931
+ }
6932
+ if (event.status === 'done') {
6933
+ this.#patch(id, { status: 'done', progress: 100, error: null });
6934
+ return;
6935
+ }
6936
+ this.#patch(id, { status: 'error', error: event.error });
6937
+ },
6938
+ error: (error) => this.#patch(id, { status: 'error', error }),
6939
+ complete: () => {
6940
+ // An uploader that completes without emitting `done` still finished successfully.
6941
+ const current = this._items().find((candidate) => candidate.id === id);
6942
+ if (current?.status === 'uploading') {
6943
+ this.#patch(id, { status: 'done', progress: 100, error: null });
6944
+ }
6945
+ }
6946
+ });
6947
+ this.#subscriptions.set(id, subscription);
6948
+ }
6949
+ /**
6950
+ * Unsubscribes from an item's upload, which aborts the underlying request.
6951
+ *
6952
+ * @param id - The id of the item.
6953
+ */
6954
+ #abort(id) {
6955
+ this.#subscriptions.get(id)?.unsubscribe();
6956
+ this.#subscriptions.delete(id);
6957
+ }
6958
+ /**
6959
+ * Immutably updates one item and notifies listeners of the new upload state.
6960
+ *
6961
+ * @param id - The id of the item to update.
6962
+ * @param patch - The fields to overwrite.
6963
+ */
6964
+ #patch(id, patch) {
6965
+ let changed = false;
6966
+ this._items.update((items) => items.map((item) => {
6967
+ if (item.id !== id) {
6968
+ return item;
6969
+ }
6970
+ changed = true;
6971
+ return { ...item, ...patch };
6972
+ }));
6973
+ if (changed) {
6974
+ this.uploadStateChange.emit(this._items());
6975
+ }
6976
+ }
6977
+ /**
6978
+ * Extracts the files carried by a drop or a paste.
6979
+ *
6980
+ * Reads `items` when present so non-file payloads (dragged text, HTML) are skipped, and falls
6981
+ * back to `files` otherwise.
6982
+ *
6983
+ * @param transfer - The `DataTransfer` of the event.
6984
+ * @returns The files it carries.
6985
+ */
6986
+ #extractFiles(transfer) {
6987
+ if (!transfer) {
6988
+ return [];
6989
+ }
6990
+ if (transfer.items?.length) {
6991
+ return Array.from(transfer.items)
6992
+ .filter((item) => item.kind === 'file')
6993
+ .map((item) => item.getAsFile())
6994
+ .filter((file) => file !== null);
6995
+ }
6996
+ return Array.from(transfer.files ?? []);
6997
+ }
6998
+ /**
6999
+ * Whether a drag event carries files, as opposed to text or a page element.
7000
+ *
7001
+ * @param event - The drag event.
7002
+ * @returns `true` when the payload includes files.
7003
+ */
7004
+ #carriesFiles(event) {
7005
+ return Array.from(event.dataTransfer?.types ?? []).includes('Files');
7006
+ }
7007
+ /**
7008
+ * Builds the live-region message describing the outcome of an add.
7009
+ *
7010
+ * @param acceptedCount - How many files were accepted.
7011
+ * @param rejections - The files that were refused.
7012
+ * @returns The message to announce.
7013
+ */
7014
+ #summarize(acceptedCount, rejections) {
7015
+ const labels = this.labels();
7016
+ const parts = [];
7017
+ if (acceptedCount > 0) {
7018
+ parts.push(labels.filesSelected(this._items().length));
7019
+ }
7020
+ rejections.forEach((rejection) => parts.push(labels.rejection(rejection)));
7021
+ return parts.join(' ');
7022
+ }
7023
+ /**
7024
+ * Publishes a message to the live region, forcing a re-announcement of identical text.
7025
+ *
7026
+ * @param message - The message to announce.
7027
+ */
7028
+ #announce(message) {
7029
+ if (!message) {
7030
+ return;
7031
+ }
7032
+ // A screen reader ignores a live region whose text did not change; the zero-width space makes
7033
+ // two consecutive identical messages differ.
7034
+ this.announcement.set(this.announcement() === message ? `${message}​` : message);
7035
+ }
7036
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubFileInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
7037
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: HubFileInputComponent, isStandalone: true, selector: "hub-file-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, minSize: { classPropertyName: "minSize", publicName: "minSize", isSignal: true, isRequired: false, transformFunction: null }, maxTotalSize: { classPropertyName: "maxTotalSize", publicName: "maxTotalSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, dragDrop: { classPropertyName: "dragDrop", publicName: "dragDrop", isSignal: true, isRequired: false, transformFunction: null }, paste: { classPropertyName: "paste", publicName: "paste", isSignal: true, isRequired: false, transformFunction: null }, preview: { classPropertyName: "preview", publicName: "preview", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, capture: { classPropertyName: "capture", publicName: "capture", isSignal: true, isRequired: false, transformFunction: null }, autoUpload: { classPropertyName: "autoUpload", publicName: "autoUpload", isSignal: true, isRequired: false, transformFunction: null }, buttonLabel: { classPropertyName: "buttonLabel", publicName: "buttonLabel", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, formText: { classPropertyName: "formText", publicName: "formText", isSignal: true, isRequired: false, transformFunction: null }, formTextType: { classPropertyName: "formTextType", publicName: "formTextType", isSignal: true, isRequired: false, transformFunction: null }, classlist: { classPropertyName: "classlist", publicName: "classlist", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", rejected: "rejected", fileRemoved: "fileRemoved", uploadStateChange: "uploadStateChange" }, host: { listeners: { "paste": "handlePaste($event)" }, properties: { "class": "classlist()", "class.hub-file-input-host": "true" } }, queries: [{ propertyName: "iconTpt", first: true, predicate: HubFileIconDirective, descendants: true, isSignal: true }, { propertyName: "previewTpt", first: true, predicate: HubFilePreviewDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "nativeInput", first: true, predicate: ["nativeInput"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n\tclass=\"hub-field hub-file-input\"\n\t[class.hub-field--disabled]=\"disabled()\"\n\t[class.hub-field--invalid]=\"isInvalid\"\n\t[class.hub-field--valid]=\"showsValid\"\n>\n\t@if (label() || required()) {\n\t\t<label [for]=\"id\" class=\"hub-field__label\">\n\t\t\t{{ label() }}\n\t\t\t@if (required()) {\n\t\t\t\t<span class=\"hub-field__required\" aria-hidden=\"true\">*</span>\n\t\t\t}\n\t\t</label>\n\t}\n\n\t<div class=\"hub-field__body\">\n\t\t<!--\n\t\t\tThe dropzone is a <label> bound to the hidden native input: clicking or activating it with\n\t\t\tthe keyboard opens the file dialog through the platform, so no role, tabindex or key\n\t\t\thandler is needed. The \"browse\" affordance is a <span>, not a <button>, because a button\n\t\t\tinside a label would open the dialog twice.\n\t\t-->\n\t\t<label\n\t\t\tclass=\"hub-file-input__dropzone\"\n\t\t\t[for]=\"id\"\n\t\t\t[class.hub-file-input__dropzone--dragover]=\"isDragging()\"\n\t\t\t[class.hub-file-input__dropzone--no-drop]=\"!dragDrop()\"\n\t\t\t[class.hub-file-input__dropzone--invalid]=\"isInvalid\"\n\t\t\t(dragenter)=\"handleDragEnter($event)\"\n\t\t\t(dragover)=\"handleDragOver($event)\"\n\t\t\t(dragleave)=\"handleDragLeave($event)\"\n\t\t\t(drop)=\"handleDrop($event)\"\n\t\t>\n\t\t\t<input\n\t\t\t\t#nativeInput\n\t\t\t\ttype=\"file\"\n\t\t\t\tclass=\"hub-file-input__native\"\n\t\t\t\t[id]=\"id\"\n\t\t\t\t[accept]=\"accept()\"\n\t\t\t\t[multiple]=\"multiple()\"\n\t\t\t\t[disabled]=\"disabled()\"\n\t\t\t\t[attr.capture]=\"capture()\"\n\t\t\t\t[attr.aria-required]=\"required() ? 'true' : null\"\n\t\t\t\t[attr.aria-invalid]=\"isInvalid\"\n\t\t\t\t[attr.aria-describedby]=\"constraintsHint() ? id + '-hint' : null\"\n\t\t\t\t(change)=\"handleNativeChange($event)\"\n\t\t\t\t(blur)=\"handleBlur($event)\"\n\t\t\t/>\n\n\t\t\t<span class=\"hub-file-input__icon\" aria-hidden=\"true\"></span>\n\n\t\t\t<span class=\"hub-file-input__prompt\">\n\t\t\t\t@if (dragDrop()) {\n\t\t\t\t\t<span class=\"hub-file-input__drop-text\">{{ labels().dropHere }}</span>\n\t\t\t\t}\n\t\t\t\t<span class=\"hub-file-input__browse\">{{ browseLabel() }}</span>\n\t\t\t</span>\n\n\t\t\t@if (constraintsHint()) {\n\t\t\t\t<span class=\"hub-file-input__hint\" [id]=\"id + '-hint'\">{{ constraintsHint() }}</span>\n\t\t\t}\n\t\t</label>\n\n\t\t@if (preview() !== 'none' && _items().length > 0) {\n\t\t\t<ul class=\"hub-file-input__list\" [class.hub-file-input__list--grid]=\"preview() === 'grid'\">\n\t\t\t\t@for (item of _items(); track item.id) {\n\t\t\t\t\t<li class=\"hub-file-input__item\" [attr.data-status]=\"item.status\">\n\t\t\t\t\t\t@if (previewTpt(); as tpt) {\n\t\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t\t[ngTemplateOutlet]=\"tpt.template\"\n\t\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"previewContext(item)\"\n\t\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t} @else {\n\t\t\t\t\t\t\t<span class=\"hub-file-input__thumb\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t@if (item.previewUrl) {\n\t\t\t\t\t\t\t\t\t<img class=\"hub-file-input__image\" [src]=\"item.previewUrl\" alt=\"\" />\n\t\t\t\t\t\t\t\t} @else if (iconTpt(); as icon) {\n\t\t\t\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t\t\t\t[ngTemplateOutlet]=\"icon.template\"\n\t\t\t\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: item }\"\n\t\t\t\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t\t\t} @else {\n\t\t\t\t\t\t\t\t\t<span class=\"hub-file-input__file-icon\"></span>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<span class=\"hub-file-input__meta\">\n\t\t\t\t\t\t\t\t<span class=\"hub-file-input__name\" [title]=\"item.file.name\">{{ item.file.name }}</span>\n\t\t\t\t\t\t\t\t<span class=\"hub-file-input__size\">{{ labels().formatSize(item.file.size) }}</span>\n\n\t\t\t\t\t\t\t\t@if (item.status === 'uploading') {\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__progress\"\n\t\t\t\t\t\t\t\t\t\t[class.hub-file-input__progress--indeterminate]=\"item.progress === null\"\n\t\t\t\t\t\t\t\t\t\trole=\"progressbar\"\n\t\t\t\t\t\t\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t\t\t\t\t\t\taria-valuemax=\"100\"\n\t\t\t\t\t\t\t\t\t\t[attr.aria-valuenow]=\"item.progress\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__progress-bar\"\n\t\t\t\t\t\t\t\t\t\t\t[style.--hub-file-input-progress-value]=\"item.progress ?? 0\"\n\t\t\t\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t@if (item.status === 'error') {\n\t\t\t\t\t\t\t\t\t<span class=\"hub-file-input__error\">{{ errorText(item) }}</span>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<span class=\"hub-file-input__actions\">\n\t\t\t\t\t\t\t\t@if (item.status === 'uploading') {\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__action hub-file-input__action--cancel\"\n\t\t\t\t\t\t\t\t\t\t[attr.aria-label]=\"labels().cancel\"\n\t\t\t\t\t\t\t\t\t\t(click)=\"cancel(item.id)\"\n\t\t\t\t\t\t\t\t\t></button>\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t@if (item.status === 'error') {\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__action hub-file-input__action--retry\"\n\t\t\t\t\t\t\t\t\t\t[attr.aria-label]=\"labels().retry\"\n\t\t\t\t\t\t\t\t\t\t(click)=\"retry(item.id)\"\n\t\t\t\t\t\t\t\t\t></button>\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t@if (!disabled() && item.status !== 'uploading') {\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__action hub-file-input__action--remove\"\n\t\t\t\t\t\t\t\t\t\t[attr.aria-label]=\"labels().remove\"\n\t\t\t\t\t\t\t\t\t\t(click)=\"remove(item.id)\"\n\t\t\t\t\t\t\t\t\t></button>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t}\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\n\t\t\t@if (_items().length > 1 && !disabled()) {\n\t\t\t\t<button type=\"button\" class=\"hub-file-input__clear\" (click)=\"clear()\">{{ labels().clear }}</button>\n\t\t\t}\n\t\t}\n\t</div>\n\n\t@if (formText() || formTextTmp()) {\n\t\t<div class=\"hub-field__form-text\" [class.hub-field__form-text--disabled]=\"disabled()\">\n\t\t\t@if (formTextTmp()) {\n\t\t\t\t<ng-container [ngTemplateOutlet]=\"formTextTmp()!\"></ng-container>\n\t\t\t} @else {\n\t\t\t\t{{ formText() }}\n\t\t\t}\n\t\t</div>\n\t}\n\n\t@if (isInvalid) {\n\t\t<div class=\"hub-field__feedback\" role=\"alert\">\n\t\t\t<ul>\n\t\t\t\t@for (error of errors | keyvalue; track error.key) {\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"_errorTemplates[error.key] ?? defaultErrorTpt\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: error.value, value: error.value }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t<ng-template #defaultErrorTpt>\n\t\t\t\t\t\t\t<span class=\"hub-field__feedback-text\" [innerHTML]=\"getInvalidFeedbackTemplate(error.key, error.value)\"></span>\n\t\t\t\t\t\t</ng-template>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\t\t</div>\n\t}\n\n\t@if (showsValid && validFeedback()) {\n\t\t<div class=\"hub-field__feedback hub-field__feedback--valid\" role=\"status\">\n\t\t\t<span class=\"hub-field__feedback-text\">{{ validFeedback() }}</span>\n\t\t</div>\n\t}\n\n\t<span class=\"hub-file-input__live\" aria-live=\"polite\">{{ announcement() }}</span>\n</div>\n", styles: [".hub-file-input__dropzone{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--hub-file-input-gap);min-height:var(--hub-file-input-min-height);padding:var(--hub-file-input-padding-y) var(--hub-file-input-padding-x);color:var(--hub-file-input-color);background-color:var(--hub-file-input-bg);border:var(--hub-file-input-border-width) var(--hub-file-input-border-style) var(--hub-file-input-border-color);border-radius:var(--hub-file-input-border-radius);text-align:center;cursor:pointer;transition:var(--hub-file-input-transition)}.hub-file-input__dropzone:hover{background-color:var(--hub-file-input-hover-bg);border-color:var(--hub-file-input-hover-border-color)}.hub-file-input__dropzone:has(.hub-file-input__native:focus-visible){border-color:var(--hub-file-input-focus-border-color);box-shadow:var(--hub-file-input-focus-box-shadow);outline:none}.hub-file-input__dropzone--dragover,.hub-file-input__dropzone--dragover:hover{background-color:var(--hub-file-input-dragover-bg);border-color:var(--hub-file-input-dragover-border-color)}.hub-file-input__dropzone--invalid{border-color:var(--hub-form-invalid-border-color)}.hub-file-input__dropzone:has(.hub-file-input__native:disabled){cursor:not-allowed}.hub-file-input__dropzone:has(.hub-file-input__native:disabled):hover{background-color:var(--hub-file-input-bg);border-color:var(--hub-file-input-border-color)}.hub-file-input__native{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.hub-file-input__icon{display:block;width:var(--hub-file-input-icon-size);height:var(--hub-file-input-icon-size);background-color:var(--hub-file-input-icon-color);-webkit-mask:var(--hub-file-input-icon) center/contain no-repeat;mask:var(--hub-file-input-icon) center/contain no-repeat}.hub-file-input__prompt{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:center;gap:.25em;color:var(--hub-file-input-prompt-color);font-size:var(--hub-file-input-prompt-font-size)}.hub-file-input__browse{color:var(--hub-file-input-browse-color);font-weight:var(--hub-file-input-browse-font-weight);text-decoration:underline}.hub-file-input__hint{color:var(--hub-file-input-hint-color);font-size:var(--hub-file-input-hint-font-size)}.hub-file-input__list{display:flex;flex-direction:column;gap:var(--hub-file-input-list-gap);margin:var(--hub-file-input-list-margin-top) 0 0;padding:0;list-style:none}.hub-file-input__list--grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--hub-file-input-grid-min-width),1fr));gap:var(--hub-file-input-grid-gap)}.hub-file-input__item{display:flex;align-items:center;gap:var(--hub-file-input-item-gap);padding:var(--hub-file-input-item-padding-y) var(--hub-file-input-item-padding-x);color:var(--hub-file-input-item-color);background-color:var(--hub-file-input-item-bg);border:var(--hub-file-input-border-width) solid var(--hub-file-input-item-border-color);border-radius:var(--hub-file-input-item-border-radius);min-width:0}.hub-file-input__item[data-status=error]{border-color:var(--hub-file-input-error-color)}.hub-file-input__item[data-status=done]{border-color:var(--hub-file-input-done-color)}.hub-file-input__thumb{display:flex;flex:none;align-items:center;justify-content:center;width:var(--hub-file-input-thumb-size);height:var(--hub-file-input-thumb-size);overflow:hidden;background-color:var(--hub-file-input-thumb-bg);border-radius:var(--hub-file-input-thumb-radius)}.hub-file-input__image{width:100%;height:100%;object-fit:cover}.hub-file-input__file-icon{display:block;width:var(--hub-file-input-file-icon-size);height:var(--hub-file-input-file-icon-size);background-color:var(--hub-file-input-file-icon-color);-webkit-mask:var(--hub-file-input-file-icon) center/contain no-repeat;mask:var(--hub-file-input-file-icon) center/contain no-repeat}.hub-file-input__meta{display:flex;flex:1 1 auto;flex-direction:column;gap:.125rem;min-width:0}.hub-file-input__name{overflow:hidden;font-size:var(--hub-file-input-name-font-size);text-overflow:ellipsis;white-space:nowrap}.hub-file-input__size{color:var(--hub-file-input-size-color);font-size:var(--hub-file-input-size-font-size)}.hub-file-input__error{color:var(--hub-file-input-error-color);font-size:var(--hub-file-input-error-font-size)}.hub-file-input__progress{display:block;width:100%;height:var(--hub-file-input-progress-height);overflow:hidden;background-color:var(--hub-file-input-progress-track-bg);border-radius:var(--hub-file-input-progress-radius)}.hub-file-input__progress-bar{display:block;width:calc(var(--hub-file-input-progress-value, 0) * 1%);height:100%;background-color:var(--hub-file-input-progress-bar-bg);border-radius:inherit;transition:width .15s linear}.hub-file-input__progress--indeterminate .hub-file-input__progress-bar{width:33%;animation:hub-file-input-indeterminate var(--hub-file-input-progress-indeterminate-duration) ease-in-out infinite}.hub-file-input__actions{display:flex;flex:none;align-items:center;gap:.25rem}.hub-file-input__action{display:block;width:var(--hub-file-input-action-size);height:var(--hub-file-input-action-size);padding:0;background-color:var(--hub-file-input-action-color);border:0;cursor:pointer;transition:var(--hub-file-input-transition)}.hub-file-input__action:hover{background-color:var(--hub-file-input-action-hover-color)}.hub-file-input__action--remove{-webkit-mask:var(--hub-file-input-remove-icon) center/contain no-repeat;mask:var(--hub-file-input-remove-icon) center/contain no-repeat}.hub-file-input__action--remove:hover{background-color:var(--hub-file-input-remove-hover-color)}.hub-file-input__action--cancel{-webkit-mask:var(--hub-file-input-cancel-icon) center/contain no-repeat;mask:var(--hub-file-input-cancel-icon) center/contain no-repeat}.hub-file-input__action--retry{-webkit-mask:var(--hub-file-input-retry-icon) center/contain no-repeat;mask:var(--hub-file-input-retry-icon) center/contain no-repeat}.hub-file-input__clear{align-self:flex-start;margin-top:var(--hub-file-input-list-margin-top);padding:0;color:var(--hub-file-input-clear-color);font-size:var(--hub-file-input-clear-font-size);background:none;border:0;text-decoration:underline;cursor:pointer}.hub-file-input__item[data-status=done] .hub-file-input__size:after{display:inline-block;width:.75em;height:.75em;margin-inline-start:.375em;background-color:var(--hub-file-input-done-color);-webkit-mask:var(--hub-file-input-done-icon) center/contain no-repeat;mask:var(--hub-file-input-done-icon) center/contain no-repeat;content:\"\"}.hub-file-input__live{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.hub-file-input__list--grid .hub-file-input__item{flex-direction:column;align-items:stretch}.hub-file-input__list--grid .hub-file-input__thumb{width:100%;height:var(--hub-file-input-grid-thumb-height)}.hub-file-input__list--grid .hub-file-input__actions{justify-content:flex-end}@keyframes hub-file-input-indeterminate{0%{transform:translate(-100%)}to{transform:translate(300%)}}@media(prefers-reduced-motion:reduce){.hub-file-input__progress--indeterminate .hub-file-input__progress-bar{animation:none;width:100%;opacity:.5}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7038
+ }
7039
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubFileInputComponent, decorators: [{
7040
+ type: Component,
7041
+ args: [{ selector: 'hub-file-input', imports: [NgTemplateOutlet, KeyValuePipe], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
7042
+ '[class]': 'classlist()',
7043
+ '[class.hub-file-input-host]': 'true',
7044
+ '(paste)': 'handlePaste($event)'
7045
+ }, template: "<div\n\tclass=\"hub-field hub-file-input\"\n\t[class.hub-field--disabled]=\"disabled()\"\n\t[class.hub-field--invalid]=\"isInvalid\"\n\t[class.hub-field--valid]=\"showsValid\"\n>\n\t@if (label() || required()) {\n\t\t<label [for]=\"id\" class=\"hub-field__label\">\n\t\t\t{{ label() }}\n\t\t\t@if (required()) {\n\t\t\t\t<span class=\"hub-field__required\" aria-hidden=\"true\">*</span>\n\t\t\t}\n\t\t</label>\n\t}\n\n\t<div class=\"hub-field__body\">\n\t\t<!--\n\t\t\tThe dropzone is a <label> bound to the hidden native input: clicking or activating it with\n\t\t\tthe keyboard opens the file dialog through the platform, so no role, tabindex or key\n\t\t\thandler is needed. The \"browse\" affordance is a <span>, not a <button>, because a button\n\t\t\tinside a label would open the dialog twice.\n\t\t-->\n\t\t<label\n\t\t\tclass=\"hub-file-input__dropzone\"\n\t\t\t[for]=\"id\"\n\t\t\t[class.hub-file-input__dropzone--dragover]=\"isDragging()\"\n\t\t\t[class.hub-file-input__dropzone--no-drop]=\"!dragDrop()\"\n\t\t\t[class.hub-file-input__dropzone--invalid]=\"isInvalid\"\n\t\t\t(dragenter)=\"handleDragEnter($event)\"\n\t\t\t(dragover)=\"handleDragOver($event)\"\n\t\t\t(dragleave)=\"handleDragLeave($event)\"\n\t\t\t(drop)=\"handleDrop($event)\"\n\t\t>\n\t\t\t<input\n\t\t\t\t#nativeInput\n\t\t\t\ttype=\"file\"\n\t\t\t\tclass=\"hub-file-input__native\"\n\t\t\t\t[id]=\"id\"\n\t\t\t\t[accept]=\"accept()\"\n\t\t\t\t[multiple]=\"multiple()\"\n\t\t\t\t[disabled]=\"disabled()\"\n\t\t\t\t[attr.capture]=\"capture()\"\n\t\t\t\t[attr.aria-required]=\"required() ? 'true' : null\"\n\t\t\t\t[attr.aria-invalid]=\"isInvalid\"\n\t\t\t\t[attr.aria-describedby]=\"constraintsHint() ? id + '-hint' : null\"\n\t\t\t\t(change)=\"handleNativeChange($event)\"\n\t\t\t\t(blur)=\"handleBlur($event)\"\n\t\t\t/>\n\n\t\t\t<span class=\"hub-file-input__icon\" aria-hidden=\"true\"></span>\n\n\t\t\t<span class=\"hub-file-input__prompt\">\n\t\t\t\t@if (dragDrop()) {\n\t\t\t\t\t<span class=\"hub-file-input__drop-text\">{{ labels().dropHere }}</span>\n\t\t\t\t}\n\t\t\t\t<span class=\"hub-file-input__browse\">{{ browseLabel() }}</span>\n\t\t\t</span>\n\n\t\t\t@if (constraintsHint()) {\n\t\t\t\t<span class=\"hub-file-input__hint\" [id]=\"id + '-hint'\">{{ constraintsHint() }}</span>\n\t\t\t}\n\t\t</label>\n\n\t\t@if (preview() !== 'none' && _items().length > 0) {\n\t\t\t<ul class=\"hub-file-input__list\" [class.hub-file-input__list--grid]=\"preview() === 'grid'\">\n\t\t\t\t@for (item of _items(); track item.id) {\n\t\t\t\t\t<li class=\"hub-file-input__item\" [attr.data-status]=\"item.status\">\n\t\t\t\t\t\t@if (previewTpt(); as tpt) {\n\t\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t\t[ngTemplateOutlet]=\"tpt.template\"\n\t\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"previewContext(item)\"\n\t\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t} @else {\n\t\t\t\t\t\t\t<span class=\"hub-file-input__thumb\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t@if (item.previewUrl) {\n\t\t\t\t\t\t\t\t\t<img class=\"hub-file-input__image\" [src]=\"item.previewUrl\" alt=\"\" />\n\t\t\t\t\t\t\t\t} @else if (iconTpt(); as icon) {\n\t\t\t\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t\t\t\t[ngTemplateOutlet]=\"icon.template\"\n\t\t\t\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: item }\"\n\t\t\t\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t\t\t} @else {\n\t\t\t\t\t\t\t\t\t<span class=\"hub-file-input__file-icon\"></span>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<span class=\"hub-file-input__meta\">\n\t\t\t\t\t\t\t\t<span class=\"hub-file-input__name\" [title]=\"item.file.name\">{{ item.file.name }}</span>\n\t\t\t\t\t\t\t\t<span class=\"hub-file-input__size\">{{ labels().formatSize(item.file.size) }}</span>\n\n\t\t\t\t\t\t\t\t@if (item.status === 'uploading') {\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__progress\"\n\t\t\t\t\t\t\t\t\t\t[class.hub-file-input__progress--indeterminate]=\"item.progress === null\"\n\t\t\t\t\t\t\t\t\t\trole=\"progressbar\"\n\t\t\t\t\t\t\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t\t\t\t\t\t\taria-valuemax=\"100\"\n\t\t\t\t\t\t\t\t\t\t[attr.aria-valuenow]=\"item.progress\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__progress-bar\"\n\t\t\t\t\t\t\t\t\t\t\t[style.--hub-file-input-progress-value]=\"item.progress ?? 0\"\n\t\t\t\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t@if (item.status === 'error') {\n\t\t\t\t\t\t\t\t\t<span class=\"hub-file-input__error\">{{ errorText(item) }}</span>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<span class=\"hub-file-input__actions\">\n\t\t\t\t\t\t\t\t@if (item.status === 'uploading') {\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__action hub-file-input__action--cancel\"\n\t\t\t\t\t\t\t\t\t\t[attr.aria-label]=\"labels().cancel\"\n\t\t\t\t\t\t\t\t\t\t(click)=\"cancel(item.id)\"\n\t\t\t\t\t\t\t\t\t></button>\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t@if (item.status === 'error') {\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__action hub-file-input__action--retry\"\n\t\t\t\t\t\t\t\t\t\t[attr.aria-label]=\"labels().retry\"\n\t\t\t\t\t\t\t\t\t\t(click)=\"retry(item.id)\"\n\t\t\t\t\t\t\t\t\t></button>\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t@if (!disabled() && item.status !== 'uploading') {\n\t\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\tclass=\"hub-file-input__action hub-file-input__action--remove\"\n\t\t\t\t\t\t\t\t\t\t[attr.aria-label]=\"labels().remove\"\n\t\t\t\t\t\t\t\t\t\t(click)=\"remove(item.id)\"\n\t\t\t\t\t\t\t\t\t></button>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t}\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\n\t\t\t@if (_items().length > 1 && !disabled()) {\n\t\t\t\t<button type=\"button\" class=\"hub-file-input__clear\" (click)=\"clear()\">{{ labels().clear }}</button>\n\t\t\t}\n\t\t}\n\t</div>\n\n\t@if (formText() || formTextTmp()) {\n\t\t<div class=\"hub-field__form-text\" [class.hub-field__form-text--disabled]=\"disabled()\">\n\t\t\t@if (formTextTmp()) {\n\t\t\t\t<ng-container [ngTemplateOutlet]=\"formTextTmp()!\"></ng-container>\n\t\t\t} @else {\n\t\t\t\t{{ formText() }}\n\t\t\t}\n\t\t</div>\n\t}\n\n\t@if (isInvalid) {\n\t\t<div class=\"hub-field__feedback\" role=\"alert\">\n\t\t\t<ul>\n\t\t\t\t@for (error of errors | keyvalue; track error.key) {\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<ng-container\n\t\t\t\t\t\t\t[ngTemplateOutlet]=\"_errorTemplates[error.key] ?? defaultErrorTpt\"\n\t\t\t\t\t\t\t[ngTemplateOutletContext]=\"{ $implicit: error.value, value: error.value }\"\n\t\t\t\t\t\t></ng-container>\n\t\t\t\t\t\t<ng-template #defaultErrorTpt>\n\t\t\t\t\t\t\t<span class=\"hub-field__feedback-text\" [innerHTML]=\"getInvalidFeedbackTemplate(error.key, error.value)\"></span>\n\t\t\t\t\t\t</ng-template>\n\t\t\t\t\t</li>\n\t\t\t\t}\n\t\t\t</ul>\n\t\t</div>\n\t}\n\n\t@if (showsValid && validFeedback()) {\n\t\t<div class=\"hub-field__feedback hub-field__feedback--valid\" role=\"status\">\n\t\t\t<span class=\"hub-field__feedback-text\">{{ validFeedback() }}</span>\n\t\t</div>\n\t}\n\n\t<span class=\"hub-file-input__live\" aria-live=\"polite\">{{ announcement() }}</span>\n</div>\n", styles: [".hub-file-input__dropzone{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--hub-file-input-gap);min-height:var(--hub-file-input-min-height);padding:var(--hub-file-input-padding-y) var(--hub-file-input-padding-x);color:var(--hub-file-input-color);background-color:var(--hub-file-input-bg);border:var(--hub-file-input-border-width) var(--hub-file-input-border-style) var(--hub-file-input-border-color);border-radius:var(--hub-file-input-border-radius);text-align:center;cursor:pointer;transition:var(--hub-file-input-transition)}.hub-file-input__dropzone:hover{background-color:var(--hub-file-input-hover-bg);border-color:var(--hub-file-input-hover-border-color)}.hub-file-input__dropzone:has(.hub-file-input__native:focus-visible){border-color:var(--hub-file-input-focus-border-color);box-shadow:var(--hub-file-input-focus-box-shadow);outline:none}.hub-file-input__dropzone--dragover,.hub-file-input__dropzone--dragover:hover{background-color:var(--hub-file-input-dragover-bg);border-color:var(--hub-file-input-dragover-border-color)}.hub-file-input__dropzone--invalid{border-color:var(--hub-form-invalid-border-color)}.hub-file-input__dropzone:has(.hub-file-input__native:disabled){cursor:not-allowed}.hub-file-input__dropzone:has(.hub-file-input__native:disabled):hover{background-color:var(--hub-file-input-bg);border-color:var(--hub-file-input-border-color)}.hub-file-input__native{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.hub-file-input__icon{display:block;width:var(--hub-file-input-icon-size);height:var(--hub-file-input-icon-size);background-color:var(--hub-file-input-icon-color);-webkit-mask:var(--hub-file-input-icon) center/contain no-repeat;mask:var(--hub-file-input-icon) center/contain no-repeat}.hub-file-input__prompt{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:center;gap:.25em;color:var(--hub-file-input-prompt-color);font-size:var(--hub-file-input-prompt-font-size)}.hub-file-input__browse{color:var(--hub-file-input-browse-color);font-weight:var(--hub-file-input-browse-font-weight);text-decoration:underline}.hub-file-input__hint{color:var(--hub-file-input-hint-color);font-size:var(--hub-file-input-hint-font-size)}.hub-file-input__list{display:flex;flex-direction:column;gap:var(--hub-file-input-list-gap);margin:var(--hub-file-input-list-margin-top) 0 0;padding:0;list-style:none}.hub-file-input__list--grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--hub-file-input-grid-min-width),1fr));gap:var(--hub-file-input-grid-gap)}.hub-file-input__item{display:flex;align-items:center;gap:var(--hub-file-input-item-gap);padding:var(--hub-file-input-item-padding-y) var(--hub-file-input-item-padding-x);color:var(--hub-file-input-item-color);background-color:var(--hub-file-input-item-bg);border:var(--hub-file-input-border-width) solid var(--hub-file-input-item-border-color);border-radius:var(--hub-file-input-item-border-radius);min-width:0}.hub-file-input__item[data-status=error]{border-color:var(--hub-file-input-error-color)}.hub-file-input__item[data-status=done]{border-color:var(--hub-file-input-done-color)}.hub-file-input__thumb{display:flex;flex:none;align-items:center;justify-content:center;width:var(--hub-file-input-thumb-size);height:var(--hub-file-input-thumb-size);overflow:hidden;background-color:var(--hub-file-input-thumb-bg);border-radius:var(--hub-file-input-thumb-radius)}.hub-file-input__image{width:100%;height:100%;object-fit:cover}.hub-file-input__file-icon{display:block;width:var(--hub-file-input-file-icon-size);height:var(--hub-file-input-file-icon-size);background-color:var(--hub-file-input-file-icon-color);-webkit-mask:var(--hub-file-input-file-icon) center/contain no-repeat;mask:var(--hub-file-input-file-icon) center/contain no-repeat}.hub-file-input__meta{display:flex;flex:1 1 auto;flex-direction:column;gap:.125rem;min-width:0}.hub-file-input__name{overflow:hidden;font-size:var(--hub-file-input-name-font-size);text-overflow:ellipsis;white-space:nowrap}.hub-file-input__size{color:var(--hub-file-input-size-color);font-size:var(--hub-file-input-size-font-size)}.hub-file-input__error{color:var(--hub-file-input-error-color);font-size:var(--hub-file-input-error-font-size)}.hub-file-input__progress{display:block;width:100%;height:var(--hub-file-input-progress-height);overflow:hidden;background-color:var(--hub-file-input-progress-track-bg);border-radius:var(--hub-file-input-progress-radius)}.hub-file-input__progress-bar{display:block;width:calc(var(--hub-file-input-progress-value, 0) * 1%);height:100%;background-color:var(--hub-file-input-progress-bar-bg);border-radius:inherit;transition:width .15s linear}.hub-file-input__progress--indeterminate .hub-file-input__progress-bar{width:33%;animation:hub-file-input-indeterminate var(--hub-file-input-progress-indeterminate-duration) ease-in-out infinite}.hub-file-input__actions{display:flex;flex:none;align-items:center;gap:.25rem}.hub-file-input__action{display:block;width:var(--hub-file-input-action-size);height:var(--hub-file-input-action-size);padding:0;background-color:var(--hub-file-input-action-color);border:0;cursor:pointer;transition:var(--hub-file-input-transition)}.hub-file-input__action:hover{background-color:var(--hub-file-input-action-hover-color)}.hub-file-input__action--remove{-webkit-mask:var(--hub-file-input-remove-icon) center/contain no-repeat;mask:var(--hub-file-input-remove-icon) center/contain no-repeat}.hub-file-input__action--remove:hover{background-color:var(--hub-file-input-remove-hover-color)}.hub-file-input__action--cancel{-webkit-mask:var(--hub-file-input-cancel-icon) center/contain no-repeat;mask:var(--hub-file-input-cancel-icon) center/contain no-repeat}.hub-file-input__action--retry{-webkit-mask:var(--hub-file-input-retry-icon) center/contain no-repeat;mask:var(--hub-file-input-retry-icon) center/contain no-repeat}.hub-file-input__clear{align-self:flex-start;margin-top:var(--hub-file-input-list-margin-top);padding:0;color:var(--hub-file-input-clear-color);font-size:var(--hub-file-input-clear-font-size);background:none;border:0;text-decoration:underline;cursor:pointer}.hub-file-input__item[data-status=done] .hub-file-input__size:after{display:inline-block;width:.75em;height:.75em;margin-inline-start:.375em;background-color:var(--hub-file-input-done-color);-webkit-mask:var(--hub-file-input-done-icon) center/contain no-repeat;mask:var(--hub-file-input-done-icon) center/contain no-repeat;content:\"\"}.hub-file-input__live{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.hub-file-input__list--grid .hub-file-input__item{flex-direction:column;align-items:stretch}.hub-file-input__list--grid .hub-file-input__thumb{width:100%;height:var(--hub-file-input-grid-thumb-height)}.hub-file-input__list--grid .hub-file-input__actions{justify-content:flex-end}@keyframes hub-file-input-indeterminate{0%{transform:translate(-100%)}to{transform:translate(300%)}}@media(prefers-reduced-motion:reduce){.hub-file-input__progress--indeterminate .hub-file-input__progress-bar{animation:none;width:100%;opacity:.5}}\n"] }]
7046
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], minSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "minSize", required: false }] }], maxTotalSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxTotalSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], dragDrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragDrop", required: false }] }], paste: [{ type: i0.Input, args: [{ isSignal: true, alias: "paste", required: false }] }], preview: [{ type: i0.Input, args: [{ isSignal: true, alias: "preview", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], capture: [{ type: i0.Input, args: [{ isSignal: true, alias: "capture", required: false }] }], autoUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoUpload", required: false }] }], buttonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], formText: [{ type: i0.Input, args: [{ isSignal: true, alias: "formText", required: false }] }], formTextType: [{ type: i0.Input, args: [{ isSignal: true, alias: "formTextType", required: false }] }], classlist: [{ type: i0.Input, args: [{ isSignal: true, alias: "classlist", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], uploadStateChange: [{ type: i0.Output, args: ["uploadStateChange"] }], nativeInput: [{ type: i0.ViewChild, args: ['nativeInput', { isSignal: true }] }], iconTpt: [{ type: i0.ContentChild, args: [i0.forwardRef(() => HubFileIconDirective), { isSignal: true }] }], previewTpt: [{ type: i0.ContentChild, args: [i0.forwardRef(() => HubFilePreviewDirective), { isSignal: true }] }] } });
7047
+
6096
7048
  /**
6097
7049
  * Inverts a HEX color, or returns the best contrasting black/white when `bw` is enabled.
6098
7050
  */
@@ -6293,6 +7245,166 @@ function hubAreEqual(primaryControlName, secondaryControlName) {
6293
7245
  };
6294
7246
  }
6295
7247
 
7248
+ /**
7249
+ * Validates that every file held by the control matches an `accept` specification.
7250
+ *
7251
+ * Complements the `accept` input of `<hub-file-input>`, which *filters* offending files before they
7252
+ * reach the value. This validator instead makes the control **invalid**, which is what you want when
7253
+ * files can also arrive from outside the component (patched value, restored draft, another control).
7254
+ *
7255
+ * Sets the `fileAccept` error with `{ accept, files }`, where `files` lists the offending names.
7256
+ * An empty control is valid — pair it with `Validators.required` to demand a file.
7257
+ *
7258
+ * @param accept - The accept specification, e.g. `'image/*,.pdf'`.
7259
+ * @returns A validator function for a file-valued control.
7260
+ *
7261
+ * @example
7262
+ * ```ts
7263
+ * new FormControl<File[]>([], [hubAcceptedFiles('image/*,.pdf')]);
7264
+ * ```
7265
+ */
7266
+ function hubAcceptedFiles(accept) {
7267
+ return (control) => {
7268
+ const offending = toFileArray(control.value).filter((file) => !matchesAccept(file, accept));
7269
+ if (offending.length === 0) {
7270
+ return null;
7271
+ }
7272
+ return { fileAccept: { accept, files: offending.map((file) => file.name) } };
7273
+ };
7274
+ }
7275
+
7276
+ /**
7277
+ * Validates that no file held by the control exceeds a maximum size.
7278
+ *
7279
+ * Sets the `fileMaxSize` error with `{ max, actual, files }`, where `actual` is the size of the
7280
+ * largest offending file and `files` lists the offending names. An empty control is valid.
7281
+ *
7282
+ * @param maxBytes - The maximum size per file, in bytes.
7283
+ * @returns A validator function for a file-valued control.
7284
+ *
7285
+ * @example
7286
+ * ```ts
7287
+ * new FormControl<File | null>(null, [hubMaxFileSize(5 * 1024 * 1024)]);
7288
+ * ```
7289
+ */
7290
+ function hubMaxFileSize(maxBytes) {
7291
+ return (control) => {
7292
+ const offending = toFileArray(control.value).filter((file) => file.size > maxBytes);
7293
+ if (offending.length === 0) {
7294
+ return null;
7295
+ }
7296
+ return {
7297
+ fileMaxSize: {
7298
+ max: maxBytes,
7299
+ actual: Math.max(...offending.map((file) => file.size)),
7300
+ files: offending.map((file) => file.name)
7301
+ }
7302
+ };
7303
+ };
7304
+ }
7305
+
7306
+ /**
7307
+ * Validates that no file held by the control falls below a minimum size.
7308
+ *
7309
+ * Useful to reject empty or truncated uploads (a 0-byte file is a common symptom of a failed export).
7310
+ * Sets the `fileMinSize` error with `{ min, actual, files }`, where `actual` is the size of the
7311
+ * smallest offending file. An empty control is valid.
7312
+ *
7313
+ * @param minBytes - The minimum size per file, in bytes.
7314
+ * @returns A validator function for a file-valued control.
7315
+ *
7316
+ * @example
7317
+ * ```ts
7318
+ * new FormControl<File | null>(null, [hubMinFileSize(1)]);
7319
+ * ```
7320
+ */
7321
+ function hubMinFileSize(minBytes) {
7322
+ return (control) => {
7323
+ const offending = toFileArray(control.value).filter((file) => file.size < minBytes);
7324
+ if (offending.length === 0) {
7325
+ return null;
7326
+ }
7327
+ return {
7328
+ fileMinSize: {
7329
+ min: minBytes,
7330
+ actual: Math.min(...offending.map((file) => file.size)),
7331
+ files: offending.map((file) => file.name)
7332
+ }
7333
+ };
7334
+ };
7335
+ }
7336
+
7337
+ /**
7338
+ * Validates that the combined size of every file held by the control stays within a budget.
7339
+ *
7340
+ * This is the constraint a request-body limit actually enforces, and it is not implied by a
7341
+ * per-file maximum: ten files of 2 MB pass `hubMaxFileSize(5 MB)` and still blow a 10 MB request.
7342
+ *
7343
+ * Sets the `fileMaxTotalSize` error with `{ max, actual }`. An empty control is valid.
7344
+ *
7345
+ * @param maxBytes - The maximum combined size, in bytes.
7346
+ * @returns A validator function for a file-valued control.
7347
+ *
7348
+ * @example
7349
+ * ```ts
7350
+ * new FormControl<File[]>([], [hubMaxTotalSize(20 * 1024 * 1024)]);
7351
+ * ```
7352
+ */
7353
+ function hubMaxTotalSize(maxBytes) {
7354
+ return (control) => {
7355
+ const files = toFileArray(control.value);
7356
+ if (files.length === 0) {
7357
+ return null;
7358
+ }
7359
+ const total = files.reduce((sum, file) => sum + file.size, 0);
7360
+ return total > maxBytes ? { fileMaxTotalSize: { max: maxBytes, actual: total } } : null;
7361
+ };
7362
+ }
7363
+
7364
+ /**
7365
+ * Validates that the control holds at most `max` files.
7366
+ *
7367
+ * Sets the `fileMaxFiles` error with `{ max, actual }`. An empty control is valid.
7368
+ *
7369
+ * @param max - The maximum number of files.
7370
+ * @returns A validator function for a file-valued control.
7371
+ *
7372
+ * @example
7373
+ * ```ts
7374
+ * new FormControl<File[]>([], [hubMaxFiles(3)]);
7375
+ * ```
7376
+ */
7377
+ function hubMaxFiles(max) {
7378
+ return (control) => {
7379
+ const count = toFileArray(control.value).length;
7380
+ return count > max ? { fileMaxFiles: { max, actual: count } } : null;
7381
+ };
7382
+ }
7383
+
7384
+ /**
7385
+ * Validates that the control holds at least `min` files.
7386
+ *
7387
+ * Unlike the other file validators, this one **does** reject an empty control: holding zero files
7388
+ * is precisely what a minimum forbids. It therefore subsumes `Validators.required`, and adding both
7389
+ * only produces two errors for the same cause.
7390
+ *
7391
+ * Sets the `fileMinFiles` error with `{ min, actual }`.
7392
+ *
7393
+ * @param min - The minimum number of files.
7394
+ * @returns A validator function for a file-valued control.
7395
+ *
7396
+ * @example
7397
+ * ```ts
7398
+ * new FormControl<File[]>([], [hubMinFiles(2)]);
7399
+ * ```
7400
+ */
7401
+ function hubMinFiles(min) {
7402
+ return (control) => {
7403
+ const count = toFileArray(control.value).length;
7404
+ return count < min ? { fileMinFiles: { min, actual: count } } : null;
7405
+ };
7406
+ }
7407
+
6296
7408
  /**
6297
7409
  * Ready-made {@link HubFormControlAdapter} backed by `HubInputComponent` /
6298
7410
  * `HubSelectComponent`.
@@ -6361,5 +7473,5 @@ const hubFormControlAdapter = {
6361
7473
  * Generated bundle index. Do not edit.
6362
7474
  */
6363
7475
 
6364
- export { FormTextTypes, HUB_FORMS_CONFIG, HubAutoresizeDirective, HubDatepickerComponent, HubFieldControl, HubFieldsetComponent, HubFormComponent, HubFormControl, HubFormTextDirective, HubGroupControl, HubInputComponent, HubInputFormats, HubInputPrefixDirective, HubInputSuffixDirective, HubInvertColorPipe, HubJoinButLastPipe, HubLabelTypes, HubLegendComponent, HubLegendDirective, HubMapPipe, HubOtpInputComponent, HubSafeUrlPipe, HubSegmentedComponent, HubSelectComponent, HubSelectFormats, HubSliderComponent, HubSnakeUpperPipe, HubTextareaComponent, HubUcfirstPipe, HubValidationErrorDirective, NgClearButtonTemplateDirective, NgFooterTemplateDirective, NgHeaderTemplateDirective, NgLabelTemplateDirective, NgLoadingSpinnerTemplateDirective, NgLoadingTextTemplateDirective, NgMultiLabelTemplateDirective, NgNotFoundTemplateDirective, NgOptgroupTemplateDirective, NgOptionComponent, NgOptionTemplateDirective, NgSelectConfig, NgTagTemplateDirective, NgTypeToSearchTemplateDirective, applyMask, areEqual, camelToSnakeUpper, controlHasMinOrMaxValidator, defaultHubDatepickerConfig, defaultHubDatepickerLabels, defaultHubFormsConfig, defaultInvalidFeedback, get, getActiveElement, getMinOrMaxValueFromValidator, hubAreEqual, hubFormControlAdapter, isDefined$1 as isDefined, isMaskActive, isString, joinButLast, provideHubForms, runInZone, uuid };
7476
+ export { FormTextTypes, HUB_FILE_UPLOADER, HUB_FORMS_CONFIG, HubAutoresizeDirective, HubDatepickerComponent, HubFieldControl, HubFieldsetComponent, HubFileIconDirective, HubFileInputComponent, HubFilePreviewDirective, HubFormComponent, HubFormControl, HubFormTextDirective, HubGroupControl, HubInputComponent, HubInputFormats, HubInputPrefixDirective, HubInputSuffixDirective, HubInvertColorPipe, HubJoinButLastPipe, HubLabelTypes, HubLegendComponent, HubLegendDirective, HubMapPipe, HubOtpInputComponent, HubSafeUrlPipe, HubSegmentedComponent, HubSelectComponent, HubSelectFormats, HubSliderComponent, HubSnakeUpperPipe, HubTextareaComponent, HubUcfirstPipe, HubValidationErrorDirective, NgClearButtonTemplateDirective, NgFooterTemplateDirective, NgHeaderTemplateDirective, NgLabelTemplateDirective, NgLoadingSpinnerTemplateDirective, NgLoadingTextTemplateDirective, NgMultiLabelTemplateDirective, NgNotFoundTemplateDirective, NgOptgroupTemplateDirective, NgOptionComponent, NgOptionTemplateDirective, NgSelectConfig, NgTagTemplateDirective, NgTypeToSearchTemplateDirective, applyMask, areEqual, camelToSnakeUpper, controlHasMinOrMaxValidator, defaultHubDatepickerConfig, defaultHubDatepickerLabels, defaultHubFileInputLabels, defaultHubFormsConfig, defaultInvalidFeedback, fileKey, formatFileSize, get, getActiveElement, getMinOrMaxValueFromValidator, hubAcceptedFiles, hubAreEqual, hubFormControlAdapter, hubMaxFileSize, hubMaxFiles, hubMaxTotalSize, hubMinFileSize, hubMinFiles, isDefined$1 as isDefined, isMaskActive, isString, joinButLast, matchesAccept, provideHubFileUploader, provideHubForms, runInZone, toFileArray, uuid };
6365
7477
  //# sourceMappingURL=ng-hub-ui-forms.mjs.map