@yuuvis/client-framework 3.10.0 → 3.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import * as i1 from '@angular/common';
2
2
  import { CommonModule, NgTemplateOutlet } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { inject, signal, computed, input, effect, Component, ChangeDetectionStrategy, Injectable, viewChild, linkedSignal, untracked, output, viewChildren, NgModule, Pipe } from '@angular/core';
4
+ import { inject, signal, computed, input, effect, Component, ChangeDetectionStrategy, Injectable, viewChild, linkedSignal, afterNextRender, untracked, output, viewChildren, NgModule, Pipe } from '@angular/core';
5
5
  import { takeUntilDestroyed, toSignal, rxResource } from '@angular/core/rxjs-interop';
6
6
  import { ReactiveFormsModule } from '@angular/forms';
7
7
  import * as i2 from '@angular/material/icon';
@@ -315,8 +315,32 @@ class ObjectMetadataSectionComponent {
315
315
  this.expandedInput = input(false, { ...(ngDevMode ? { debugName: "expandedInput" } : /* istanbul ignore next */ {}), alias: 'expanded' });
316
316
  this.elementExtensions = input([], ...(ngDevMode ? [{ debugName: "elementExtensions" }] : /* istanbul ignore next */ []));
317
317
  this.readonly = input(...(ngDevMode ? [undefined, { debugName: "readonly" }] : /* istanbul ignore next */ []));
318
+ /**
319
+ * When `true`, the section's expand/collapse feature is removed: the content is
320
+ * always visible, cannot be collapsed, and the accordion toggle (arrow) is hidden.
321
+ */
322
+ this.skipExpand = input(...(ngDevMode ? [undefined, { debugName: "skipExpand" }] : /* istanbul ignore next */ []));
323
+ /**
324
+ * When `true`, the section header (title, icon and toggle) is not rendered while the
325
+ * content region is still shown. Typically combined with {@link skipExpand} to display
326
+ * a title-less, always-open section.
327
+ */
328
+ this.skipTitle = input(...(ngDevMode ? [undefined, { debugName: "skipTitle" }] : /* istanbul ignore next */ []));
318
329
  this.expanded = linkedSignal(this.expandedInput, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
330
+ /**
331
+ * Effective expanded state. When `skipExpand` is set the section is always visible
332
+ * and cannot be collapsed, so it is treated as permanently expanded.
333
+ */
334
+ this.isExpanded = computed(() => !!this.skipExpand() || this.expanded(), ...(ngDevMode ? [{ debugName: "isExpanded" }] : /* istanbul ignore next */ []));
319
335
  this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
336
+ /**
337
+ * Gate for the reveal/collapse transition. Stays `false` for the first render so a
338
+ * section that mounts already expanded (auto-expanded first section or {@link skipExpand})
339
+ * appears instantly without a slide. Enabled after the initial render so that only
340
+ * subsequent user-driven toggles animate.
341
+ */
342
+ this.animate = signal(false, ...(ngDevMode ? [{ debugName: "animate" }] : /* istanbul ignore next */ []));
343
+ this.#enableAnimateOnMount = afterNextRender(() => this.animate.set(true));
320
344
  // linkedSignal instead of plain signal so that formOptions resets to undefined whenever
321
345
  // the section input changes (e.g. a different object is loaded). This ensures the form
322
346
  // is reloaded with fresh data for the new section while still allowing .set() mutations
@@ -327,7 +351,7 @@ class ObjectMetadataSectionComponent {
327
351
  // Load the object form when the section is first expanded.
328
352
  // Don't use rxResource here because it messes with the state of the formOptions.
329
353
  const section = this.section();
330
- const expanded = this.expanded();
354
+ const expanded = this.isExpanded();
331
355
  // untracked() reads formOptions without adding it as a reactive dependency of this
332
356
  // effect (which would cause an infinite loop). The guard ensures we only fetch once
333
357
  // per section: subsequent expand cycles reuse the existing formOptions so that any
@@ -358,11 +382,36 @@ class ObjectMetadataSectionComponent {
358
382
  * classes when the default `yuv-object-form` (and thus `objectForm()`) is absent.
359
383
  */
360
384
  this.customStatus = signal(undefined, ...(ngDevMode ? [{ debugName: "customStatus" }] : /* istanbul ignore next */ []));
385
+ /**
386
+ * Settled status of the default `yuv-object-form`, captured from its `statusChanged`
387
+ * output. The host classes read this signal instead of the live `form.invalid`/
388
+ * `form.dirty` for two reasons:
389
+ * - it is only updated once the form has emitted a settled status (after the model
390
+ * is built and its values are patched), so the transient window where required
391
+ * controls are momentarily invalid never reaches the DOM (no red flash);
392
+ * - a signal value is stable within a change-detection cycle, so expanding a section
393
+ * mid-cycle no longer throws NG0100 (removing the consumer-side "defer expand by a
394
+ * tick" workaround).
395
+ */
396
+ this.formStatus = signal(undefined, ...(ngDevMode ? [{ debugName: "formStatus" }] : /* istanbul ignore next */ []));
397
+ /** Active status: the custom renderer's when present, otherwise the object-form's. */
398
+ this.#status = computed(() => (this.customTemplate() ? this.customStatus() : this.formStatus()), ...(ngDevMode ? [{ debugName: "#status" }] : /* istanbul ignore next */ []));
399
+ /**
400
+ * Reflect the invalid styling only once the form has been edited (dirty). This
401
+ * suppresses the load transient and follows the convention of not surfacing errors
402
+ * before the user interacts. A form that is genuinely invalid after user interaction
403
+ * still shows the red border/outline.
404
+ */
405
+ this.showInvalid = computed(() => {
406
+ const status = this.#status();
407
+ return !!status?.invalid && !!status?.dirty;
408
+ }, ...(ngDevMode ? [{ debugName: "showInvalid" }] : /* istanbul ignore next */ []));
409
+ this.showDirty = computed(() => !!this.#status()?.dirty, ...(ngDevMode ? [{ debugName: "showDirty" }] : /* istanbul ignore next */ []));
361
410
  // Keep the custom renderer mounted once the section has been expanded so user edits
362
411
  // persist across collapse/expand cycles (mirrors the default form's `[hidden]` approach).
363
412
  this.#hasExpanded = signal(false, ...(ngDevMode ? [{ debugName: "#hasExpanded" }] : /* istanbul ignore next */ []));
364
413
  this.#expandTrackEffect = effect(() => {
365
- if (this.expanded())
414
+ if (this.isExpanded())
366
415
  this.#hasExpanded.set(true);
367
416
  }, ...(ngDevMode ? [{ debugName: "#expandTrackEffect" }] : /* istanbul ignore next */ []));
368
417
  this.renderCustom = computed(() => !!this.customTemplate() && this.#hasExpanded(), ...(ngDevMode ? [{ debugName: "renderCustom" }] : /* istanbul ignore next */ []));
@@ -388,7 +437,10 @@ class ObjectMetadataSectionComponent {
388
437
  }
389
438
  #system;
390
439
  #sotRenderers;
440
+ #enableAnimateOnMount;
391
441
  #formOptionsEffect;
442
+ /** Active status: the custom renderer's when present, otherwise the object-form's. */
443
+ #status;
392
444
  // Keep the custom renderer mounted once the section has been expanded so user edits
393
445
  // persist across collapse/expand cycles (mirrors the default form's `[hidden]` approach).
394
446
  #hasExpanded;
@@ -396,6 +448,16 @@ class ObjectMetadataSectionComponent {
396
448
  // INBOUND command channel: reset/pristine triggered by the surrounding metadata form.
397
449
  #commandSubject;
398
450
  #setCustomStatus;
451
+ toggle() {
452
+ if (this.skipExpand())
453
+ return;
454
+ this.expanded.set(!this.expanded());
455
+ }
456
+ /** Capture the default form's settled status, then re-emit it to consumers. */
457
+ onFormStatusChanged(evt) {
458
+ this.formStatus.set(evt);
459
+ this.statusChanged.emit(evt);
460
+ }
399
461
  resetForm() {
400
462
  if (this.customTemplate())
401
463
  this.#commandSubject.next({ type: 'reset' });
@@ -407,19 +469,23 @@ class ObjectMetadataSectionComponent {
407
469
  this.#commandSubject.next({ type: 'pristine' });
408
470
  this.customStatus.set(undefined);
409
471
  }
410
- else
472
+ else {
411
473
  this.objectForm()?.setFormPristine();
474
+ // setFormPristine() marks the form pristine without emitting statusChanged, so
475
+ // clear the settled status here to drop the dirty/invalid host styling.
476
+ this.formStatus.set(undefined);
477
+ }
412
478
  }
413
479
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ObjectMetadataSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
414
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: ObjectMetadataSectionComponent, isStandalone: true, selector: "yuv-object-metadata-section", inputs: { section: { classPropertyName: "section", publicName: "section", isSignal: true, isRequired: true, transformFunction: null }, expandedInput: { classPropertyName: "expandedInput", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null }, elementExtensions: { classPropertyName: "elementExtensions", publicName: "elementExtensions", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { statusChanged: "statusChanged" }, host: { properties: { "class.invalid": "customTemplate() ? !!customStatus()?.invalid : objectForm()?.form?.invalid", "class.dirty": "customTemplate() ? !!customStatus()?.dirty : objectForm()?.form?.dirty" } }, viewQueries: [{ propertyName: "objectForm", first: true, predicate: ObjectFormComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@let _section = section();\n@let fo = formOptions();\n\n<h3>\n <button\n type=\"button\"\n [attr.aria-expanded]=\"expanded()\"\n class=\"accordion-trigger\"\n [attr.aria-controls]=\"'sect_' + _section.id\"\n [id]=\"'accordion_' + _section.id\"\n (click)=\"expanded.set(!expanded())\"\n [attr.aria-busy]=\"busy()\"\n >\n @if (_section.icon) {\n @if (_section.svgIcon) {\n <mat-icon [svgIcon]=\"_section.icon\" />\n } @else {\n <mat-icon>{{ _section.icon }}</mat-icon>\n }\n }\n <span>{{ _section.label }}</span>\n @if (busy()) {\n <mat-spinner diameter=\"24\" />\n }\n <mat-icon class=\"arr\">keyboard_arrow_down</mat-icon>\n </button>\n</h3>\n<div\n [id]=\"'sect_' + _section.id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'accordion_' + _section.id\"\n class=\"accordion-panel\"\n>\n @if (customTemplate()) {\n @if (renderCustom()) {\n <div [hidden]=\"!expanded()\">\n <ng-container *ngTemplateOutlet=\"customTemplate()!; context: { $implicit: sotContext() }\" />\n </div>\n }\n } @else if (fo) {\n <yuv-object-form\n #objectForm\n [hidden]=\"!expanded()\"\n [readonly]=\"readonly()\"\n [formOptions]=\"fo\"\n [elementExtensions]=\"elementExtensions()\"\n (statusChanged)=\"statusChanged.emit($event)\"\n />\n }\n</div>\n", styles: [":host{--section-state-border-color: var(--ymt-outline-variant);--section-state-outline-color: transparent;background-color:var(--_object-metadata-section-background);border:1px solid var(--section-state-border-color);border-radius:var(--_object-metadata-section-corner);outline:3px solid var(--section-state-outline-color)}:host.invalid{--section-state-border-color: var(--ymt-danger);--section-state-outline-color: rgb(from var(--ymt-danger) r g b /.2)}:host.dirty:not(.invalid){--section-state-border-color: var(--ymt-success);--section-state-outline-color: rgb(from var(--ymt-success) r g b /.2)}:host h3{margin:0;padding:0;display:flex}:host h3 button{margin:var(--ymt-spacing-2xs);border-radius:calc(var(--_object-metadata-section-corner) * .5);flex:1;padding:var(--ymt-spacing-xs);cursor:pointer;display:flex;gap:var(--ymt-spacing-xs);align-items:center;font-weight:700;color:var(--ymt-text-color-subtle);background-color:transparent;border:none}:host h3 button:hover{background-color:var(--ymt-hover-background)}:host h3 button:focus-visible{background-color:var(--ymt-hover-background);outline:2px solid var(--ymt-outline);outline-offset:-2px}:host h3 button[aria-expanded=true] mat-icon.arr{transform:rotate(180deg)}:host h3 button span{flex:1;text-align:start}:host h3 button mat-icon{margin-inline-end:var(--ymt-spacing-xs);transition:transform .3s ease}:host details{background-color:var(--_object-metadata-section-background);border:1px solid var(--ymt-outline-variant);border-radius:var(--_object-metadata-section-corner)}:host details:not(:last-child){margin-block-end:var(--ymt-spacing-m)}:host details[open] yuv-icon.arr{transform:rotate(180deg)}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i2$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: ObjectFormComponent, selector: "yuv-object-form", inputs: ["formOptions", "inert", "readonly", "elementExtensions", "isInnerTableForm"], outputs: ["statusChanged", "onFormReady"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
480
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: ObjectMetadataSectionComponent, isStandalone: true, selector: "yuv-object-metadata-section", inputs: { section: { classPropertyName: "section", publicName: "section", isSignal: true, isRequired: true, transformFunction: null }, expandedInput: { classPropertyName: "expandedInput", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null }, elementExtensions: { classPropertyName: "elementExtensions", publicName: "elementExtensions", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, skipExpand: { classPropertyName: "skipExpand", publicName: "skipExpand", isSignal: true, isRequired: false, transformFunction: null }, skipTitle: { classPropertyName: "skipTitle", publicName: "skipTitle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { statusChanged: "statusChanged" }, host: { properties: { "class.invalid": "showInvalid()", "class.dirty": "showDirty()" } }, viewQueries: [{ propertyName: "objectForm", first: true, predicate: ObjectFormComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@let _section = section();\n@let fo = formOptions();\n\n@if (!skipTitle()) {\n <h3>\n <button\n type=\"button\"\n [attr.aria-expanded]=\"isExpanded()\"\n class=\"accordion-trigger\"\n [class.non-collapsible]=\"skipExpand()\"\n [attr.aria-controls]=\"'sect_' + _section.id\"\n [id]=\"'accordion_' + _section.id\"\n [disabled]=\"skipExpand()\"\n (click)=\"toggle()\"\n [attr.aria-busy]=\"busy()\"\n >\n @if (_section.icon) {\n @if (_section.svgIcon) {\n <mat-icon [svgIcon]=\"_section.icon\" aria-hidden=\"true\" />\n } @else {\n <mat-icon aria-hidden=\"true\">{{ _section.icon }}</mat-icon>\n }\n }\n <span>{{ _section.label }}</span>\n @if (busy()) {\n <mat-spinner diameter=\"24\" />\n }\n @if (!skipExpand()) {\n <mat-icon class=\"arr\" aria-hidden=\"true\">keyboard_arrow_down</mat-icon>\n }\n </button>\n </h3>\n}\n<div\n [id]=\"'sect_' + _section.id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'accordion_' + _section.id\"\n class=\"accordion-panel\"\n [class.expanded]=\"isExpanded()\"\n [class.animate]=\"animate()\"\n [attr.inert]=\"isExpanded() ? null : ''\"\n>\n <div class=\"accordion-panel-content\">\n @if (customTemplate()) {\n @if (renderCustom()) {\n <div>\n <ng-container *ngTemplateOutlet=\"customTemplate()!; context: { $implicit: sotContext() }\" />\n </div>\n }\n } @else if (fo) {\n <yuv-object-form\n #objectForm\n [readonly]=\"readonly()\"\n [formOptions]=\"fo\"\n [elementExtensions]=\"elementExtensions()\"\n (statusChanged)=\"onFormStatusChanged($event)\"\n />\n }\n </div>\n</div>\n", styles: [":host{--section-state-border-color: var(--ymt-outline-variant);--section-state-outline-color: transparent;background-color:var(--_object-metadata-section-background);border:1px solid var(--section-state-border-color);border-radius:var(--_object-metadata-section-corner);outline:3px solid var(--section-state-outline-color)}:host.invalid{--section-state-border-color: var(--ymt-danger);--section-state-outline-color: rgb(from var(--ymt-danger) r g b / .2)}:host.dirty:not(.invalid){--section-state-border-color: var(--ymt-success);--section-state-outline-color: rgb(from var(--ymt-success) r g b / .2)}:host h3{margin:0;padding:0;display:flex}:host h3 button{margin:var(--ymt-spacing-2xs);border-radius:calc(var(--_object-metadata-section-corner) * .5);flex:1;padding:var(--ymt-spacing-xs);cursor:pointer;display:flex;gap:var(--ymt-spacing-xs);align-items:center;font-weight:700;color:var(--ymt-text-color-subtle);background-color:transparent;border:none}:host h3 button.non-collapsible{cursor:default}:host h3 button.non-collapsible:hover{background-color:transparent}:host h3 button:not(.non-collapsible):hover{background-color:var(--ymt-hover-background)}:host h3 button:focus-visible{background-color:var(--ymt-hover-background);outline:2px solid var(--ymt-outline);outline-offset:-2px}:host h3 button[aria-expanded=true] mat-icon.arr{transform:rotate(180deg)}:host h3 button span{flex:1;text-align:start}:host h3 button mat-icon{margin-inline-end:var(--ymt-spacing-xs);transition:transform .3s ease}:host .accordion-panel{display:grid;grid-template-rows:0fr;opacity:0}:host .accordion-panel.expanded{grid-template-rows:1fr;opacity:1}:host .accordion-panel.animate{transition:grid-template-rows .2s ease-out,opacity .2s ease-out}:host .accordion-panel .accordion-panel-content{overflow:hidden;min-height:0}:host details{background-color:var(--_object-metadata-section-background);border:1px solid var(--ymt-outline-variant);border-radius:var(--_object-metadata-section-corner)}:host details:not(:last-child){margin-block-end:var(--ymt-spacing-m)}:host details[open] yuv-icon.arr{transform:rotate(180deg)}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i2$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: ObjectFormComponent, selector: "yuv-object-form", inputs: ["formOptions", "inert", "readonly", "elementExtensions", "isInnerTableForm"], outputs: ["statusChanged", "onFormReady"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
415
481
  }
416
482
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ObjectMetadataSectionComponent, decorators: [{
417
483
  type: Component,
418
484
  args: [{ selector: 'yuv-object-metadata-section', imports: [MatIconModule, MatProgressSpinnerModule, ObjectFormComponent, NgTemplateOutlet], host: {
419
- '[class.invalid]': 'customTemplate() ? !!customStatus()?.invalid : objectForm()?.form?.invalid',
420
- '[class.dirty]': 'customTemplate() ? !!customStatus()?.dirty : objectForm()?.form?.dirty'
421
- }, template: "@let _section = section();\n@let fo = formOptions();\n\n<h3>\n <button\n type=\"button\"\n [attr.aria-expanded]=\"expanded()\"\n class=\"accordion-trigger\"\n [attr.aria-controls]=\"'sect_' + _section.id\"\n [id]=\"'accordion_' + _section.id\"\n (click)=\"expanded.set(!expanded())\"\n [attr.aria-busy]=\"busy()\"\n >\n @if (_section.icon) {\n @if (_section.svgIcon) {\n <mat-icon [svgIcon]=\"_section.icon\" />\n } @else {\n <mat-icon>{{ _section.icon }}</mat-icon>\n }\n }\n <span>{{ _section.label }}</span>\n @if (busy()) {\n <mat-spinner diameter=\"24\" />\n }\n <mat-icon class=\"arr\">keyboard_arrow_down</mat-icon>\n </button>\n</h3>\n<div\n [id]=\"'sect_' + _section.id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'accordion_' + _section.id\"\n class=\"accordion-panel\"\n>\n @if (customTemplate()) {\n @if (renderCustom()) {\n <div [hidden]=\"!expanded()\">\n <ng-container *ngTemplateOutlet=\"customTemplate()!; context: { $implicit: sotContext() }\" />\n </div>\n }\n } @else if (fo) {\n <yuv-object-form\n #objectForm\n [hidden]=\"!expanded()\"\n [readonly]=\"readonly()\"\n [formOptions]=\"fo\"\n [elementExtensions]=\"elementExtensions()\"\n (statusChanged)=\"statusChanged.emit($event)\"\n />\n }\n</div>\n", styles: [":host{--section-state-border-color: var(--ymt-outline-variant);--section-state-outline-color: transparent;background-color:var(--_object-metadata-section-background);border:1px solid var(--section-state-border-color);border-radius:var(--_object-metadata-section-corner);outline:3px solid var(--section-state-outline-color)}:host.invalid{--section-state-border-color: var(--ymt-danger);--section-state-outline-color: rgb(from var(--ymt-danger) r g b /.2)}:host.dirty:not(.invalid){--section-state-border-color: var(--ymt-success);--section-state-outline-color: rgb(from var(--ymt-success) r g b /.2)}:host h3{margin:0;padding:0;display:flex}:host h3 button{margin:var(--ymt-spacing-2xs);border-radius:calc(var(--_object-metadata-section-corner) * .5);flex:1;padding:var(--ymt-spacing-xs);cursor:pointer;display:flex;gap:var(--ymt-spacing-xs);align-items:center;font-weight:700;color:var(--ymt-text-color-subtle);background-color:transparent;border:none}:host h3 button:hover{background-color:var(--ymt-hover-background)}:host h3 button:focus-visible{background-color:var(--ymt-hover-background);outline:2px solid var(--ymt-outline);outline-offset:-2px}:host h3 button[aria-expanded=true] mat-icon.arr{transform:rotate(180deg)}:host h3 button span{flex:1;text-align:start}:host h3 button mat-icon{margin-inline-end:var(--ymt-spacing-xs);transition:transform .3s ease}:host details{background-color:var(--_object-metadata-section-background);border:1px solid var(--ymt-outline-variant);border-radius:var(--_object-metadata-section-corner)}:host details:not(:last-child){margin-block-end:var(--ymt-spacing-m)}:host details[open] yuv-icon.arr{transform:rotate(180deg)}\n"] }]
422
- }], propDecorators: { objectForm: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ObjectFormComponent), { isSignal: true }] }], section: [{ type: i0.Input, args: [{ isSignal: true, alias: "section", required: true }] }], expandedInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }], elementExtensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementExtensions", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], statusChanged: [{ type: i0.Output, args: ["statusChanged"] }] } });
485
+ '[class.invalid]': 'showInvalid()',
486
+ '[class.dirty]': 'showDirty()'
487
+ }, template: "@let _section = section();\n@let fo = formOptions();\n\n@if (!skipTitle()) {\n <h3>\n <button\n type=\"button\"\n [attr.aria-expanded]=\"isExpanded()\"\n class=\"accordion-trigger\"\n [class.non-collapsible]=\"skipExpand()\"\n [attr.aria-controls]=\"'sect_' + _section.id\"\n [id]=\"'accordion_' + _section.id\"\n [disabled]=\"skipExpand()\"\n (click)=\"toggle()\"\n [attr.aria-busy]=\"busy()\"\n >\n @if (_section.icon) {\n @if (_section.svgIcon) {\n <mat-icon [svgIcon]=\"_section.icon\" aria-hidden=\"true\" />\n } @else {\n <mat-icon aria-hidden=\"true\">{{ _section.icon }}</mat-icon>\n }\n }\n <span>{{ _section.label }}</span>\n @if (busy()) {\n <mat-spinner diameter=\"24\" />\n }\n @if (!skipExpand()) {\n <mat-icon class=\"arr\" aria-hidden=\"true\">keyboard_arrow_down</mat-icon>\n }\n </button>\n </h3>\n}\n<div\n [id]=\"'sect_' + _section.id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'accordion_' + _section.id\"\n class=\"accordion-panel\"\n [class.expanded]=\"isExpanded()\"\n [class.animate]=\"animate()\"\n [attr.inert]=\"isExpanded() ? null : ''\"\n>\n <div class=\"accordion-panel-content\">\n @if (customTemplate()) {\n @if (renderCustom()) {\n <div>\n <ng-container *ngTemplateOutlet=\"customTemplate()!; context: { $implicit: sotContext() }\" />\n </div>\n }\n } @else if (fo) {\n <yuv-object-form\n #objectForm\n [readonly]=\"readonly()\"\n [formOptions]=\"fo\"\n [elementExtensions]=\"elementExtensions()\"\n (statusChanged)=\"onFormStatusChanged($event)\"\n />\n }\n </div>\n</div>\n", styles: [":host{--section-state-border-color: var(--ymt-outline-variant);--section-state-outline-color: transparent;background-color:var(--_object-metadata-section-background);border:1px solid var(--section-state-border-color);border-radius:var(--_object-metadata-section-corner);outline:3px solid var(--section-state-outline-color)}:host.invalid{--section-state-border-color: var(--ymt-danger);--section-state-outline-color: rgb(from var(--ymt-danger) r g b / .2)}:host.dirty:not(.invalid){--section-state-border-color: var(--ymt-success);--section-state-outline-color: rgb(from var(--ymt-success) r g b / .2)}:host h3{margin:0;padding:0;display:flex}:host h3 button{margin:var(--ymt-spacing-2xs);border-radius:calc(var(--_object-metadata-section-corner) * .5);flex:1;padding:var(--ymt-spacing-xs);cursor:pointer;display:flex;gap:var(--ymt-spacing-xs);align-items:center;font-weight:700;color:var(--ymt-text-color-subtle);background-color:transparent;border:none}:host h3 button.non-collapsible{cursor:default}:host h3 button.non-collapsible:hover{background-color:transparent}:host h3 button:not(.non-collapsible):hover{background-color:var(--ymt-hover-background)}:host h3 button:focus-visible{background-color:var(--ymt-hover-background);outline:2px solid var(--ymt-outline);outline-offset:-2px}:host h3 button[aria-expanded=true] mat-icon.arr{transform:rotate(180deg)}:host h3 button span{flex:1;text-align:start}:host h3 button mat-icon{margin-inline-end:var(--ymt-spacing-xs);transition:transform .3s ease}:host .accordion-panel{display:grid;grid-template-rows:0fr;opacity:0}:host .accordion-panel.expanded{grid-template-rows:1fr;opacity:1}:host .accordion-panel.animate{transition:grid-template-rows .2s ease-out,opacity .2s ease-out}:host .accordion-panel .accordion-panel-content{overflow:hidden;min-height:0}:host details{background-color:var(--_object-metadata-section-background);border:1px solid var(--ymt-outline-variant);border-radius:var(--_object-metadata-section-corner)}:host details:not(:last-child){margin-block-end:var(--ymt-spacing-m)}:host details[open] yuv-icon.arr{transform:rotate(180deg)}\n"] }]
488
+ }], propDecorators: { objectForm: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ObjectFormComponent), { isSignal: true }] }], section: [{ type: i0.Input, args: [{ isSignal: true, alias: "section", required: true }] }], expandedInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }], elementExtensions: [{ type: i0.Input, args: [{ isSignal: true, alias: "elementExtensions", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], skipExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipExpand", required: false }] }], skipTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipTitle", required: false }] }], statusChanged: [{ type: i0.Output, args: ["statusChanged"] }] } });
423
489
 
