@truenas/ui-components 0.3.12 → 0.3.14

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.
@@ -2022,8 +2022,14 @@ class TnBannerComponent {
2022
2022
  actionContent = contentChildren(TnBannerActionDirective, ...(ngDevMode ? [{ debugName: "actionContent" }] : []));
2023
2023
  /** Signal indicating whether action content has been projected */
2024
2024
  hasAction = computed(() => this.actionContent().length > 0, ...(ngDevMode ? [{ debugName: "hasAction" }] : []));
2025
+ /**
2026
+ * Signal indicating whether the built-in structured layout (icon + heading/message)
2027
+ * should be rendered. When neither heading nor message is provided, the banner
2028
+ * instead renders arbitrary projected content from the default slot.
2029
+ */
2030
+ hasStructuredContent = computed(() => !!this.heading() || !!this.message(), ...(ngDevMode ? [{ debugName: "hasStructuredContent" }] : []));
2025
2031
  // Signal-based inputs (modern Angular 19+)
2026
- heading = input.required(...(ngDevMode ? [{ debugName: "heading" }] : []));
2032
+ heading = input(undefined, ...(ngDevMode ? [{ debugName: "heading" }] : []));
2027
2033
  message = input(undefined, ...(ngDevMode ? [{ debugName: "message" }] : []));
2028
2034
  type = input('info', ...(ngDevMode ? [{ debugName: "type" }] : []));
2029
2035
  bordered = input(false, ...(ngDevMode ? [{ debugName: "bordered" }] : []));
@@ -2082,12 +2088,12 @@ class TnBannerComponent {
2082
2088
  return result;
2083
2089
  }, ...(ngDevMode ? [{ debugName: "classes" }] : []));
2084
2090
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnBannerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2085
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnBannerComponent, isStandalone: true, selector: "tn-banner", inputs: { heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: true, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "actionContent", predicate: TnBannerActionDirective, isSignal: true }], ngImport: i0, template: "<div\n aria-live=\"polite\"\n [ngClass]=\"classes()\"\n [attr.role]=\"ariaRole()\"\n>\n <div class=\"tn-banner__main\">\n <div class=\"tn-banner__icon\">\n <tn-icon\n library=\"mdi\"\n size=\"md\"\n aria-hidden=\"true\"\n [name]=\"iconName()\"\n />\n </div>\n\n <div class=\"tn-banner__content\">\n <div class=\"tn-banner__heading\">\n {{ heading() }}\n </div>\n\n @if (message()) {\n <div class=\"tn-banner__message\">\n {{ message() }}\n </div>\n }\n </div>\n </div>\n\n @if (hasAction()) {\n <div class=\"tn-banner__action\">\n <ng-content select=\"[tnBannerAction]\" />\n </div>\n }\n</div>\n", styles: [".tn-banner{display:grid;gap:8px;align-items:center;padding:16px;border-radius:6px;border-left:4px solid;transition:opacity .2s ease}@media(prefers-reduced-motion:reduce){.tn-banner{transition:none}}.tn-banner--info{border-left-color:var(--tn-info, #3b82f6);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--info .tn-banner__heading,.tn-banner--info .tn-banner__icon{color:var(--tn-info, #3b82f6)}.tn-banner--warning{border-left-color:var(--tn-warning, #f59e0b);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--warning .tn-banner__heading,.tn-banner--warning .tn-banner__icon{color:var(--tn-warning, #f59e0b)}.tn-banner--error{border-left-color:var(--tn-error, #ef4444);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--error .tn-banner__heading,.tn-banner--error .tn-banner__icon{color:var(--tn-error, #ef4444)}.tn-banner--success{border-left-color:var(--tn-success, #10b981);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--success .tn-banner__heading,.tn-banner--success .tn-banner__icon{color:var(--tn-success, #10b981)}.tn-banner--bordered{border-top:1px solid var(--tn-lines, #404040);border-right:1px solid var(--tn-lines, #404040);border-bottom:1px solid var(--tn-lines, #404040)}.tn-banner__main{display:flex;align-items:flex-start;gap:12px;flex:1;min-width:0}.tn-banner__icon{flex-shrink:0;display:flex;align-items:center;justify-content:center;margin-top:2px}.tn-banner__content{flex:1;min-width:0}.tn-banner__heading{font-size:1rem;font-weight:600;line-height:1.5;margin:0}.tn-banner__message{font-size:1rem;color:var(--tn-fg2, #6b7280);line-height:1.5;margin-top:4px}.tn-banner__action{display:flex;align-items:center;gap:8px 16px;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }] });
2091
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnBannerComponent, isStandalone: true, selector: "tn-banner", inputs: { heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "actionContent", predicate: TnBannerActionDirective, isSignal: true }], ngImport: i0, template: "<div\n aria-live=\"polite\"\n [ngClass]=\"classes()\"\n [attr.role]=\"ariaRole()\"\n>\n @if (hasStructuredContent()) {\n <div class=\"tn-banner__main\">\n <div class=\"tn-banner__icon\">\n <tn-icon\n library=\"mdi\"\n size=\"md\"\n aria-hidden=\"true\"\n [name]=\"iconName()\"\n />\n </div>\n\n <div class=\"tn-banner__content\">\n @if (heading()) {\n <div class=\"tn-banner__heading\">\n {{ heading() }}\n </div>\n }\n\n @if (message()) {\n <div class=\"tn-banner__message\">\n {{ message() }}\n </div>\n }\n </div>\n </div>\n } @else {\n <div class=\"tn-banner__main\">\n <ng-content />\n </div>\n }\n\n @if (hasAction()) {\n <div class=\"tn-banner__action\">\n <ng-content select=\"[tnBannerAction]\" />\n </div>\n }\n</div>\n", styles: [".tn-banner{display:grid;gap:8px;align-items:center;padding:16px;border-radius:6px;border-left:4px solid;transition:opacity .2s ease}@media(prefers-reduced-motion:reduce){.tn-banner{transition:none}}.tn-banner--info{border-left-color:var(--tn-info, #3b82f6);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--info .tn-banner__heading,.tn-banner--info .tn-banner__icon{color:var(--tn-info, #3b82f6)}.tn-banner--warning{border-left-color:var(--tn-warning, #f59e0b);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--warning .tn-banner__heading,.tn-banner--warning .tn-banner__icon{color:var(--tn-warning, #f59e0b)}.tn-banner--error{border-left-color:var(--tn-error, #ef4444);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--error .tn-banner__heading,.tn-banner--error .tn-banner__icon{color:var(--tn-error, #ef4444)}.tn-banner--success{border-left-color:var(--tn-success, #10b981);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--success .tn-banner__heading,.tn-banner--success .tn-banner__icon{color:var(--tn-success, #10b981)}.tn-banner--bordered{border-top:1px solid var(--tn-lines, #404040);border-right:1px solid var(--tn-lines, #404040);border-bottom:1px solid var(--tn-lines, #404040)}.tn-banner__main{display:flex;align-items:flex-start;gap:12px;flex:1;min-width:0}.tn-banner__icon{flex-shrink:0;display:flex;align-items:center;justify-content:center;margin-top:2px}.tn-banner__content{flex:1;min-width:0}.tn-banner__heading{font-size:1rem;font-weight:600;line-height:1.5;margin:0}.tn-banner__message{font-size:1rem;color:var(--tn-fg2, #6b7280);line-height:1.5;margin-top:4px}.tn-banner__action{display:flex;align-items:center;gap:8px 16px;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }] });
2086
2092
  }