424
490
  class ObjectMetadataComponent {
425
491
  constructor() {
@@ -746,7 +812,7 @@ class ObjectMetadataComponent {
746
812
  // fresh writable signal per <yuv-object-metadata> instance → per-object scope.
747
813
  // Element templates read it (read-only) via the metadata form field context.
748
814
  { provide: OBJECT_CONTEXT, useFactory: () => signal(undefined) }
749
- ], viewQueries: [{ propertyName: "objectForms", predicate: ObjectFormComponent, descendants: true, isSignal: true }, { propertyName: "objectMetadataSectionForms", predicate: ObjectMetadataSectionComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<main [yuvBusyOverlay]=\"busySaving() || busyLoading() || mainFormOptionsIsLoading()\">\n @if (empty()) {\n <p>{{ 'yuv.object-metadata.empty.message' | translate }}</p>\n }\n @if (!mainFormOptions()) {\n <yuv-object-form\n [readonly]=\"!hasWritePermission()\"\n [formOptions]=\"mainFormOptions()\"\n [elementExtensions]=\"elementExtensions()\"\n (statusChanged)=\"onFormStatusChanged('main', $event)\"\n />\n }\n @for (fo of flavorFormOptions(); track $index) {\n <yuv-object-metadata-section\n [readonly]=\"!hasWritePermission()\"\n [section]=\"fo\"\n [elementExtensions]=\"elementExtensions()\"\n [expanded]=\"$index === 0\"\n (statusChanged)=\"onFormStatusChanged(fo.id, $event)\"\n />\n }\n\n <!-- base data -->\n @let object = dmsObject() ?? dmsObject();\n @if (object && !disableBasicMetadata()) {\n <yuv-object-summary-data [dmsObject]=\"object\" />\n }\n</main>\n\n<footer [attr.inert]=\"disableControls() || !controlsVisible() || null\">\n <div class=\"container\">\n <button\n ymtButton=\"secondary\"\n button-size=\"small\"\n [disabled]=\"busySaving() || busyLoading()\"\n (click)=\"resetForm()\"\n [hidden]=\"!combinedFormState?.dirty\"\n [disabled]=\"!combinedFormState?.dirty\"\n >\n {{ 'yuv.object-metadata.button.reset' | translate }}\n </button>\n <button\n ymtButton=\"primary\"\n button-size=\"small\"\n (click)=\"save()\"\n [disabled]=\"!combinedFormState?.dirty || combinedFormState?.invalid || busySaving() || busyLoading()\"\n >\n {{ 'yuv.object-metadata.button.save' | translate }}\n </button>\n </div>\n</footer>\n", styles: [":host{--_object-metadata-panel-background: var(--object-metadata-panel-background, var(--ymt-surface-container-low));--_object-metadata-section-background: var(--object-metadata-section-background, var(--ymt-surface));--_object-metadata-section-corner: var(--object-metadata-section-corner, var(--ymt-corner-m));--_object-metadata-footer-background: var(--object-metadata-footer-background, var(--ymt-surface-container));--_object-metadata-footer-border: 1px solid var(--object-metadata-footer-border, var(--ymt-outline-variant));--_object-metadata-footer-indent: var(--object-metadata-footer-indent, var(--ymt-spacing-xs));--_object-metadata-footer-height: 4rem;--_object-metadata-footer-corner: var(--object-metadata-footer-corner, var(--ymt-corner-s));display:grid;grid-template-rows:1fr auto;grid-template-columns:1fr;height:100%;overflow:hidden;position:relative;background-color:var(--_object-metadata-panel-background)}:host.empty main{justify-content:center;align-items:center}:host.loading main>*{opacity:0}:host main{grid-row:1/span 3;grid-column:1;overflow-y:auto;width:100%;display:flex;flex-flow:column;padding:var(--ymt-spacing-3xl) var(--ymt-spacing-l) var(--_object-metadata-footer-height) var(--ymt-spacing-l)}:host footer{grid-row:-1;grid-column:1;padding:var(--_object-metadata-footer-indent);opacity:0;z-index:101}:host footer .container{background:var(--_object-metadata-footer-background);padding:var(--ymt-spacing-s);display:flex;align-items:center;justify-content:end;gap:var(--ymt-spacing-s);border-radius:var(--_object-metadata-footer-corner);border:var(--_object-metadata-footer-border)}:host footer:not([inert]){animation:controlsAppear .2s ease-in-out forwards}:host main yuv-object-metadata-section{margin-block-end:var(--ymt-spacing-m)}:host main yuv-object-summary-data{background-color:var(--_object-metadata-section-background);border:1px solid var(--ymt-outline-variant);border-radius:var(--_object-metadata-section-corner);padding:var(--ymt-spacing-m)}@keyframes controlsAppear{0%{opacity:0;translate:0 var(--ymt-spacing-m)}to{opacity:1;translate:0 0}}\n"], dependencies: [{ kind: "component", type: ObjectFormComponent, selector: "yuv-object-form", inputs: ["formOptions", "inert", "readonly", "elementExtensions", "isInnerTableForm"], outputs: ["statusChanged", "onFormReady"] }, { kind: "component", type: ObjectSummaryDataComponent, selector: "yuv-object-summary-data", inputs: ["dmsObject", "flavors", "showAppliedFlavors"] }, { kind: "directive", type: BusyOverlayDirective, selector: "[yuvBusyOverlay]", inputs: ["yuvBusyOverlay", "yuvBusyOverlayConfig", "yuvBusyError"], outputs: ["yuvBusyErrorDismiss"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: ObjectMetadataSectionComponent, selector: "yuv-object-metadata-section", inputs: ["section", "expanded", "elementExtensions", "readonly"], outputs: ["statusChanged"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] }); }
815
+ ], viewQueries: [{ propertyName: "objectForms", predicate: ObjectFormComponent, descendants: true, isSignal: true }, { propertyName: "objectMetadataSectionForms", predicate: ObjectMetadataSectionComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<main [yuvBusyOverlay]=\"busySaving() || busyLoading() || mainFormOptionsIsLoading()\">\n @if (empty()) {\n <p>{{ 'yuv.object-metadata.empty.message' | translate }}</p>\n }\n @if (!mainFormOptions()) {\n <yuv-object-form\n [readonly]=\"!hasWritePermission()\"\n [formOptions]=\"mainFormOptions()\"\n [elementExtensions]=\"elementExtensions()\"\n (statusChanged)=\"onFormStatusChanged('main', $event)\"\n />\n }\n @for (fo of flavorFormOptions(); track $index) {\n <yuv-object-metadata-section\n [readonly]=\"!hasWritePermission()\"\n [section]=\"fo\"\n [elementExtensions]=\"elementExtensions()\"\n [expanded]=\"$index === 0\"\n (statusChanged)=\"onFormStatusChanged(fo.id, $event)\"\n />\n }\n\n <!-- base data -->\n @let object = dmsObject() ?? dmsObject();\n @if (object && !disableBasicMetadata()) {\n <yuv-object-summary-data [dmsObject]=\"object\" />\n }\n</main>\n\n<footer [attr.inert]=\"disableControls() || !controlsVisible() || null\">\n <div class=\"container\">\n <button\n ymtButton=\"secondary\"\n button-size=\"small\"\n [disabled]=\"busySaving() || busyLoading()\"\n (click)=\"resetForm()\"\n [hidden]=\"!combinedFormState?.dirty\"\n [disabled]=\"!combinedFormState?.dirty\"\n >\n {{ 'yuv.object-metadata.button.reset' | translate }}\n </button>\n <button\n ymtButton=\"primary\"\n button-size=\"small\"\n (click)=\"save()\"\n [disabled]=\"!combinedFormState?.dirty || combinedFormState?.invalid || busySaving() || busyLoading()\"\n >\n {{ 'yuv.object-metadata.button.save' | translate }}\n </button>\n </div>\n</footer>\n", styles: [":host{--_object-metadata-panel-background: var(--object-metadata-panel-background, var(--ymt-surface-container-low));--_object-metadata-section-background: var(--object-metadata-section-background, var(--ymt-surface));--_object-metadata-section-corner: var(--object-metadata-section-corner, var(--ymt-corner-m));--_object-metadata-footer-background: var(--object-metadata-footer-background, var(--ymt-surface-container));--_object-metadata-footer-border: 1px solid var(--object-metadata-footer-border, var(--ymt-outline-variant));--_object-metadata-footer-indent: var(--object-metadata-footer-indent, var(--ymt-spacing-xs));--_object-metadata-footer-height: 4rem;--_object-metadata-footer-corner: var(--object-metadata-footer-corner, var(--ymt-corner-s));display:grid;grid-template-rows:1fr auto;grid-template-columns:1fr;height:100%;overflow:hidden;position:relative;background-color:var(--_object-metadata-panel-background)}:host.empty main{justify-content:center;align-items:center}:host.loading main>*{opacity:0}:host main{grid-row:1/span 3;grid-column:1;overflow-y:auto;width:100%;display:flex;flex-flow:column;padding:var(--ymt-spacing-3xl) var(--ymt-spacing-l) var(--_object-metadata-footer-height) var(--ymt-spacing-l)}:host footer{grid-row:-1;grid-column:1;padding:var(--_object-metadata-footer-indent);opacity:0;z-index:101}:host footer .container{background:var(--_object-metadata-footer-background);padding:var(--ymt-spacing-s);display:flex;align-items:center;justify-content:end;gap:var(--ymt-spacing-s);border-radius:var(--_object-metadata-footer-corner);border:var(--_object-metadata-footer-border)}:host footer:not([inert]){animation:controlsAppear .2s ease-in-out forwards}:host main yuv-object-metadata-section{margin-block-end:var(--ymt-spacing-m)}:host main yuv-object-summary-data{background-color:var(--_object-metadata-section-background);border:1px solid var(--ymt-outline-variant);border-radius:var(--_object-metadata-section-corner);padding:var(--ymt-spacing-m)}@keyframes controlsAppear{0%{opacity:0;translate:0 var(--ymt-spacing-m)}to{opacity:1;translate:0 0}}\n"], dependencies: [{ kind: "component", type: ObjectFormComponent, selector: "yuv-object-form", inputs: ["formOptions", "inert", "readonly", "elementExtensions", "isInnerTableForm"], outputs: ["statusChanged", "onFormReady"] }, { kind: "component", type: ObjectSummaryDataComponent, selector: "yuv-object-summary-data", inputs: ["dmsObject", "flavors", "showAppliedFlavors"] }, { kind: "directive", type: BusyOverlayDirective, selector: "[yuvBusyOverlay]", inputs: ["yuvBusyOverlay", "yuvBusyOverlayConfig", "yuvBusyError"], outputs: ["yuvBusyErrorDismiss"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: ObjectMetadataSectionComponent, selector: "yuv-object-metadata-section", inputs: ["section", "expanded", "elementExtensions", "readonly", "skipExpand", "skipTitle"], outputs: ["statusChanged"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] }); }
750
816
  }
751
817
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ObjectMetadataComponent, decorators: [{
752
818
  type: Component,