2087
2093
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnBannerComponent, decorators: [{
2088
2094
  type: Component,
2089
- args: [{ selector: 'tn-banner', standalone: true, imports: [CommonModule, TnIconComponent], template: "<div\n aria-live=\"polite\"\n [ngClass]=\"classes()\"\n [attr.role]=\"ariaRole()\"\n>\n <div class=\"tn-banner__main\">\n <div class=\"tn-banner__icon\">\n <tn-icon\n library=\"mdi\"\n size=\"md\"\n aria-hidden=\"true\"\n [name]=\"iconName()\"\n />\n </div>\n\n <div class=\"tn-banner__content\">\n <div class=\"tn-banner__heading\">\n {{ heading() }}\n </div>\n\n @if (message()) {\n <div class=\"tn-banner__message\">\n {{ message() }}\n </div>\n }\n </div>\n </div>\n\n @if (hasAction()) {\n <div class=\"tn-banner__action\">\n <ng-content select=\"[tnBannerAction]\" />\n </div>\n }\n</div>\n", styles: [".tn-banner{display:grid;gap:8px;align-items:center;padding:16px;border-radius:6px;border-left:4px solid;transition:opacity .2s ease}@media(prefers-reduced-motion:reduce){.tn-banner{transition:none}}.tn-banner--info{border-left-color:var(--tn-info, #3b82f6);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--info .tn-banner__heading,.tn-banner--info .tn-banner__icon{color:var(--tn-info, #3b82f6)}.tn-banner--warning{border-left-color:var(--tn-warning, #f59e0b);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--warning .tn-banner__heading,.tn-banner--warning .tn-banner__icon{color:var(--tn-warning, #f59e0b)}.tn-banner--error{border-left-color:var(--tn-error, #ef4444);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--error .tn-banner__heading,.tn-banner--error .tn-banner__icon{color:var(--tn-error, #ef4444)}.tn-banner--success{border-left-color:var(--tn-success, #10b981);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--success .tn-banner__heading,.tn-banner--success .tn-banner__icon{color:var(--tn-success, #10b981)}.tn-banner--bordered{border-top:1px solid var(--tn-lines, #404040);border-right:1px solid var(--tn-lines, #404040);border-bottom:1px solid var(--tn-lines, #404040)}.tn-banner__main{display:flex;align-items:flex-start;gap:12px;flex:1;min-width:0}.tn-banner__icon{flex-shrink:0;display:flex;align-items:center;justify-content:center;margin-top:2px}.tn-banner__content{flex:1;min-width:0}.tn-banner__heading{font-size:1rem;font-weight:600;line-height:1.5;margin:0}.tn-banner__message{font-size:1rem;color:var(--tn-fg2, #6b7280);line-height:1.5;margin-top:4px}.tn-banner__action{display:flex;align-items:center;gap:8px 16px;justify-content:flex-end}\n"] }]
2090
- }], ctorParameters: () => [], propDecorators: { actionContent: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnBannerActionDirective), { isSignal: true }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }] } });
2095
+ args: [{ selector: 'tn-banner', standalone: true, imports: [CommonModule, TnIconComponent], template: "<div\n aria-live=\"polite\"\n [ngClass]=\"classes()\"\n [attr.role]=\"ariaRole()\"\n>\n @if (hasStructuredContent()) {\n <div class=\"tn-banner__main\">\n <div class=\"tn-banner__icon\">\n <tn-icon\n library=\"mdi\"\n size=\"md\"\n aria-hidden=\"true\"\n [name]=\"iconName()\"\n />\n </div>\n\n <div class=\"tn-banner__content\">\n @if (heading()) {\n <div class=\"tn-banner__heading\">\n {{ heading() }}\n </div>\n }\n\n @if (message()) {\n <div class=\"tn-banner__message\">\n {{ message() }}\n </div>\n }\n </div>\n </div>\n } @else {\n <div class=\"tn-banner__main\">\n <ng-content />\n </div>\n }\n\n @if (hasAction()) {\n <div class=\"tn-banner__action\">\n <ng-content select=\"[tnBannerAction]\" />\n </div>\n }\n</div>\n", styles: [".tn-banner{display:grid;gap:8px;align-items:center;padding:16px;border-radius:6px;border-left:4px solid;transition:opacity .2s ease}@media(prefers-reduced-motion:reduce){.tn-banner{transition:none}}.tn-banner--info{border-left-color:var(--tn-info, #3b82f6);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--info .tn-banner__heading,.tn-banner--info .tn-banner__icon{color:var(--tn-info, #3b82f6)}.tn-banner--warning{border-left-color:var(--tn-warning, #f59e0b);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--warning .tn-banner__heading,.tn-banner--warning .tn-banner__icon{color:var(--tn-warning, #f59e0b)}.tn-banner--error{border-left-color:var(--tn-error, #ef4444);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--error .tn-banner__heading,.tn-banner--error .tn-banner__icon{color:var(--tn-error, #ef4444)}.tn-banner--success{border-left-color:var(--tn-success, #10b981);background-color:var(--tn-alt-bg1, #383838)}.tn-banner--success .tn-banner__heading,.tn-banner--success .tn-banner__icon{color:var(--tn-success, #10b981)}.tn-banner--bordered{border-top:1px solid var(--tn-lines, #404040);border-right:1px solid var(--tn-lines, #404040);border-bottom:1px solid var(--tn-lines, #404040)}.tn-banner__main{display:flex;align-items:flex-start;gap:12px;flex:1;min-width:0}.tn-banner__icon{flex-shrink:0;display:flex;align-items:center;justify-content:center;margin-top:2px}.tn-banner__content{flex:1;min-width:0}.tn-banner__heading{font-size:1rem;font-weight:600;line-height:1.5;margin:0}.tn-banner__message{font-size:1rem;color:var(--tn-fg2, #6b7280);line-height:1.5;margin-top:4px}.tn-banner__action{display:flex;align-items:center;gap:8px 16px;justify-content:flex-end}\n"] }]
2096
+ }], ctorParameters: () => [], propDecorators: { actionContent: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnBannerActionDirective), { isSignal: true }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }] } });
2091
2097
 
2092
2098
  /**
2093
2099
  * Harness for interacting with tn-banner in tests.
@@ -4199,6 +4205,262 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
4199
4205
  }]
4200
4206
  }], ctorParameters: () => [] });
4201
4207
 
4208
+ /**
4209
+ * A minimal file-selection form control. Renders a styled "Choose File" button
4210
+ * that opens the native file dialog and exposes the picked `File`(s) both as a
4211
+ * `(change)` output and as the value of an Angular form control.
4212
+ *
4213
+ * The selected file name(s) are shown next to the button (toggle with
4214
+ * `showFileName`). Use inside a `<tn-form-field>` to get a label, required
4215
+ * asterisk and help tooltip.
4216
+ *
4217
+ * Like every native file input, the browser forbids programmatically setting
4218
+ * the chosen files for security reasons. Writing a non-empty value via a form
4219
+ * control therefore only updates the displayed name; writing `null` (or an empty
4220
+ * array) clears the control. User selection is the only way to populate real
4221
+ * `File` objects.
4222
+ *
4223
+ * @example
4224
+ * ```html
4225
+ * <tn-form-field label="Update File" [required]="true" tooltip="Upload a .tar file">
4226
+ * <tn-file-input accept=".tar" formControlName="update" (change)="onFile($event)" />
4227
+ * </tn-form-field>
4228
+ * ```
4229
+ */
4230
+ class TnFileInputComponent {
4231
+ fileInputEl = viewChild.required('fileInput');
4232
+ /** Label rendered inside the trigger button. */
4233
+ buttonLabel = input('Choose File', ...(ngDevMode ? [{ debugName: "buttonLabel" }] : []));
4234
+ /**
4235
+ * Native `accept` attribute forwarded to the file input — a comma-separated
4236
+ * list of extensions and/or MIME types (e.g. `".tar,.txt"`, `"image/*"`).
4237
+ */
4238
+ accept = input(undefined, ...(ngDevMode ? [{ debugName: "accept" }] : []));
4239
+ /** Allow selecting more than one file. The value becomes a `File[]`. */
4240
+ multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
4241
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
4242
+ /** Whether to render the selected file name(s) beside the button. */
4243
+ showFileName = input(true, ...(ngDevMode ? [{ debugName: "showFileName" }] : []));
4244
+ /** Text shown beside the button when no file is selected. */
4245
+ noFileText = input('No file chosen', ...(ngDevMode ? [{ debugName: "noFileText" }] : []));
4246
+ /**
4247
+ * Accessible label for the trigger button, forwarded to `tn-button`. Set this
4248
+ * when the visible "Choose File" text alone doesn't convey what is being
4249
+ * uploaded — e.g. pass the field label so a screen reader announces
4250
+ * "Update File" rather than just "Choose File". (`tn-form-field`'s `<label>`
4251
+ * is not programmatically associated with the projected control.)
4252
+ */
4253
+ ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
4254
+ /**
4255
+ * Semantic test-id base. Rendered under whichever attribute name is configured
4256
+ * via `TN_TEST_ATTR` (default `data-testid`).
4257
+ */
4258
+ testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
4259
+ /**
4260
+ * Emitted whenever the selection changes. `null` when the input is cleared.
4261
+ * Named `selectionChange` (not `change`) to avoid colliding with the native
4262
+ * `change` event bubbling from the inner file input.
4263
+ */
4264
+ selectionChange = output();
4265
+ // Selected files, kept for display and for the form value.
4266
+ selectedFiles = signal([], ...(ngDevMode ? [{ debugName: "selectedFiles" }] : []));
4267
+ formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
4268
+ isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
4269
+ fileNames = computed(() => this.selectedFiles().map(file => file.name).join(', '), ...(ngDevMode ? [{ debugName: "fileNames" }] : []));
4270
+ hasFiles = computed(() => this.selectedFiles().length > 0, ...(ngDevMode ? [{ debugName: "hasFiles" }] : []));
4271
+ onChange = (_value) => { };
4272
+ onTouched = () => { };
4273
+ // ControlValueAccessor
4274
+ writeValue(value) {
4275
+ // Native file inputs cannot be populated programmatically; we only mirror
4276
+ // the value for display and treat null/empty as a clear of the control.
4277
+ if (value == null || (Array.isArray(value) && value.length === 0)) {
4278
+ this.selectedFiles.set([]);
4279
+ this.fileInputEl().nativeElement.value = '';
4280
+ }
4281
+ else {
4282
+ this.selectedFiles.set(Array.isArray(value) ? value : [value]);
4283
+ }
4284
+ }
4285
+ registerOnChange(fn) {
4286
+ this.onChange = fn;
4287
+ }
4288
+ registerOnTouched(fn) {
4289
+ this.onTouched = fn;
4290
+ }
4291
+ setDisabledState(isDisabled) {
4292
+ this.formDisabled.set(isDisabled);
4293
+ }
4294
+ openFileDialog() {
4295
+ if (this.isDisabled()) {
4296
+ return;
4297
+ }
4298
+ // Clear the native value first so re-picking the same file still fires
4299
+ // `change` (browsers suppress it when the selection is identical).
4300
+ this.fileInputEl().nativeElement.value = '';
4301
+ this.fileInputEl().nativeElement.click();
4302
+ }
4303
+ /** Marks the control as touched when focus leaves it (e.g. for validation). */
4304
+ onBlur() {
4305
+ this.onTouched();
4306
+ }
4307
+ onFilesSelected(event) {
4308
+ const target = event.target;
4309
+ const files = target.files ? Array.from(target.files) : [];
4310
+ this.selectedFiles.set(files);
4311
+ const value = this.toValue(files);
4312
+ this.onChange(value);
4313
+ this.onTouched();
4314
+ this.selectionChange.emit(value);
4315
+ }
4316
+ toValue(files) {
4317
+ if (files.length === 0) {
4318
+ return null;
4319
+ }
4320
+ return this.multiple() ? files : files[0];
4321
+ }
4322
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4323
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFileInputComponent, isStandalone: true, selector: "tn-file-input", inputs: { buttonLabel: { classPropertyName: "buttonLabel", publicName: "buttonLabel", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showFileName: { classPropertyName: "showFileName", publicName: "showFileName", isSignal: true, isRequired: false, transformFunction: null }, noFileText: { classPropertyName: "noFileText", publicName: "noFileText", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { classAttribute: "tn-file-input" }, providers: [
4324
+ {
4325
+ provide: NG_VALUE_ACCESSOR,
4326
+ useExisting: forwardRef(() => TnFileInputComponent),
4327
+ multi: true
4328
+ }
4329
+ ], viewQueries: [{ propertyName: "fileInputEl", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-file-input__container\" tnTestIdType=\"file-input\" [tnTestId]=\"testId()\" (focusout)=\"onBlur()\">\n <input\n #fileInput\n type=\"file\"\n class=\"tn-file-input__native\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onFilesSelected($event)\">\n\n <tn-button\n class=\"tn-file-input__button\"\n color=\"default\"\n [label]=\"buttonLabel()\"\n [disabled]=\"isDisabled()\"\n [ariaLabel]=\"ariaLabel()\"\n (onClick)=\"openFileDialog()\" />\n\n @if (showFileName()) {\n <span class=\"tn-file-input__filename\" [class.tn-file-input__filename--empty]=\"!hasFiles()\">\n {{ hasFiles() ? fileNames() : noFileText() }}\n </span>\n }\n\n <!--\n Always-present live region so screen readers are told what was picked even\n when the visible file name is suppressed (showFileName=false).\n -->\n <span class=\"tn-file-input__announcer\" aria-live=\"polite\">\n {{ hasFiles() ? fileNames() : '' }}\n </span>\n</div>\n", styles: [":host{display:inline-block}.tn-file-input__container{display:inline-flex;align-items:center;gap:12px}.tn-file-input__native,.tn-file-input__announcer{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.tn-file-input__button{flex-shrink:0}.tn-file-input__filename{font-size:14px;color:var(--tn-fg1);max-width:30ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-file-input__filename--empty{color:var(--tn-fg2)}\n"], dependencies: [{ kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
4330
+ }
4331
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFileInputComponent, decorators: [{
4332
+ type: Component,
4333
+ args: [{ selector: 'tn-file-input', standalone: true, imports: [TnButtonComponent, TnTestIdDirective], providers: [
4334
+ {
4335
+ provide: NG_VALUE_ACCESSOR,
4336
+ useExisting: forwardRef(() => TnFileInputComponent),
4337
+ multi: true
4338
+ }
4339
+ ], host: {
4340
+ 'class': 'tn-file-input'
4341
+ }, template: "<div class=\"tn-file-input__container\" tnTestIdType=\"file-input\" [tnTestId]=\"testId()\" (focusout)=\"onBlur()\">\n <input\n #fileInput\n type=\"file\"\n class=\"tn-file-input__native\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onFilesSelected($event)\">\n\n <tn-button\n class=\"tn-file-input__button\"\n color=\"default\"\n [label]=\"buttonLabel()\"\n [disabled]=\"isDisabled()\"\n [ariaLabel]=\"ariaLabel()\"\n (onClick)=\"openFileDialog()\" />\n\n @if (showFileName()) {\n <span class=\"tn-file-input__filename\" [class.tn-file-input__filename--empty]=\"!hasFiles()\">\n {{ hasFiles() ? fileNames() : noFileText() }}\n </span>\n }\n\n <!--\n Always-present live region so screen readers are told what was picked even\n when the visible file name is suppressed (showFileName=false).\n -->\n <span class=\"tn-file-input__announcer\" aria-live=\"polite\">\n {{ hasFiles() ? fileNames() : '' }}\n </span>\n</div>\n", styles: [":host{display:inline-block}.tn-file-input__container{display:inline-flex;align-items:center;gap:12px}.tn-file-input__native,.tn-file-input__announcer{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.tn-file-input__button{flex-shrink:0}.tn-file-input__filename{font-size:14px;color:var(--tn-fg1);max-width:30ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-file-input__filename--empty{color:var(--tn-fg2)}\n"] }]
4342
+ }], propDecorators: { fileInputEl: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], buttonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonLabel", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showFileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFileName", required: false }] }], noFileText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noFileText", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
4343
+
4344
+ /**
4345
+ * Harness for interacting with `tn-file-input` in tests.
4346
+ *
4347
+ * @example
4348
+ * ```typescript
4349
+ * const fileInput = await loader.getHarness(TnFileInputHarness.with({ testId: 'update' }));
4350
+ * expect(await fileInput.getButtonText()).toBe('Choose File');
4351
+ * expect(await fileInput.hasFile()).toBe(false);
4352
+ * expect(await fileInput.getFileName()).toBe('No file chosen');
4353
+ * ```
4354
+ */
4355
+ class TnFileInputHarness extends ComponentHarness {
4356
+ /** The selector for the host element of a `TnFileInputComponent` instance. */
4357
+ static hostSelector = 'tn-file-input';
4358
+ // The trigger is a `tn-button`; target its rendered `<button>` element.
4359
+ _button = this.locatorFor('.tn-file-input__button .storybook-button');
4360
+ _native = this.locatorFor('input[type="file"]');
4361
+ _filename = this.locatorForOptional('.tn-file-input__filename');
4362
+ /**
4363
+ * Gets a `HarnessPredicate` to search for a file input with specific attributes.
4364
+ *
4365
+ * @param options Options for filtering which instances are considered a match.
4366
+ * @returns A `HarnessPredicate` configured with the given options.
4367
+ *
4368
+ * @example
4369
+ * ```typescript
4370
+ * const fileInput = await loader.getHarness(TnFileInputHarness.with({ testId: 'update' }));
4371
+ * ```
4372
+ */
4373
+ static with(options = {}) {
4374
+ return new HarnessPredicate(TnFileInputHarness, options)
4375
+ .addOption('buttonText', options.buttonText, (harness, text) => HarnessPredicate.stringMatches(harness.getButtonText(), text))
4376
+ .addOption('testId', options.testId, async (harness, testId) => (await harness.getTestId()) === testId);
4377
+ }
4378
+ /**
4379
+ * Gets the trigger button's text.
4380
+ *
4381
+ * @returns Promise resolving to the button label.
4382
+ *
4383
+ * @example
4384
+ * ```typescript
4385
+ * expect(await fileInput.getButtonText()).toBe('Choose File');
4386
+ * ```
4387
+ */
4388
+ async getButtonText() {
4389
+ return (await (await this._button()).text()).trim();
4390
+ }
4391
+ /**
4392
+ * Gets the displayed file-name text (or the empty-state text).
4393
+ *
4394
+ * @returns Promise resolving to the text, or null when the name is hidden.
4395
+ *
4396
+ * @example
4397
+ * ```typescript
4398
+ * expect(await fileInput.getFileName()).toBe('No file chosen');
4399
+ * ```
4400
+ */
4401
+ async getFileName() {
4402
+ const filename = await this._filename();
4403
+ return filename ? (await filename.text()).trim() : null;
4404
+ }
4405
+ /**
4406
+ * Whether a file is currently selected. A native file input reports an empty
4407
+ * `value` until the user picks a file, so this reflects real user selection.
4408
+ *
4409
+ * @returns Promise resolving to true when a file has been chosen.
4410
+ *
4411
+ * @example
4412
+ * ```typescript
4413
+ * expect(await fileInput.hasFile()).toBe(false);
4414
+ * ```
4415
+ */
4416
+ async hasFile() {
4417
+ const native = await this._native();
4418
+ const value = (await native.getProperty('value')) ?? '';
4419
+ return value !== '';
4420
+ }
4421
+ /**
4422
+ * Whether the control is disabled.
4423
+ *
4424
+ * @returns Promise resolving to true if the button is disabled.
4425
+ *
4426
+ * @example
4427
+ * ```typescript
4428
+ * expect(await fileInput.isDisabled()).toBe(true);
4429
+ * ```
4430
+ */
4431
+ async isDisabled() {
4432
+ const button = await this._button();
4433
+ return (await button.getProperty('disabled')) ?? false;
4434
+ }
4435
+ /**
4436
+ * Opens the native file dialog by clicking the trigger button. The browser
4437
+ * does not let tests pick a real file, so this is mainly useful for asserting
4438
+ * click handling and focus behaviour.
4439
+ *
4440
+ * @example
4441
+ * ```typescript
4442
+ * await fileInput.open();
4443
+ * ```
4444
+ */
4445
+ async open() {
4446
+ await (await this._button()).click();
4447
+ }
4448
+ /**
4449
+ * Gets the test-id attribute value from the container.
4450
+ *
4451
+ * @returns Promise resolving to the test-id, or null when unset.
4452
+ *
4453
+ * @example
4454
+ * ```typescript
4455
+ * expect(await fileInput.getTestId()).toBe('file-input-update');
4456
+ * ```
4457
+ */
4458
+ async getTestId() {
4459
+ const container = await this.locatorFor('.tn-file-input__container')();
4460
+ return (await container.getAttribute('data-testid')) ?? (await container.getAttribute('data-test'));
4461
+ }
4462
+ }
4463
+
4202
4464
  /**
4203
4465
  * Strips lightweight label markup (**bold**, *italic*, `code`), returning
4204
4466
  * plain text for attribute contexts such as aria-label and title.
@@ -17345,5 +17607,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
17345
17607
  * Generated bundle index. Do not edit.
17346
17608
  */
17347
17609
 
17348
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, composeTestId, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, formatSize, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
17610
+ export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, composeTestId, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, formatSize, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
17349
17611
  //# sourceMappingURL=truenas-ui-components.mjs.map