ng-form-foundry 0.5.4 → 0.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.
@@ -10,16 +10,17 @@ import * as i4 from '@angular/material/checkbox';
10
10
  import { MatCheckboxModule } from '@angular/material/checkbox';
11
11
  import * as i3 from '@angular/material/select';
12
12
  import { MatSelectModule } from '@angular/material/select';
13
- import * as i1$1 from '@angular/material/icon';
13
+ import * as i2$1 from '@angular/material/icon';
14
14
  import { MatIconModule } from '@angular/material/icon';
15
- import * as i2$1 from '@angular/material/button';
15
+ import * as i2$2 from '@angular/material/button';
16
16
  import { MatButtonModule } from '@angular/material/button';
17
+ import * as i3$1 from '@angular/material/tooltip';
17
18
  import { MatTooltip, MatTooltipModule } from '@angular/material/tooltip';
18
19
  import { MatDialog } from '@angular/material/dialog';
19
- import * as i2$2 from '@angular/material/expansion';
20
+ import * as i2$3 from '@angular/material/expansion';
20
21
  import { MatExpansionModule } from '@angular/material/expansion';
21
22
  import { NgTemplateOutlet } from '@angular/common';
22
- import * as i3$1 from '@angular/material/menu';
23
+ import * as i3$2 from '@angular/material/menu';
23
24
  import { MatMenuModule } from '@angular/material/menu';
24
25
 
25
26
  /**
@@ -50,7 +51,12 @@ function isMap(node) {
50
51
  }
51
52
  /** Whether the node is an optional (presence) node: its key is data, absent until enabled. */
52
53
  function hasPresence(node) {
53
- return ((node.kind === 'leaf' || node.kind === 'nodeGroup' || node.kind === 'map' || node.kind === 'choice') &&
54
+ return ((node.kind === 'leaf' ||
55
+ node.kind === 'nodeGroup' ||
56
+ node.kind === 'map' ||
57
+ node.kind === 'choice' ||
58
+ node.kind === 'leafList' ||
59
+ node.kind === 'nodeGroupList') &&
54
60
  node.presence === true);
55
61
  }
56
62
  /**
@@ -477,6 +483,37 @@ function removeMapEntry(group, map, key) {
477
483
  group.removeControl(key);
478
484
  return true;
479
485
  }
486
+ /**
487
+ * Materialize or de-materialize an optional (presence) child of `group`,
488
+ * mirroring the presence toggle the form UI offers — but callable by any host
489
+ * holding the {@link FormGroup}, for a key at any depth. `present: true` builds
490
+ * the child fresh from `schema` (seeded with `initial` if given; nested
491
+ * presence descendants start absent, as {@link buildControl}); `present: false`
492
+ * removes it, dropping the key from the form value.
493
+ *
494
+ * The materialize direction is why the coupling exists: a materialized
495
+ * non-nullable presence leaf carries `Validators.required`, because an
496
+ * enabled-but-empty key would serialize as `null` and fail a typed schema. A
497
+ * host that materializes fields for editing therefore drops the ones left
498
+ * empty on cancel by de-materializing them — this API is that primitive.
499
+ *
500
+ * `schema` must be the presence node for `key`; a non-presence `schema` is a
501
+ * no-op. Returns whether the form changed.
502
+ */
503
+ function setNodePresence(group, schema, key, present, initial) {
504
+ if (!hasPresence(schema))
505
+ return false;
506
+ if (present) {
507
+ if (group.contains(key))
508
+ return false;
509
+ group.addControl(key, buildControl(schema, initial));
510
+ return true;
511
+ }
512
+ if (!group.contains(key))
513
+ return false;
514
+ group.removeControl(key);
515
+ return true;
516
+ }
480
517
  /**
481
518
  * The map's own constraints as a group validator: entry count against
482
519
  * `minEntries`/`maxEntries` and every entry key against `keyPattern`. The UI
@@ -930,7 +967,7 @@ class LeafRendererComponent {
930
967
  return 'Invalid value';
931
968
  }
932
969
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
933
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafRendererComponent, isStandalone: true, selector: "nff-leaf-renderer", inputs: { leaf_: "leaf_", control: "control", removable: "removable", editable: "editable", autofocus: "autofocus", placeholder: "placeholder" }, outputs: { remove: "remove" }, host: { properties: { "class": "\"leaf-type-\" + (leaf_?.type ?? \"unknown\")" } }, ngImport: i0, template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n @if (leaf_.radix) {\n <!-- The bigint decimal-string carry, presented in its source base. -->\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [nffRadixInput]=\"leaf_.radix\" nffRadixValueType=\"string\" [placeholder]=\"placeholder\" [formControl]=\"control\">\n } @else {\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [placeholder]=\"placeholder\" [formControl]=\"control\">\n }\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'number') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n @if (leaf_.radix) {\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [nffRadixInput]=\"leaf_.radix\" [placeholder]=\"placeholder\" [formControl]=\"control\">\n } @else {\n <input [readonly]=\"!fieldEditable\" matInput type=\"number\" [placeholder]=\"placeholder\" [formControl]=\"control\">\n }\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'boolean') {\n @if (fieldEditable) {\n <mat-checkbox [formControl]=\"control\">{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n } @else {\n <!-- Display-only: no formControl binding, so the wire value cannot change.\n The control itself is never disable()d \u2014 a disabled control would\n still appear in the form value with different semantics. -->\n <mat-checkbox [checked]=\"control.value === true\" disabled>{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n }\n } @else if (leaf_.type === 'enum') {\n <nff-leaf-enum-renderer [editable]=\"fieldEditable\" [leafEnum]=\"leaf_\" [control]=\"control\" [placeholder]=\"placeholder\"></nff-leaf-enum-renderer>\n }\n @if (removable && editable) {\n <button matIconButton class=\"remove-button small-icon-button\" [matTooltip]=\"'Remove ' + ('label' in leaf_ && leaf_.label ? leaf_.label : 'name' in leaf_ ? leaf_.name : 'field')\" [attr.aria-label]=\"'Remove ' + ('label' in leaf_ && leaf_.label ? leaf_.label : 'name' in leaf_ ? leaf_.name : 'field')\" (click)=\"remove.emit()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n}\n", styles: [":host{display:flex;flex:1 1 0;min-width:10%;gap:8px}:host(.leaf-type-string),:host(.leaf-type-enum){min-width:var(--nff-min-text-field-width, 10%)}:host(.leaf-type-number){min-width:var(--nff-min-number-field-width, 10%);max-width:var(--nff-max-number-field-width, none)}mat-form-field{flex:1;min-width:0}.remove-button{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.remove-button{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.remove-button{transition:none}}@media (hover: hover){:host .remove-button{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}:host:hover .remove-button,:host:focus-within .remove-button{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: LeafEnumRendererComponent, selector: "nff-leaf-enum-renderer", inputs: ["leafEnum", "initialValue", "control", "removable", "remove", "editable", "placeholder"] }, { kind: "directive", type: RadixInputDirective, selector: "input[nffRadixInput]", inputs: ["nffRadixInput", "nffRadixValueType"] }] });
970
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafRendererComponent, isStandalone: true, selector: "nff-leaf-renderer", inputs: { leaf_: "leaf_", control: "control", removable: "removable", editable: "editable", autofocus: "autofocus", placeholder: "placeholder" }, outputs: { remove: "remove" }, host: { properties: { "class": "\"leaf-type-\" + (leaf_?.type ?? \"unknown\")" } }, ngImport: i0, template: "@if ('name' in leaf_) {\n @if (leaf_.type === 'string') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n @if (leaf_.radix) {\n <!-- The bigint decimal-string carry, presented in its source base. -->\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [nffRadixInput]=\"leaf_.radix\" nffRadixValueType=\"string\" [placeholder]=\"placeholder\" [formControl]=\"control\">\n } @else {\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [placeholder]=\"placeholder\" [formControl]=\"control\">\n }\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'number') {\n <mat-form-field [appearance]=\"fieldEditable ? 'fill' : 'outline'\">\n <mat-label>{{ leaf_.label ?? leaf_.name }}</mat-label>\n @if (leaf_.radix) {\n <input [readonly]=\"!fieldEditable\" matInput type=\"text\" [nffRadixInput]=\"leaf_.radix\" [placeholder]=\"placeholder\" [formControl]=\"control\">\n } @else {\n <input [readonly]=\"!fieldEditable\" matInput type=\"number\" [placeholder]=\"placeholder\" [formControl]=\"control\">\n }\n <mat-error>{{ errorText }}</mat-error>\n </mat-form-field>\n } @else if (leaf_.type === 'boolean') {\n @if (fieldEditable) {\n <mat-checkbox [formControl]=\"control\">{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n } @else {\n <!-- Display-only: no formControl binding, so the wire value cannot change.\n The control itself is never disable()d \u2014 a disabled control would\n still appear in the form value with different semantics. -->\n <mat-checkbox [checked]=\"control.value === true\" disabled>{{ leaf_.label ?? leaf_.name }}</mat-checkbox>\n }\n } @else if (leaf_.type === 'enum') {\n <nff-leaf-enum-renderer [editable]=\"fieldEditable\" [leafEnum]=\"leaf_\" [control]=\"control\" [placeholder]=\"placeholder\"></nff-leaf-enum-renderer>\n }\n @if (removable && editable) {\n <button matIconButton class=\"remove-button small-icon-button\" [matTooltip]=\"'Remove ' + ('label' in leaf_ && leaf_.label ? leaf_.label : 'name' in leaf_ ? leaf_.name : 'field')\" [attr.aria-label]=\"'Remove ' + ('label' in leaf_ && leaf_.label ? leaf_.label : 'name' in leaf_ ? leaf_.name : 'field')\" (click)=\"remove.emit()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n}\n", styles: [":host{display:flex;flex:1 1 0;min-width:10%;gap:8px}:host(.leaf-type-string),:host(.leaf-type-enum){min-width:var(--nff-min-text-field-width, 10%)}:host(.leaf-type-number){min-width:var(--nff-min-number-field-width, 10%);max-width:var(--nff-max-number-field-width, none)}mat-form-field{flex:1;min-width:0}.remove-button{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.remove-button{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.remove-button{transition:none}}@media (hover: hover){:host .remove-button{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}:host:hover .remove-button,:host:focus-within .remove-button{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "directive", type: i2.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i4.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: LeafEnumRendererComponent, selector: "nff-leaf-enum-renderer", inputs: ["leafEnum", "initialValue", "control", "removable", "remove", "editable", "placeholder"] }, { kind: "directive", type: RadixInputDirective, selector: "input[nffRadixInput]", inputs: ["nffRadixInput", "nffRadixValueType"] }] });
934
971
  }
935
972
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafRendererComponent, decorators: [{
936
973
  type: Component,
@@ -1034,11 +1071,19 @@ class NodeGroupListRendererComponent {
1034
1071
  editable = true;
1035
1072
  /** Forwarded to each entry's embedded form — see the form's input of the same name. */
1036
1073
  showAbsentOptionals = false;
1037
- minItems = 1;
1038
- maxItems = 1;
1074
+ // Schema-driven bounds, matching the config editor: absent minItems floors
1075
+ // at 0 (an unbounded list can be emptied), absent maxItems is unbounded.
1076
+ minItems = 0;
1077
+ maxItems = Number.POSITIVE_INFINITY;
1039
1078
  /** Field-layout appearance from the enclosing group, forwarded to every item form. */
1040
1079
  inheritedAppearance = null;
1041
1080
  message = new EventEmitter();
1081
+ /**
1082
+ * Emitted when the last entry of a presence (optional / advisoryRequired) list
1083
+ * is removed — the host de-materializes the whole list (→ absent). An optional
1084
+ * list has no present-empty state; a required list never routes here.
1085
+ */
1086
+ removeList = new EventEmitter();
1042
1087
  // forwardRef: DynamicRecursiveFormComponent and this component import each
1043
1088
  // other, so the class reference is undefined when this query is evaluated at
1044
1089
  // decoration time. forwardRef defers the lookup and keeps the selector valid.
@@ -1048,12 +1093,19 @@ class NodeGroupListRendererComponent {
1048
1093
  if (this.initialValue) {
1049
1094
  this.formArray.patchValue(this.initialValue);
1050
1095
  }
1051
- if (this.nodeGroupList.maxItems) {
1052
- this.maxItems = this.nodeGroupList.maxItems;
1053
- }
1054
- if (this.nodeGroupList.minItems) {
1055
- this.minItems = this.nodeGroupList.minItems;
1056
- }
1096
+ }
1097
+ // Effective bounds: the schema wins when it declares them (an explicit
1098
+ // `minItems: 0` included), else the `@Input` fallback for a host that binds
1099
+ // the renderer directly. Getters, so a schema rebind is reflected live.
1100
+ get effectiveMin() {
1101
+ return this.nodeGroupList?.minItems ?? this.minItems;
1102
+ }
1103
+ get effectiveMax() {
1104
+ return this.nodeGroupList?.maxItems ?? this.maxItems;
1105
+ }
1106
+ /** Whether another item may be appended (below the effective maximum). */
1107
+ get canAdd() {
1108
+ return this.formArray.length < this.effectiveMax;
1057
1109
  }
1058
1110
  ngAfterViewInit() {
1059
1111
  this.items.changes.subscribe(() => {
@@ -1061,7 +1113,14 @@ class NodeGroupListRendererComponent {
1061
1113
  });
1062
1114
  }
1063
1115
  removeItem($index) {
1064
- if (this.formArray.length <= this.minItems) {
1116
+ // A presence (optional / advisoryRequired) list has no present-empty state:
1117
+ // removing its last entry de-materializes the whole list (→ absent). A
1118
+ // required (non-presence) list instead stops at its minimum, staying `[]`.
1119
+ if (this.nodeGroupList?.presence && this.formArray.length === 1) {
1120
+ this.removeList.emit();
1121
+ return;
1122
+ }
1123
+ if (this.formArray.length <= this.effectiveMin) {
1065
1124
  this.message.emit({
1066
1125
  message: `You cannot remove the last ${this.nodeGroupList.type.name} configuration!`,
1067
1126
  type: 'error',
@@ -1071,6 +1130,8 @@ class NodeGroupListRendererComponent {
1071
1130
  this.formArray.removeAt($index);
1072
1131
  }
1073
1132
  addItem = (index) => {
1133
+ if (this.formArray.length >= this.effectiveMax)
1134
+ return;
1074
1135
  this.formArray.push(buildFormFromSchema(this.nodeGroupList.type, null));
1075
1136
  };
1076
1137
  setLastEditable() {
@@ -1093,7 +1154,7 @@ class NodeGroupListRendererComponent {
1093
1154
  }
1094
1155
  }
1095
1156
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1096
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeGroupListRendererComponent, isStandalone: true, selector: "nff-node-group-list-renderer", inputs: { nodeGroupList: "nodeGroupList", initialValue: "initialValue", formArray: "formArray", editable: "editable", showAbsentOptionals: "showAbsentOptionals", minItems: "minItems", maxItems: "maxItems", inheritedAppearance: "inheritedAppearance" }, outputs: { message: "message" }, viewQueries: [{ propertyName: "items", predicate: i0.forwardRef(() => DynamicRecursiveFormComponent), descendants: true }], ngImport: i0, template: "@if (formArray && nodeGroupList) {\n <div class=\"fields-container\">\n @for (group of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"inheritedAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > minItems\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"addItem\"\n [addButtonLabel]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name) + ' #' + (formArray.length + 1)\"\n />\n </div>\n }\n </div>\n}\n", styles: [":host{position:relative;width:100%;flex:1 1 0}.fields{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.fields-container{display:flex;flex-direction:column}.field-item{display:flex;flex-direction:row;gap:8px}.actions{display:flex;flex-direction:row;gap:8px;align-items:center;margin-top:0;padding:0;min-width:24px}.title{display:flex;flex-direction:column;gap:8px;align-items:center}nff-dynamic-recursive-form{flex:1}.add-button{background-color:var(--mat-sys-primary-container)}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "ngmodule", type: i0.forwardRef(() => MatTooltipModule) }] });
1157
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeGroupListRendererComponent, isStandalone: true, selector: "nff-node-group-list-renderer", inputs: { nodeGroupList: "nodeGroupList", initialValue: "initialValue", formArray: "formArray", editable: "editable", showAbsentOptionals: "showAbsentOptionals", minItems: "minItems", maxItems: "maxItems", inheritedAppearance: "inheritedAppearance" }, outputs: { message: "message", removeList: "removeList" }, viewQueries: [{ propertyName: "items", predicate: i0.forwardRef(() => DynamicRecursiveFormComponent), descendants: true }], ngImport: i0, template: "@if (formArray && nodeGroupList) {\n <div class=\"fields-container\">\n @for (group of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"inheritedAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > effectiveMin\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"canAdd ? addItem : null\"\n [addButtonLabel]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name) + ' #' + (formArray.length + 1)\"\n />\n </div>\n }\n @if (editable && formArray.length === 0 && canAdd) {\n <!-- An empty list has no per-item header to host the add button, so it\n would otherwise be an unrecoverable dead end. This footer add\n creates the first item (mirrors the config editor's \"Add \u2026 #1\"). -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [matTooltip]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name)\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ nodeGroupList.type.label ?? nodeGroupList.type.name }} #1\n </button>\n }\n </div>\n}\n", styles: [":host{position:relative;width:100%;flex:1 1 0}.fields{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.fields-container{display:flex;flex-direction:column}.field-item{display:flex;flex-direction:row;gap:8px}.actions{display:flex;flex-direction:row;gap:8px;align-items:center;margin-top:0;padding:0;min-width:24px}.title{display:flex;flex-direction:column;gap:8px;align-items:center}nff-dynamic-recursive-form{flex:1}.add-button{background-color:var(--mat-sys-primary-container)}.empty-add-button{width:100%}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$2.MatButton), selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatTooltipModule) }, { kind: "directive", type: i0.forwardRef(() => i3$1.MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1097
1158
  }
1098
1159
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeGroupListRendererComponent, decorators: [{
1099
1160
  type: Component,
@@ -1102,7 +1163,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1102
1163
  MatButtonModule,
1103
1164
  MatIconModule,
1104
1165
  MatTooltipModule,
1105
- ], template: "@if (formArray && nodeGroupList) {\n <div class=\"fields-container\">\n @for (group of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"inheritedAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > minItems\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"addItem\"\n [addButtonLabel]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name) + ' #' + (formArray.length + 1)\"\n />\n </div>\n }\n </div>\n}\n", styles: [":host{position:relative;width:100%;flex:1 1 0}.fields{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.fields-container{display:flex;flex-direction:column}.field-item{display:flex;flex-direction:row;gap:8px}.actions{display:flex;flex-direction:row;gap:8px;align-items:center;margin-top:0;padding:0;min-width:24px}.title{display:flex;flex-direction:column;gap:8px;align-items:center}nff-dynamic-recursive-form{flex:1}.add-button{background-color:var(--mat-sys-primary-container)}\n"] }]
1166
+ ], template: "@if (formArray && nodeGroupList) {\n <div class=\"fields-container\">\n @for (group of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"inheritedAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"nodeGroupList.type\"\n [formGroup]=\"asFormGroup(group)\"\n [title]=\"getTitle($index)\"\n [index]=\"0\"\n [removable]=\"formArray.length > effectiveMin\"\n (remove)=\"removeItem($index)\"\n [editable]=\"editable\"\n [addButtonCallback]=\"canAdd ? addItem : null\"\n [addButtonLabel]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name) + ' #' + (formArray.length + 1)\"\n />\n </div>\n }\n @if (editable && formArray.length === 0 && canAdd) {\n <!-- An empty list has no per-item header to host the add button, so it\n would otherwise be an unrecoverable dead end. This footer add\n creates the first item (mirrors the config editor's \"Add \u2026 #1\"). -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [matTooltip]=\"'Add ' + (nodeGroupList.type.label ?? nodeGroupList.type.name)\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ nodeGroupList.type.label ?? nodeGroupList.type.name }} #1\n </button>\n }\n </div>\n}\n", styles: [":host{position:relative;width:100%;flex:1 1 0}.fields{display:flex;flex-direction:column;flex-wrap:wrap;width:100%}.fields-container{display:flex;flex-direction:column}.field-item{display:flex;flex-direction:row;gap:8px}.actions{display:flex;flex-direction:row;gap:8px;align-items:center;margin-top:0;padding:0;min-width:24px}.title{display:flex;flex-direction:column;gap:8px;align-items:center}nff-dynamic-recursive-form{flex:1}.add-button{background-color:var(--mat-sys-primary-container)}.empty-add-button{width:100%}\n"] }]
1106
1167
  }], propDecorators: { nodeGroupList: [{
1107
1168
  type: Input
1108
1169
  }], initialValue: [{
@@ -1121,6 +1182,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1121
1182
  type: Input
1122
1183
  }], message: [{
1123
1184
  type: Output
1185
+ }], removeList: [{
1186
+ type: Output
1124
1187
  }], items: [{
1125
1188
  type: ViewChildren,
1126
1189
  args: [forwardRef(() => DynamicRecursiveFormComponent)]
@@ -1181,7 +1244,11 @@ class LeafListRendererComponent {
1181
1244
  initialValue;
1182
1245
  formArray;
1183
1246
  editable = true;
1184
- minItems = 1;
1247
+ // Schema-driven bounds: absent `minItems` floors at 0 (an unbounded list can
1248
+ // be emptied), absent `maxItems` is unbounded. Read from the schema in
1249
+ // ngOnInit so a bare `[leaf_]` binding honors them without extra wiring.
1250
+ minItems = 0;
1251
+ maxItems = Number.POSITIVE_INFINITY;
1185
1252
  /**
1186
1253
  * The parent group's grid layout (its `fieldsLayout` styles). A stacked list
1187
1254
  * spans the parent's full row, so its entries repeat the same tracks and
@@ -1190,6 +1257,12 @@ class LeafListRendererComponent {
1190
1257
  */
1191
1258
  layout = null;
1192
1259
  message = new EventEmitter();
1260
+ /**
1261
+ * Emitted from the empty-list remove button when the list is a presence list —
1262
+ * the host removes the whole list (de-materializes it, → absent). Only wired
1263
+ * for a `presence` `leafList`; the parent maps it to `setNodePresence`.
1264
+ */
1265
+ removeList = new EventEmitter();
1193
1266
  matDialog = inject(MatDialog);
1194
1267
  /**
1195
1268
  * Take a full-width row of its own once the array holds more than one entry.
@@ -1213,8 +1286,28 @@ class LeafListRendererComponent {
1213
1286
  this.formArray.patchValue(this.initialValue);
1214
1287
  }
1215
1288
  }
1289
+ // Effective bounds: the schema wins when it declares them (an explicit
1290
+ // `minItems: 0` included), else the `@Input` fallback for a host that binds
1291
+ // the renderer directly. Getters, so a schema rebind is reflected live.
1292
+ get effectiveMin() {
1293
+ return this.leaf_?.minItems ?? this.minItems;
1294
+ }
1295
+ get effectiveMax() {
1296
+ return this.leaf_?.maxItems ?? this.maxItems;
1297
+ }
1298
+ /** Whether another item may be appended (below the effective maximum). */
1299
+ get canAdd() {
1300
+ return (this.formArray?.length ?? 0) < this.effectiveMax;
1301
+ }
1216
1302
  removeItem($index) {
1217
- if (this.formArray.length <= this.minItems) {
1303
+ // A presence (optional / advisoryRequired) list has no present-empty state:
1304
+ // removing its last entry de-materializes the whole list (→ absent). A
1305
+ // required (non-presence) list instead stops at its minimum, staying `[]`.
1306
+ if (this.leaf_?.presence && this.formArray.length === 1) {
1307
+ this.removeList.emit();
1308
+ return;
1309
+ }
1310
+ if (this.formArray.length <= this.effectiveMin) {
1218
1311
  this.message.emit({
1219
1312
  message: 'You cannot remove the last item!',
1220
1313
  type: 'error',
@@ -1224,10 +1317,12 @@ class LeafListRendererComponent {
1224
1317
  this.formArray.removeAt($index);
1225
1318
  }
1226
1319
  addItem() {
1320
+ if (this.formArray.length >= this.effectiveMax)
1321
+ return;
1227
1322
  this.formArray.push(new FormControl());
1228
1323
  }
1229
1324
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1230
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafListRendererComponent, isStandalone: true, selector: "nff-leaf-list-renderer", inputs: { leaf_: "leaf_", initialValue: "initialValue", formArray: "formArray", editable: "editable", minItems: "minItems", layout: "layout" }, outputs: { message: "message" }, host: { properties: { "class.stacked": "this.stacked", "class": "this.typeClass", "style": "this.hostLayout" } }, ngImport: i0, template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"minItems == null || formArray.length > minItems\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > minItems) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 0}:host(.list-type-string) .field-item,:host(.list-type-enum) .field-item{min-width:var(--nff-min-text-field-width, 0px)}:host(.list-type-number) .field-item{min-width:var(--nff-min-number-field-width, 0px);max-width:var(--nff-max-number-field-width, none)}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{display:flex;flex:1 1 auto;min-width:0}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.actions{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.actions{transition:none}}@media (hover: hover){.field-item .actions{max-width:0;margin-left:-4px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.field-item:hover .actions,.field-item:focus-within .actions{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: AnonLeafRendererComponent, selector: "nff-anon-leaf-renderer", inputs: ["AnonLeaf", "initialValue", "control", "removable", "editable", "index", "label"], outputs: ["remove"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1325
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: LeafListRendererComponent, isStandalone: true, selector: "nff-leaf-list-renderer", inputs: { leaf_: "leaf_", initialValue: "initialValue", formArray: "formArray", editable: "editable", minItems: "minItems", maxItems: "maxItems", layout: "layout" }, outputs: { message: "message", removeList: "removeList" }, host: { properties: { "class.stacked": "this.stacked", "class": "this.typeClass", "style": "this.hostLayout" } }, ngImport: i0, template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"formArray.length > effectiveMin\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > effectiveMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last && canAdd) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n @if (editable && formArray.length === 0 && canAdd) {\n <!-- A required (non-presence) list can be present-but-empty; this labeled\n affordance adds the first entry so an empty list is not a dead end\n (mirrors the node-group list's \"Add \u2026 #1\"). A presence list never\n renders empty here \u2014 its first entry appears on materialize, and\n removing the last entry de-materializes it. -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ leaf_.label ?? leaf_.name }} item\n </button>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 auto}:host(.list-type-string) .field-item,:host(.list-type-enum) .field-item{min-width:var(--nff-min-text-field-width, 220px)}:host(.list-type-number) .field-item{min-width:var(--nff-min-number-field-width, 140px);max-width:var(--nff-max-number-field-width, none)}.empty-add-button{width:100%}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{display:flex;flex:1 1 auto;min-width:0}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.actions{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.actions{transition:none}}@media (hover: hover){.field-item .actions{max-width:0;margin-left:-4px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.field-item:hover .actions,.field-item:focus-within .actions{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: AnonLeafRendererComponent, selector: "nff-anon-leaf-renderer", inputs: ["AnonLeaf", "initialValue", "control", "removable", "editable", "index", "label"], outputs: ["remove"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1231
1326
  }
1232
1327
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: LeafListRendererComponent, decorators: [{
1233
1328
  type: Component,
@@ -1237,7 +1332,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1237
1332
  MatButtonModule,
1238
1333
  MatPrefix,
1239
1334
  MatTooltip,
1240
- ], template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"minItems == null || formArray.length > minItems\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > minItems) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 0}:host(.list-type-string) .field-item,:host(.list-type-enum) .field-item{min-width:var(--nff-min-text-field-width, 0px)}:host(.list-type-number) .field-item{min-width:var(--nff-min-number-field-width, 0px);max-width:var(--nff-max-number-field-width, none)}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{display:flex;flex:1 1 auto;min-width:0}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.actions{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.actions{transition:none}}@media (hover: hover){.field-item .actions{max-width:0;margin-left:-4px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.field-item:hover .actions,.field-item:focus-within .actions{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1335
+ ], template: "@if (formArray && leaf_) {\n @for (control of formArray.controls; track $index) {\n <div class=\"field-item\">\n <nff-anon-leaf-renderer\n [AnonLeaf]=\"leaf_\" [control]=\"control\"\n [removable]=\"formArray.length > effectiveMin\"\n [index]=\"$index\"\n [editable]=\"editable\"\n [label]=\"leaf_.label ?? leaf_.name\"\n (remove)=\"removeItem($index)\"\n />\n @if (editable) {\n <div class=\"actions\">\n @if (formArray.length > effectiveMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Remove ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"removeItem($index)\">\n <mat-icon matPrefix>delete</mat-icon>\n </button>\n }\n @if ($last && canAdd) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\" (click)=\"addItem()\">\n <mat-icon matPrefix>add</mat-icon>\n </button>\n }\n <div class=\"actions-spacer\"></div>\n </div>\n }\n </div>\n }\n @if (editable && formArray.length === 0 && canAdd) {\n <!-- A required (non-presence) list can be present-but-empty; this labeled\n affordance adds the first entry so an empty list is not a dead end\n (mirrors the node-group list's \"Add \u2026 #1\"). A presence list never\n renders empty here \u2014 its first entry appears on materialize, and\n removing the last entry de-materializes it. -->\n <button\n matButton=\"outlined\"\n class=\"empty-add-button\"\n [attr.aria-label]=\"'Add ' + (leaf_.label ?? leaf_.name) + ' item'\"\n (click)=\"addItem()\"\n >\n <mat-icon>add</mat-icon> Add {{ leaf_.label ?? leaf_.name }} item\n </button>\n }\n}\n", styles: [":host{display:flex;flex-direction:row;flex-wrap:wrap;gap:16px}:host(.stacked){flex-basis:100%}.fields{position:relative;display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.field-item{display:flex;flex-direction:row;gap:4px;align-items:center;flex:1 1 auto}:host(.list-type-string) .field-item,:host(.list-type-enum) .field-item{min-width:var(--nff-min-text-field-width, 220px)}:host(.list-type-number) .field-item{min-width:var(--nff-min-number-field-width, 140px);max-width:var(--nff-max-number-field-width, none)}.empty-add-button{width:100%}.actions{z-index:100;display:flex;flex-direction:column;align-items:flex-end;gap:4px;height:100%}.actions-spacer{height:20px}nff-anon-leaf-renderer{display:flex;flex:1 1 auto;min-width:0}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.title{display:flex;flex-direction:row;gap:8px;align-items:center}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.actions{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.actions{transition:none}}@media (hover: hover){.field-item .actions{max-width:0;margin-left:-4px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.field-item:hover .actions,.field-item:focus-within .actions{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1241
1336
  }], propDecorators: { leaf_: [{
1242
1337
  type: Input
1243
1338
  }], initialValue: [{
@@ -1248,10 +1343,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1248
1343
  type: Input
1249
1344
  }], minItems: [{
1250
1345
  type: Input
1346
+ }], maxItems: [{
1347
+ type: Input
1251
1348
  }], layout: [{
1252
1349
  type: Input
1253
1350
  }], message: [{
1254
1351
  type: Output
1352
+ }], removeList: [{
1353
+ type: Output
1255
1354
  }], stacked: [{
1256
1355
  type: HostBinding,
1257
1356
  args: ['class.stacked']
@@ -1313,6 +1412,16 @@ class NodeMapRendererComponent {
1313
1412
  get valueGroup() {
1314
1413
  return this.nodeMap.value;
1315
1414
  }
1415
+ /** Narrowing casts for the complex value kinds the template renders. */
1416
+ asLeafList(v) {
1417
+ return v;
1418
+ }
1419
+ asNodeGroupList(v) {
1420
+ return v;
1421
+ }
1422
+ asNodeMap(v) {
1423
+ return v;
1424
+ }
1316
1425
  get atMax() {
1317
1426
  return this.nodeMap.maxEntries != null && this.entryKeys.length >= this.nodeMap.maxEntries;
1318
1427
  }
@@ -1344,8 +1453,9 @@ class NodeMapRendererComponent {
1344
1453
  }
1345
1454
  asFormControl = asFormControl;
1346
1455
  asFormGroup = asFormGroup;
1456
+ asFormArray = asFormArray;
1347
1457
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1348
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeMapRendererComponent, isStandalone: true, selector: "nff-node-map-renderer", inputs: { nodeMap: "nodeMap", formGroup: "formGroup", editable: "editable", showAbsentOptionals: "showAbsentOptionals", inheritedAppearance: "inheritedAppearance" }, usesOnChanges: true, ngImport: i0, template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <!-- On a rejected rename (empty, duplicate, keyPattern violation) the\n input snaps back to the committed key, so the display never\n disagrees with the key getRawValue() will emit. -->\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value); keyInput.value = key\"\n >\n </mat-form-field>\n\n <!-- Index access, not .get(): entry keys are arbitrary runtime data and\n .get() would split a key like 'web.example.com' into a dotted path. -->\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n }\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + key + ' entry'\" [attr.aria-label]=\"'Remove ' + key + ' entry'\" (click)=\"removeEntry(key)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n }\n @if (editable && !atMax) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" [attr.aria-label]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" (click)=\"addEntry()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </div>\n}\n", styles: [":host{display:flex;flex:1 1 100%}.map-node{display:flex;flex-direction:column;gap:8px;width:100%}.map-entry{display:flex;flex-direction:row;gap:8px;align-items:flex-start}.key-field{flex:0 0 30%;min-width:120px}.value-field{display:flex;flex:1 1 0;min-width:0}.add-button{align-self:flex-start;background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.6;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.remove-button{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.remove-button{transition:none}}@media (hover: hover){.map-entry .remove-button{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.map-entry:hover .remove-button,.map-entry:focus-within .remove-button{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIconButton), selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i1$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatFormFieldModule) }, { kind: "component", type: i0.forwardRef(() => i2.MatFormField), selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i0.forwardRef(() => i2.MatLabel), selector: "mat-label" }, { kind: "ngmodule", type: i0.forwardRef(() => MatInputModule) }, { kind: "directive", type: i0.forwardRef(() => i5.MatInput), selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: i0.forwardRef(() => MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus", "placeholder"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }] });
1458
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: NodeMapRendererComponent, isStandalone: true, selector: "nff-node-map-renderer", inputs: { nodeMap: "nodeMap", formGroup: "formGroup", editable: "editable", showAbsentOptionals: "showAbsentOptionals", inheritedAppearance: "inheritedAppearance" }, usesOnChanges: true, ngImport: i0, template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <!-- On a rejected rename (empty, duplicate, keyPattern violation) the\n input snaps back to the committed key, so the display never\n disagrees with the key getRawValue() will emit. -->\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value); keyInput.value = key\"\n >\n </mat-form-field>\n\n <!-- Index access, not .get(): entry keys are arbitrary runtime data and\n .get() would split a key like 'web.example.com' into a dotted path. -->\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"asLeafList(nodeMap.value)\"\n [formArray]=\"formGroup.controls[key]\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroupList') {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [nodeGroupList]=\"asNodeGroupList(nodeMap.value)\"\n [formArray]=\"asFormArray(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'map') {\n <nff-node-map-renderer\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [nodeMap]=\"asNodeMap(nodeMap.value)\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n }\n <!-- A choice-valued map (additionalProperties: anyOf/oneOf) has no\n standalone bare-choice renderer; use the config editor for it. -->\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + key + ' entry'\" [attr.aria-label]=\"'Remove ' + key + ' entry'\" (click)=\"removeEntry(key)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n }\n @if (editable && !atMax) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" [attr.aria-label]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" (click)=\"addEntry()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </div>\n}\n", styles: [":host{display:flex;flex:1 1 100%}.map-node{display:flex;flex-direction:column;gap:8px;width:100%}.map-entry{display:flex;flex-direction:row;gap:8px;align-items:flex-start}.key-field{flex:0 0 30%;min-width:120px}.value-field{display:flex;flex:1 1 0;min-width:0}.add-button{align-self:flex-start;background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.6;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.remove-button{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.remove-button{transition:none}}@media (hover: hover){.map-entry .remove-button{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.map-entry:hover .remove-button,.map-entry:focus-within .remove-button{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable", "showAbsentOptionals", "inheritedAppearance"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$2.MatIconButton), selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatFormFieldModule) }, { kind: "component", type: i0.forwardRef(() => i2.MatFormField), selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i0.forwardRef(() => i2.MatLabel), selector: "mat-label" }, { kind: "ngmodule", type: i0.forwardRef(() => MatInputModule) }, { kind: "directive", type: i0.forwardRef(() => i5.MatInput), selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: i0.forwardRef(() => MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus", "placeholder"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems", "maxItems", "layout"], outputs: ["message", "removeList"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "showAbsentOptionals", "minItems", "maxItems", "inheritedAppearance"], outputs: ["message", "removeList"] }, { kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }] });
1349
1459
  }
1350
1460
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: NodeMapRendererComponent, decorators: [{
1351
1461
  type: Component,
@@ -1356,10 +1466,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1356
1466
  MatInputModule,
1357
1467
  MatTooltip,
1358
1468
  LeafRendererComponent,
1359
- // node-map-renderer and dynamic-recursive-form import each other (a map value
1360
- // may be a group); forwardRef defers the reference to break the cycle.
1469
+ LeafListRendererComponent,
1470
+ NodeGroupListRendererComponent,
1471
+ // node-map-renderer references itself (a map-valued map) and the form (a
1472
+ // group-valued map), which import each other; forwardRef breaks the cycles.
1473
+ forwardRef(() => NodeMapRendererComponent),
1361
1474
  forwardRef(() => DynamicRecursiveFormComponent),
1362
- ], template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <!-- On a rejected rename (empty, duplicate, keyPattern violation) the\n input snaps back to the committed key, so the display never\n disagrees with the key getRawValue() will emit. -->\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value); keyInput.value = key\"\n >\n </mat-form-field>\n\n <!-- Index access, not .get(): entry keys are arbitrary runtime data and\n .get() would split a key like 'web.example.com' into a dotted path. -->\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n }\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + key + ' entry'\" [attr.aria-label]=\"'Remove ' + key + ' entry'\" (click)=\"removeEntry(key)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n }\n @if (editable && !atMax) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" [attr.aria-label]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" (click)=\"addEntry()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </div>\n}\n", styles: [":host{display:flex;flex:1 1 100%}.map-node{display:flex;flex-direction:column;gap:8px;width:100%}.map-entry{display:flex;flex-direction:row;gap:8px;align-items:flex-start}.key-field{flex:0 0 30%;min-width:120px}.value-field{display:flex;flex:1 1 0;min-width:0}.add-button{align-self:flex-start;background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.6;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.remove-button{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.remove-button{transition:none}}@media (hover: hover){.map-entry .remove-button{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.map-entry:hover .remove-button,.map-entry:focus-within .remove-button{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1475
+ ], template: "@if (formGroup && nodeMap) {\n <div class=\"map-node\">\n @for (key of entryKeys; track key) {\n <div class=\"map-entry\">\n <mat-form-field class=\"key-field\" [appearance]=\"editable ? 'fill' : 'outline'\">\n <mat-label>{{ nodeMap.keyLabel ?? 'Key' }}</mat-label>\n <!-- On a rejected rename (empty, duplicate, keyPattern violation) the\n input snaps back to the committed key, so the display never\n disagrees with the key getRawValue() will emit. -->\n <input\n matInput\n #keyInput\n [readonly]=\"!editable\"\n [value]=\"key\"\n (change)=\"renameEntry(key, keyInput.value); keyInput.value = key\"\n >\n </mat-form-field>\n\n <!-- Index access, not .get(): entry keys are arbitrary runtime data and\n .get() would split a key like 'web.example.com' into a dotted path. -->\n <div class=\"value-field\">\n @if (nodeMap.value.kind === 'leaf') {\n <nff-leaf-renderer\n [leaf_]=\"valueLeaf\"\n [control]=\"asFormControl(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [schema]=\"valueGroup\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'leafList') {\n <nff-leaf-list-renderer\n [leaf_]=\"asLeafList(nodeMap.value)\"\n [formArray]=\"formGroup.controls[key]\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'nodeGroupList') {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [nodeGroupList]=\"asNodeGroupList(nodeMap.value)\"\n [formArray]=\"asFormArray(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n } @else if (nodeMap.value.kind === 'map') {\n <nff-node-map-renderer\n [inheritedAppearance]=\"entryAppearance\"\n [showAbsentOptionals]=\"showAbsentOptionals\"\n [nodeMap]=\"asNodeMap(nodeMap.value)\"\n [formGroup]=\"asFormGroup(formGroup.controls[key])\"\n [editable]=\"editable\"\n />\n }\n <!-- A choice-valued map (additionalProperties: anyOf/oneOf) has no\n standalone bare-choice renderer; use the config editor for it. -->\n </div>\n\n @if (editable && !atMin) {\n <button class=\"remove-button small-icon-button\" matIconButton [matTooltip]=\"'Remove ' + key + ' entry'\" [attr.aria-label]=\"'Remove ' + key + ' entry'\" (click)=\"removeEntry(key)\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n }\n @if (editable && !atMax) {\n <button class=\"add-button small-icon-button\" matIconButton [matTooltip]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" [attr.aria-label]=\"'Add ' + (nodeMap.label ?? nodeMap.name) + ' entry'\" (click)=\"addEntry()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n </div>\n}\n", styles: [":host{display:flex;flex:1 1 100%}.map-node{display:flex;flex-direction:column;gap:8px;width:100%}.map-entry{display:flex;flex-direction:row;gap:8px;align-items:flex-start}.key-field{flex:0 0 30%;min-width:120px}.value-field{display:flex;flex:1 1 0;min-width:0}.add-button{align-self:flex-start;background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.6;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.remove-button{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.remove-button{transition:none}}@media (hover: hover){.map-entry .remove-button{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.map-entry:hover .remove-button,.map-entry:focus-within .remove-button{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1363
1476
  }], propDecorators: { nodeMap: [{
1364
1477
  type: Input
1365
1478
  }], formGroup: [{
@@ -1554,18 +1667,11 @@ class DynamicRecursiveFormComponent {
1554
1667
  * Add or remove a presence node's control on this form — any presence kind:
1555
1668
  * group, leaf, map, or choice. Removing it drops the key from `form.value`;
1556
1669
  * adding it builds the control fresh from its schema (nested presence
1557
- * children start absent).
1670
+ * children start absent). Delegates to {@link setNodePresence}, the
1671
+ * host-callable primitive.
1558
1672
  */
1559
1673
  toggleNodePresence(key, schema, present) {
1560
- const group = this.formGroup();
1561
- if (present) {
1562
- if (!group.get(key)) {
1563
- group.addControl(key, buildControl(schema));
1564
- }
1565
- }
1566
- else if (group.get(key)) {
1567
- group.removeControl(key);
1568
- }
1674
+ setNodePresence(this.formGroup(), schema, key, present);
1569
1675
  // Either way the key's ghost stand-in is stale: drop it, so a later
1570
1676
  // re-ghosting renders a pristine one even if the old instance was mutated.
1571
1677
  this.ghostControls.delete(key);
@@ -1622,6 +1728,17 @@ class DynamicRecursiveFormComponent {
1622
1728
  if (!present && this.presenceFocusKey === key)
1623
1729
  this.presenceFocusKey = null;
1624
1730
  }
1731
+ /**
1732
+ * Materialize an absent presence (optional / `advisoryRequired`) list *with
1733
+ * its first entry* — clicking "Add <list>" makes the first entry appear at
1734
+ * once. An optional list has no present-empty state: removing the last entry
1735
+ * de-materializes it (→ absent). A required list is always present instead and
1736
+ * uses the renderer's own "Add item" affordance, so it never routes here.
1737
+ */
1738
+ addPresenceList(key, schema) {
1739
+ setNodePresence(this.formGroup(), schema, key, true, [null]);
1740
+ this.ghostControls.delete(key);
1741
+ }
1625
1742
  objectKeys(obj) {
1626
1743
  return Object.keys(obj);
1627
1744
  }
@@ -1666,7 +1783,7 @@ class DynamicRecursiveFormComponent {
1666
1783
  asFormArray = asFormArray;
1667
1784
  asFormControl = asFormControl;
1668
1785
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1669
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: DynamicRecursiveFormComponent, isStandalone: true, selector: "nff-dynamic-recursive-form", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, showAbsentOptionals: { classPropertyName: "showAbsentOptionals", publicName: "showAbsentOptionals", isSignal: true, isRequired: false, transformFunction: null }, addButtonCallback: { classPropertyName: "addButtonCallback", publicName: "addButtonCallback", isSignal: true, isRequired: false, transformFunction: null }, addButtonLabel: { classPropertyName: "addButtonLabel", publicName: "addButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, focusLeaf: { classPropertyName: "focusLeaf", publicName: "focusLeaf", isSignal: true, isRequired: false, transformFunction: null }, inheritedAppearance: { classPropertyName: "inheritedAppearance", publicName: "inheritedAppearance", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove", editable: "editableChange" }, ngImport: i0, template: "<!-- Checkbox fields grouped by appearance.booleanFields: a compact wrapping\n row of natural-width items, rendered before or after the field flow. -->\n<ng-template #booleanArea>\n @if (booleanAreaVisible()) {\n <div class=\"boolean-fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.type == 'boolean') {\n @if (!child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n } @else if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n @if (showAbsentOptionals()) {\n <!-- Ghost preview of an absent boolean: unchecked, display-only,\n no control in the form; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n }\n</ng-template>\n<!-- The node's scalar-field area \u2014 gathered booleans (beginning), the .fields\n flow, gathered booleans (end) \u2014 shared by the root and non-root branches. -->\n<ng-template #fieldFlow>\n @if (booleanPlacement() === 'beginning') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n <div class=\"fields\" [class.grid-fields]=\"!!fieldsLayout()\" [class.grid-cols]=\"gridHasCols()\" [style]=\"fieldsLayout()\" [style.--nff-min-text-field-width]=\"textFieldMin()\" [style.--nff-min-number-field-width]=\"numberFieldMin()\" [style.--nff-max-number-field-width]=\"numberFieldMax()\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence && inFieldFlow(child)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence && inFieldFlow(child)) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n @if (showAbsentOptionals()) {\n <!-- Ghost preview: the field itself, read-only against a detached\n null control (never part of the form value), default as\n placeholder; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n [placeholder]=\"ghostPlaceholder(child)\"\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n @if (booleanPlacement() === 'end') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n</ng-template>\n@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n </div>\n </ng-template>\n @if (schema().appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton [matTooltip]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton [matTooltip]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!); $event.stopPropagation()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <!-- The edit toggle sits on its own row: inline it would offset the\n first field row against the rows below (flex) or claim a whole\n track (grid). -->\n <div class=\"form-toolbar\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\" [expanded]=\"!!formGroup().get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.node-section{width:100%}.presence-leaf-add{flex:1 1 0;min-width:10%;align-self:flex-start;margin-top:8px;margin-bottom:20px;opacity:.6;transition:opacity .12s ease}.presence-leaf-add:hover,.presence-leaf-add:focus-visible{opacity:1}.ghost-field{display:flex;flex:1 1 0;min-width:10%;gap:8px}.ghost-field nff-leaf-renderer{opacity:.55}.ghost-field .ghost-add{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}.fields.grid-fields{flex:0 0 auto;align-content:start}.fields.grid-fields nff-leaf-renderer,.fields.grid-fields nff-leaf-list-renderer{min-width:0;max-width:none}.fields.grid-fields>button{justify-self:start}.fields.grid-cols nff-leaf-list-renderer.stacked{grid-column:1/-1}.form-toolbar{display:flex;align-items:center}nff-leaf-list-renderer{flex:1}.boolean-fields{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}.boolean-fields nff-leaf-renderer{flex:0 0 auto;min-width:0}.boolean-fields .presence-leaf-add{flex:0 0 auto;min-width:0;margin-top:0;margin-bottom:0;align-self:center}.boolean-fields .ghost-field{flex:0 0 auto;min-width:0;align-items:center}.boolean-fields .ghost-field .ghost-add{margin-top:0;align-self:center}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;width:100%;gap:8px}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.ghost-add{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.ghost-add{transition:none}}@media (hover: hover){.ghost-field .ghost-add{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.ghost-field:hover .ghost-add,.ghost-field:focus-within .ghost-add{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus", "placeholder"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems", "layout"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "showAbsentOptionals", "minItems", "maxItems", "inheritedAppearance"], outputs: ["message"] }, { kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable", "showAbsentOptionals", "inheritedAppearance"] }, { kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "directive", type: i0.forwardRef(() => i1.NgControlStatusGroup), selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i0.forwardRef(() => i1.FormGroupDirective), selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatExpansionModule) }, { kind: "component", type: i0.forwardRef(() => i2$2.MatExpansionPanel), selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i0.forwardRef(() => i2$2.MatExpansionPanelHeader), selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatExpansionPanelTitle), selector: "mat-panel-title" }, { kind: "directive", type: i0.forwardRef(() => i2$2.MatExpansionPanelDescription), selector: "mat-panel-description" }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i1$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatButton), selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIconButton), selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i0.forwardRef(() => NgTemplateOutlet), selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatCheckboxModule) }, { kind: "component", type: i0.forwardRef(() => i4.MatCheckbox), selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatFormFieldModule) }, { kind: "component", type: i0.forwardRef(() => i2.MatFormField), selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i0.forwardRef(() => i2.MatLabel), selector: "mat-label" }, { kind: "ngmodule", type: i0.forwardRef(() => MatSelectModule) }, { kind: "component", type: i0.forwardRef(() => i3.MatSelect), selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i0.forwardRef(() => i3.MatOption), selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i0.forwardRef(() => MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1786
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: DynamicRecursiveFormComponent, isStandalone: true, selector: "nff-dynamic-recursive-form", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, showAbsentOptionals: { classPropertyName: "showAbsentOptionals", publicName: "showAbsentOptionals", isSignal: true, isRequired: false, transformFunction: null }, addButtonCallback: { classPropertyName: "addButtonCallback", publicName: "addButtonCallback", isSignal: true, isRequired: false, transformFunction: null }, addButtonLabel: { classPropertyName: "addButtonLabel", publicName: "addButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, focusLeaf: { classPropertyName: "focusLeaf", publicName: "focusLeaf", isSignal: true, isRequired: false, transformFunction: null }, inheritedAppearance: { classPropertyName: "inheritedAppearance", publicName: "inheritedAppearance", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove", editable: "editableChange" }, ngImport: i0, template: "<!-- Checkbox fields grouped by appearance.booleanFields: a compact wrapping\n row of natural-width items, rendered before or after the field flow. -->\n<ng-template #booleanArea>\n @if (booleanAreaVisible()) {\n <div class=\"boolean-fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.type == 'boolean') {\n @if (!child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n } @else if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n @if (showAbsentOptionals()) {\n <!-- Ghost preview of an absent boolean: unchecked, display-only,\n no control in the form; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n }\n</ng-template>\n<!-- The node's scalar-field area \u2014 gathered booleans (beginning), the .fields\n flow, gathered booleans (end) \u2014 shared by the root and non-root branches. -->\n<ng-template #fieldFlow>\n @if (booleanPlacement() === 'beginning') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n <div class=\"fields\" [class.grid-fields]=\"!!fieldsLayout()\" [class.grid-cols]=\"gridHasCols()\" [style]=\"fieldsLayout()\" [style.--nff-min-text-field-width]=\"textFieldMin()\" [style.--nff-min-number-field-width]=\"numberFieldMin()\" [style.--nff-max-number-field-width]=\"numberFieldMax()\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence && inFieldFlow(child)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList' && !child.presence) {\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n @else if (child.kind == 'leafList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry (no\n empty state). Removing the last entry de-materializes it (the\n renderer's `removeList` \u2192 absent). -->\n @let leafListArray = asFormArray(formGroup().get(childItem.key));\n @if (leafListArray) {\n <div class=\"presence-list\">\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"leafListArray\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence && inFieldFlow(child)) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n @if (showAbsentOptionals()) {\n <!-- Ghost preview: the field itself, read-only against a detached\n null control (never part of the form value), default as\n placeholder; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n [placeholder]=\"ghostPlaceholder(child)\"\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n @if (booleanPlacement() === 'end') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n</ng-template>\n@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && !child.presence) {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'nodeGroupList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry.\n Removing the last entry de-materializes it (`removeList` \u2192 absent). -->\n @let presenceListArray = asFormArray(formGroup().get(childItem.key));\n @if (presenceListArray) {\n <div class=\"presence-list\">\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"presenceListArray\"\n [editable]=\"editable()\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n </div>\n </ng-template>\n @if (schema().appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton [matTooltip]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton [matTooltip]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!); $event.stopPropagation()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <!-- The edit toggle sits on its own row: inline it would offset the\n first field row against the rows below (flex) or claim a whole\n track (grid). -->\n <div class=\"form-toolbar\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && !child.presence) {\n <!-- A nodeGroupList has no `appearance` of its own, so its section is\n always shown; only an ancestor's collapse hides it. -->\n <mat-expansion-panel class=\"node-section\" [expanded]=\"true\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ child.label ?? child.name }}</mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry.\n Removing the last entry de-materializes it (`removeList` \u2192 absent). -->\n @let rootListArray = asFormArray(formGroup().get(childItem.key));\n @if (rootListArray) {\n <div class=\"presence-list\">\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"rootListArray\"\n [editable]=\"editable()\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\" [expanded]=\"!!formGroup().get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.node-section{width:100%}.presence-leaf-add{flex:1 1 0;min-width:10%;align-self:flex-start;margin-top:8px;margin-bottom:20px;opacity:.6;transition:opacity .12s ease}.presence-leaf-add:hover,.presence-leaf-add:focus-visible{opacity:1}.presence-list-add{width:100%;flex-shrink:0;margin:8px 0;opacity:.6;transition:opacity .12s ease}.presence-list-add:hover,.presence-list-add:focus-visible{opacity:1}.presence-list{flex:1 1 100%;width:100%}.ghost-field{display:flex;flex:1 1 0;min-width:10%;gap:8px}.ghost-field nff-leaf-renderer{opacity:.55}.ghost-field .ghost-add{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}.fields.grid-fields{flex:0 0 auto;align-content:start}.fields.grid-fields nff-leaf-renderer,.fields.grid-fields nff-leaf-list-renderer{min-width:0;max-width:none}.fields.grid-fields>button{justify-self:start}.fields.grid-cols nff-leaf-list-renderer.stacked{grid-column:1/-1}.form-toolbar{display:flex;align-items:center}nff-leaf-list-renderer{flex:1}.boolean-fields{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}.boolean-fields nff-leaf-renderer{flex:0 0 auto;min-width:0}.boolean-fields .presence-leaf-add{flex:0 0 auto;min-width:0;margin-top:0;margin-bottom:0;align-self:center}.boolean-fields .ghost-field{flex:0 0 auto;min-width:0;align-items:center}.boolean-fields .ghost-field .ghost-add{margin-top:0;align-self:center}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;width:100%;gap:8px}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.45;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.45;transition:opacity .12s ease}.remove-button:hover,.remove-button:focus-visible{opacity:1}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.ghost-add{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.ghost-add{transition:none}}@media (hover: hover){.ghost-field .ghost-add{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.ghost-field:hover .ghost-add,.ghost-field:focus-within .ghost-add{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => DynamicRecursiveFormComponent), selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "component", type: i0.forwardRef(() => LeafRendererComponent), selector: "nff-leaf-renderer", inputs: ["leaf_", "control", "removable", "editable", "autofocus", "placeholder"], outputs: ["remove"] }, { kind: "component", type: i0.forwardRef(() => LeafListRendererComponent), selector: "nff-leaf-list-renderer", inputs: ["leaf_", "initialValue", "formArray", "editable", "minItems", "maxItems", "layout"], outputs: ["message", "removeList"] }, { kind: "component", type: i0.forwardRef(() => NodeGroupListRendererComponent), selector: "nff-node-group-list-renderer", inputs: ["nodeGroupList", "initialValue", "formArray", "editable", "showAbsentOptionals", "minItems", "maxItems", "inheritedAppearance"], outputs: ["message", "removeList"] }, { kind: "component", type: i0.forwardRef(() => NodeMapRendererComponent), selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable", "showAbsentOptionals", "inheritedAppearance"] }, { kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "directive", type: i0.forwardRef(() => i1.NgControlStatusGroup), selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i0.forwardRef(() => i1.FormGroupDirective), selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatExpansionModule) }, { kind: "component", type: i0.forwardRef(() => i2$3.MatExpansionPanel), selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i0.forwardRef(() => i2$3.MatExpansionPanelHeader), selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i0.forwardRef(() => i2$3.MatExpansionPanelTitle), selector: "mat-panel-title" }, { kind: "directive", type: i0.forwardRef(() => i2$3.MatExpansionPanelDescription), selector: "mat-panel-description" }, { kind: "ngmodule", type: i0.forwardRef(() => MatIconModule) }, { kind: "component", type: i0.forwardRef(() => i2$1.MatIcon), selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatButtonModule) }, { kind: "component", type: i0.forwardRef(() => i2$2.MatButton), selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i0.forwardRef(() => i2$2.MatIconButton), selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i0.forwardRef(() => NgTemplateOutlet), selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatCheckboxModule) }, { kind: "component", type: i0.forwardRef(() => i4.MatCheckbox), selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: i0.forwardRef(() => MatFormFieldModule) }, { kind: "component", type: i0.forwardRef(() => i2.MatFormField), selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i0.forwardRef(() => i2.MatLabel), selector: "mat-label" }, { kind: "ngmodule", type: i0.forwardRef(() => MatSelectModule) }, { kind: "component", type: i0.forwardRef(() => i3.MatSelect), selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i0.forwardRef(() => i3.MatOption), selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i0.forwardRef(() => MatTooltip), selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1670
1787
  }
1671
1788
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: DynamicRecursiveFormComponent, decorators: [{
1672
1789
  type: Component,
@@ -1687,7 +1804,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
1687
1804
  MatFormFieldModule,
1688
1805
  MatSelectModule,
1689
1806
  MatTooltip,
1690
- ], selector: 'nff-dynamic-recursive-form', standalone: true, template: "<!-- Checkbox fields grouped by appearance.booleanFields: a compact wrapping\n row of natural-width items, rendered before or after the field flow. -->\n<ng-template #booleanArea>\n @if (booleanAreaVisible()) {\n <div class=\"boolean-fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.type == 'boolean') {\n @if (!child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n } @else if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n @if (showAbsentOptionals()) {\n <!-- Ghost preview of an absent boolean: unchecked, display-only,\n no control in the form; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n }\n</ng-template>\n<!-- The node's scalar-field area \u2014 gathered booleans (beginning), the .fields\n flow, gathered booleans (end) \u2014 shared by the root and non-root branches. -->\n<ng-template #fieldFlow>\n @if (booleanPlacement() === 'beginning') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n <div class=\"fields\" [class.grid-fields]=\"!!fieldsLayout()\" [class.grid-cols]=\"gridHasCols()\" [style]=\"fieldsLayout()\" [style.--nff-min-text-field-width]=\"textFieldMin()\" [style.--nff-min-number-field-width]=\"numberFieldMin()\" [style.--nff-max-number-field-width]=\"numberFieldMax()\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence && inFieldFlow(child)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList') {\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence && inFieldFlow(child)) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n @if (showAbsentOptionals()) {\n <!-- Ghost preview: the field itself, read-only against a detached\n null control (never part of the form value), default as\n placeholder; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n [placeholder]=\"ghostPlaceholder(child)\"\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n @if (booleanPlacement() === 'end') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n</ng-template>\n@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n </div>\n </ng-template>\n @if (schema().appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton [matTooltip]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton [matTooltip]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!); $event.stopPropagation()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <!-- The edit toggle sits on its own row: inline it would offset the\n first field row against the rows below (flex) or claim a whole\n track (grid). -->\n <div class=\"form-toolbar\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList') {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n {{ child.label ?? child.name }}\n </mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\" [expanded]=\"!!formGroup().get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.node-section{width:100%}.presence-leaf-add{flex:1 1 0;min-width:10%;align-self:flex-start;margin-top:8px;margin-bottom:20px;opacity:.6;transition:opacity .12s ease}.presence-leaf-add:hover,.presence-leaf-add:focus-visible{opacity:1}.ghost-field{display:flex;flex:1 1 0;min-width:10%;gap:8px}.ghost-field nff-leaf-renderer{opacity:.55}.ghost-field .ghost-add{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}.fields.grid-fields{flex:0 0 auto;align-content:start}.fields.grid-fields nff-leaf-renderer,.fields.grid-fields nff-leaf-list-renderer{min-width:0;max-width:none}.fields.grid-fields>button{justify-self:start}.fields.grid-cols nff-leaf-list-renderer.stacked{grid-column:1/-1}.form-toolbar{display:flex;align-items:center}nff-leaf-list-renderer{flex:1}.boolean-fields{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}.boolean-fields nff-leaf-renderer{flex:0 0 auto;min-width:0}.boolean-fields .presence-leaf-add{flex:0 0 auto;min-width:0;margin-top:0;margin-bottom:0;align-self:center}.boolean-fields .ghost-field{flex:0 0 auto;min-width:0;align-items:center}.boolean-fields .ghost-field .ghost-add{margin-top:0;align-self:center}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;width:100%;gap:8px}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.ghost-add{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.ghost-add{transition:none}}@media (hover: hover){.ghost-field .ghost-add{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.ghost-field:hover .ghost-add,.ghost-field:focus-within .ghost-add{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1807
+ ], selector: 'nff-dynamic-recursive-form', standalone: true, template: "<!-- Checkbox fields grouped by appearance.booleanFields: a compact wrapping\n row of natural-width items, rendered before or after the field flow. -->\n<ng-template #booleanArea>\n @if (booleanAreaVisible()) {\n <div class=\"boolean-fields\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.type == 'boolean') {\n @if (!child.presence) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n } @else if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n @if (showAbsentOptionals()) {\n <!-- Ghost preview of an absent boolean: unchecked, display-only,\n no control in the form; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n }\n</ng-template>\n<!-- The node's scalar-field area \u2014 gathered booleans (beginning), the .fields\n flow, gathered booleans (end) \u2014 shared by the root and non-root branches. -->\n<ng-template #fieldFlow>\n @if (booleanPlacement() === 'beginning') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n <div class=\"fields\" [class.grid-fields]=\"!!fieldsLayout()\" [class.grid-cols]=\"gridHasCols()\" [style]=\"fieldsLayout()\" [style.--nff-min-text-field-width]=\"textFieldMin()\" [style.--nff-min-number-field-width]=\"numberFieldMin()\" [style.--nff-max-number-field-width]=\"numberFieldMax()\">\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && !child.presence && inFieldFlow(child)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=false\n [editable]=\"editable()\"\n />\n }\n }\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leafList' && !child.presence) {\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n />\n }\n @else if (child.kind == 'leafList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry (no\n empty state). Removing the last entry de-materializes it (the\n renderer's `removeList` \u2192 absent). -->\n @let leafListArray = asFormArray(formGroup().get(childItem.key));\n @if (leafListArray) {\n <div class=\"presence-list\">\n <nff-leaf-list-renderer\n [layout]=\"gridHasCols() ? fieldsLayout() : null\"\n [leaf_]=\"child\"\n [editable]=\"editable()\"\n [formArray]=\"leafListArray\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n <!-- Presence leaves trail the regular fields so their add buttons don't\n interrupt the field flow; an enabled one keeps its trailing spot. -->\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'leaf' && child.presence && inFieldFlow(child)) {\n @if (formGroup().get(childItem.key)) {\n <nff-leaf-renderer\n [leaf_]=child\n [control]=asFormControl(formGroup().get(childItem.key))\n [removable]=true\n [editable]=\"editable()\"\n [autofocus]=\"presenceFocusKey === childItem.key || focusLeaf() === childItem.key\"\n (remove)=\"toggleLeafPresence(childItem.key, child, false)\"\n />\n } @else if (editable()) {\n <!-- Read-only mode renders nothing for an absent presence leaf:\n the key is simply absent, and add affordances are hidden\n like every other structural control. -->\n @if (showAbsentOptionals()) {\n <!-- Ghost preview: the field itself, read-only against a detached\n null control (never part of the form value), default as\n placeholder; (+) incorporates it. -->\n <div class=\"ghost-field\">\n <nff-leaf-renderer\n [leaf_]=child\n [control]=\"ghostControl(childItem.key)\"\n [removable]=false\n [editable]=false\n [placeholder]=\"ghostPlaceholder(child)\"\n />\n <button\n matIconButton\n class=\"ghost-add small-icon-button\"\n [matTooltip]=\"'Add ' + (child.label ?? child.name)\"\n [attr.aria-label]=\"'Add ' + (child.label ?? child.name)\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon>\n </button>\n </div>\n } @else {\n <button\n matButton=\"outlined\"\n class=\"presence-leaf-add\"\n (click)=\"toggleLeafPresence(childItem.key, child, true)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n }\n }\n </div>\n @if (booleanPlacement() === 'end') {\n <ng-container *ngTemplateOutlet=\"booleanArea\"></ng-container>\n }\n</ng-template>\n@if (!root()) {\n <ng-template #formRender>\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'nodeGroup' && child.presence) {\n <div class=\"presence-group\">\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && !child.presence) {\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n @else if (child.kind == 'nodeGroupList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry.\n Removing the last entry de-materializes it (`removeList` \u2192 absent). -->\n @let presenceListArray = asFormArray(formGroup().get(childItem.key));\n @if (presenceListArray) {\n <div class=\"presence-list\">\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"presenceListArray\"\n [editable]=\"editable()\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n }\n </div>\n </ng-template>\n @if (schema().appearance?.flatten) {\n <div class=\"flattened-form\">\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n <div class=\"flattened-actions\">\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton [matTooltip]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!)\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n </div>\n }\n @else {\n <mat-expansion-panel class=\"node-section\" [expanded]=\"!schema().appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title class=\"config-form-subsection-card\">\n {{ title() ?? schema().label ?? schema().name }}\n </mat-panel-title>\n <mat-panel-description class=\"section-actions\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable()); $event.stopPropagation()\">\n <mat-icon>edit</mat-icon>\n </button>\n @if (editable() && index() != null && addButtonCallback() != null) {\n <button matIconButton [matTooltip]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" [attr.aria-label]=\"addButtonLabel() ?? 'Add new ' + (schema().label ?? schema().name)\" class=\"add-button small-icon-button\" (click)=\"addButtonCallback()!(index()!); $event.stopPropagation()\">\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && removable()) {\n <button matIconButton class=\"remove-button small-icon-button\" matTooltip=\"Remove {{ schema().label ?? schema().name }}\" [attr.aria-label]=\"'Remove ' + (schema().label ?? schema().name)\" (click)=\"emitRemoveEvent(); $event.stopPropagation()\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-panel-description>\n </mat-expansion-panel-header>\n <ng-container *ngTemplateOutlet=\"formRender\"></ng-container>\n </mat-expansion-panel>\n }\n} @else {\n <div class=\"form-content\" [formGroup]=\"formGroup()\">\n <div class=\"leafs-container\">\n <!-- The edit toggle sits on its own row: inline it would offset the\n first field row against the rows below (flex) or claim a whole\n track (grid). -->\n <div class=\"form-toolbar\">\n <button matIconButton class=\"small-icon-button\" [class]=\"{ 'edit-icon': !editable() }\" [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\" (click)=\"editable.set(!editable())\">\n <mat-icon>edit</mat-icon>\n </button>\n </div>\n <ng-container *ngTemplateOutlet=\"fieldFlow\"></ng-container>\n </div>\n @for (childItem of nodeGroupChildrenList; track $index) {\n @let child = childItem.value;\n @if (child.kind == 'choice') {\n @let choiceGroup = asFormGroup(formGroup().get(childItem.key));\n @let selectedCase = activeCase(childItem.key);\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!choiceGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!choiceGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (choiceGroup) {\n <div class=\"choice-group\">\n <mat-form-field [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>Selected option</mat-label>\n <mat-select\n [value]=\"activeCase(childItem.key)\"\n (selectionChange)=\"switchCase(childItem.key, child, $event.value)\"\n >\n @for (caseName of objectKeys(child.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(child, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n @if (selectedCase) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"caseAsGroup(child, selectedCase)\"\n [formGroup]=\"choiceGroup\"\n [editable]=\"editable()\"\n />\n }\n </div>\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'map') {\n @let mapGroup = asFormGroup(formGroup().get(childItem.key));\n <mat-expansion-panel class=\"node-section\" [expanded]=\"child.presence ? !!mapGroup : !child.appearance?.collapsed\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n @if (child.presence) {\n <mat-checkbox\n [checked]=\"!!mapGroup\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n } @else {\n {{ child.label ?? child.name }}\n }\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (mapGroup) {\n <nff-node-map-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeMap]=\"child\"\n [formGroup]=\"mapGroup\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && !child.presence) {\n <!-- A nodeGroupList has no `appearance` of its own, so its section is\n always shown; only an ancestor's collapse hides it. -->\n <mat-expansion-panel class=\"node-section\" [expanded]=\"true\" togglePosition=\"before\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ child.label ?? child.name }}</mat-panel-title>\n </mat-expansion-panel-header>\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"asFormArray(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroupList' && child.presence) {\n <!-- Optional (advisoryRequired) list: an \"Add <list>\" button while\n absent; clicking it materializes the list with its first entry.\n Removing the last entry de-materializes it (`removeList` \u2192 absent). -->\n @let rootListArray = asFormArray(formGroup().get(childItem.key));\n @if (rootListArray) {\n <div class=\"presence-list\">\n <nff-node-group-list-renderer\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [nodeGroupList]=\"child\"\n [formArray]=\"rootListArray\"\n [editable]=\"editable()\"\n (removeList)=\"toggleNodePresence(childItem.key, child, false)\"\n />\n </div>\n } @else if (editable()) {\n <button\n matButton=\"outlined\"\n class=\"presence-list-add\"\n (click)=\"addPresenceList(childItem.key, child)\"\n >\n <mat-icon>add</mat-icon> Add {{ child.label ?? child.name }}\n </button>\n }\n }\n @else if (child.kind == 'nodeGroup' && child.presence) {\n <mat-expansion-panel class=\"node-section\" togglePosition=\"before\" [expanded]=\"!!formGroup().get(childItem.key)\">\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-checkbox\n [checked]=\"!!formGroup().get(childItem.key)\"\n [disabled]=\"!editable()\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleNodePresence(childItem.key, child, $event.checked)\"\n >{{ child.label ?? child.name }}</mat-checkbox>\n </mat-panel-title>\n </mat-expansion-panel-header>\n @if (formGroup().get(childItem.key)) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"flatGroup(child)\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [editable]=\"editable()\"\n />\n }\n </mat-expansion-panel>\n }\n @else if (child.kind == 'nodeGroup') {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"childAppearance()\" [showAbsentOptionals]=\"showAbsentOptionals()\"\n [schema]=\"child\"\n [formGroup]=\"asFormGroup(formGroup().get(childItem.key))\"\n [initialValue]=\"formGroup().get(childItem.key)?.value\"\n [editable]=\"editable()\"\n />\n }\n }\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;flex-wrap:wrap;width:100%;margin:4px 0}.presence-group,.choice-group{display:flex;flex-direction:column;gap:8px}.node-section{width:100%}.presence-leaf-add{flex:1 1 0;min-width:10%;align-self:flex-start;margin-top:8px;margin-bottom:20px;opacity:.6;transition:opacity .12s ease}.presence-leaf-add:hover,.presence-leaf-add:focus-visible{opacity:1}.presence-list-add{width:100%;flex-shrink:0;margin:8px 0;opacity:.6;transition:opacity .12s ease}.presence-list-add:hover,.presence-list-add:focus-visible{opacity:1}.presence-list{flex:1 1 100%;width:100%}.ghost-field{display:flex;flex:1 1 0;min-width:10%;gap:8px}.ghost-field nff-leaf-renderer{opacity:.55}.ghost-field .ghost-add{flex:0 0 auto;align-self:flex-start;margin-top:4px;background-color:var(--mat-sys-primary-container);color:var(--mat-sys-on-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.fields{display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;align-items:flex-start;flex:1 1 0}.fields.grid-fields{flex:0 0 auto;align-content:start}.fields.grid-fields nff-leaf-renderer,.fields.grid-fields nff-leaf-list-renderer{min-width:0;max-width:none}.fields.grid-fields>button{justify-self:start}.fields.grid-cols nff-leaf-list-renderer.stacked{grid-column:1/-1}.form-toolbar{display:flex;align-items:center}nff-leaf-list-renderer{flex:1}.boolean-fields{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}.boolean-fields nff-leaf-renderer{flex:0 0 auto;min-width:0}.boolean-fields .presence-leaf-add{flex:0 0 auto;min-width:0;margin-top:0;margin-bottom:0;align-self:center}.boolean-fields .ghost-field{flex:0 0 auto;min-width:0;align-items:center}.boolean-fields .ghost-field .ghost-add{margin-top:0;align-self:center}.mat-expansion-panel-content{display:flex;flex-direction:row;gap:16px}.form-content{display:flex;flex-direction:column;width:100%;gap:8px}.leafs-container{display:flex;flex-direction:column;gap:8px;flex:1 1 0}mat-panel-description{display:flex;flex-direction:row}.section-actions{display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-end;width:100%}.edit-icon{opacity:.2;animation:icon-released .5s}.edit-icon:hover{opacity:1;animation:icon-hovered .5s}.flattened-actions{display:flex;flex-direction:column;align-items:flex-start;align-self:flex-start;gap:4px}.add-button{background-color:var(--mat-sys-primary-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.45;transition:opacity .12s ease}.add-button:hover,.add-button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f;opacity:.45;transition:opacity .12s ease}.remove-button:hover,.remove-button:focus-visible{opacity:1}.flattened-form{display:flex;flex-direction:row;width:100%;gap:4px}@keyframes icon-hovered{0%{opacity:.4}}@keyframes icon-released{0%{opacity:1}to{opacity:.4}}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.ghost-add{flex-shrink:0;transition:opacity .15s ease,max-width .15s ease,margin-left .15s ease,transform .15s ease}@media (prefers-reduced-motion: reduce){.ghost-add{transition:none}}@media (hover: hover){.ghost-field .ghost-add{max-width:0;margin-left:-8px;opacity:0;transform:translate(8px);overflow:hidden;pointer-events:none}.ghost-field:hover .ghost-add,.ghost-field:focus-within .ghost-add{max-width:48px;margin-left:0;opacity:1;transform:none;overflow:visible;pointer-events:auto}}\n"] }]
1691
1808
  }], propDecorators: { schema: [{ type: i0.Input, args: [{ isSignal: true, alias: "schema", required: true }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: false }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], remove: [{ type: i0.Output, args: ["remove"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }, { type: i0.Output, args: ["editableChange"] }], showAbsentOptionals: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAbsentOptionals", required: false }] }], addButtonCallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "addButtonCallback", required: false }] }], addButtonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "addButtonLabel", required: false }] }], focusLeaf: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusLeaf", required: false }] }], inheritedAppearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "inheritedAppearance", required: false }] }] } });
1692
1809
 
1693
1810
  /**
@@ -1763,6 +1880,7 @@ class ConfigEditorComponent {
1763
1880
  shape = '';
1764
1881
  changes;
1765
1882
  host = inject(ElementRef);
1883
+ cdr = inject(ChangeDetectorRef);
1766
1884
  /** Stable per-instance ids for the shape signature, so replacing a control (setControl) reads as a structural change. */
1767
1885
  controlIds = new WeakMap();
1768
1886
  controlIdSeq = 0;
@@ -1787,10 +1905,40 @@ class ConfigEditorComponent {
1787
1905
  this.shape = this.shapeOf(group);
1788
1906
  this.expanded.add(this.root.id);
1789
1907
  this.select(this.root);
1790
- // The detail sections mutate the FormGroup directly (presence toggles,
1791
- // list items, map entries, case switches); a shape change there must
1792
- // reflect in the tree.
1793
- this.changes = group.valueChanges.subscribe(() => this.syncShape());
1908
+ // Any change to the bound form — whether from the detail sections or from
1909
+ // an external holder of the same FormGroup must reach the detail pane.
1910
+ // Leaf inputs self-heal (the reactive ValueAccessor writes the new value
1911
+ // synchronously), but the pane's own non-reactive displays (the case
1912
+ // selector, a map entry's key) are one-shot bindings; mark the view so
1913
+ // they re-read under OnPush and zoneless change detection. A shape change
1914
+ // additionally rebuilds the tree.
1915
+ this.changes = group.valueChanges.subscribe(() => {
1916
+ this.cdr.markForCheck();
1917
+ this.syncShape();
1918
+ });
1919
+ }
1920
+ /**
1921
+ * Force the editor to re-read the bound form. A leaf field normally reflects
1922
+ * an external `setValue` on its own — the reactive value accessor writes the
1923
+ * value synchronously, no change detection needed — so this is only for the
1924
+ * corners that bypass that: a structural change made with
1925
+ * `{ emitEvent: false }` (the tree wouldn't otherwise rebuild), a non-reactive
1926
+ * display (the case selector, a map key) that needs re-reading, or a host
1927
+ * whose change detector was detached during the mutation.
1928
+ *
1929
+ * It re-syncs the tree structure and **tears down and recreates** the detail
1930
+ * sections, so every rendered control rebinds to the live form and re-pulls
1931
+ * its current value — even a field whose bound input somehow missed the
1932
+ * external write.
1933
+ */
1934
+ refresh() {
1935
+ this.syncShape(); // structural changes (added/removed/renamed controls)
1936
+ const selected = this.selected;
1937
+ this.sections = []; // tear the embedded detail forms down…
1938
+ this.cdr.detectChanges();
1939
+ if (selected)
1940
+ this.select(selected, false); // …and rebuild them, fresh-bound
1941
+ this.cdr.detectChanges();
1794
1942
  }
1795
1943
  select(node, reveal = true) {
1796
1944
  this.selected = node;
@@ -1924,9 +2072,8 @@ class ConfigEditorComponent {
1924
2072
  focusLeafKey = null;
1925
2073
  /** Add an absent optional child from the menu: build its control and select the node it lands on. */
1926
2074
  addOptional(node, entry) {
1927
- if (!node.group || node.group.get(entry.key))
2075
+ if (!node.group || !setNodePresence(node.group, entry.schema, entry.key, true))
1928
2076
  return;
1929
- node.group.addControl(entry.key, buildControl(entry.schema));
1930
2077
  // A leaf renders in the parent's detail pane; complex kinds become tree nodes.
1931
2078
  this.selectByPath(entry.schema.kind === 'leaf' ? node.id : this.join(node.id, entry.key));
1932
2079
  // Adding the last optional removes the menu row that held focus.
@@ -1948,10 +2095,10 @@ class ConfigEditorComponent {
1948
2095
  }
1949
2096
  /** Remove a present optional child node, returning its entry to the parent's menu. */
1950
2097
  removeOptional(parent, node) {
1951
- const key = node.presenceRemovable?.key;
1952
- if (!key || !parent.group)
2098
+ const removable = node.presenceRemovable;
2099
+ if (!removable || !parent.group)
1953
2100
  return;
1954
- parent.group.removeControl(key);
2101
+ setNodePresence(parent.group, removable.schema, removable.key, false);
1955
2102
  this.selectByPath(parent.id);
1956
2103
  this.focusSelectedRow();
1957
2104
  }
@@ -2288,10 +2435,10 @@ class ConfigEditorComponent {
2288
2435
  if (node.choice) {
2289
2436
  const active = this.activeCase(node);
2290
2437
  const body = active && node.choice.schema.cases[active] ? this.caseAsGroup(node.choice.schema, active) : null;
2291
- return { schema: body ? this.leafOnly(body) : null, group: node.choice.group };
2438
+ return { schema: body ? this.leafOnly(body, node.choice.group) : null, group: node.choice.group };
2292
2439
  }
2293
2440
  if (node.schema && node.group) {
2294
- return { schema: this.leafOnly(node.schema), group: node.group };
2441
+ return { schema: this.leafOnly(node.schema, node.group), group: node.group };
2295
2442
  }
2296
2443
  return { schema: null, group: null };
2297
2444
  }
@@ -2302,12 +2449,17 @@ class ConfigEditorComponent {
2302
2449
  * (grid / field-width layout) carries into the slice. Null when there are
2303
2450
  * no own fields.
2304
2451
  */
2305
- leafOnly(schema) {
2452
+ leafOnly(schema, group) {
2306
2453
  const children = {};
2307
2454
  for (const key of Object.keys(schema.children)) {
2308
2455
  const child = schema.children[key];
2309
- if (child.kind === 'leaf' || child.kind === 'leafList')
2310
- children[key] = child;
2456
+ if (child.kind !== 'leaf' && child.kind !== 'leafList')
2457
+ continue;
2458
+ // An absent presence leafList has no inline affordance and no control to
2459
+ // render — it is offered by the "+ Optional field" menu instead.
2460
+ if (child.kind === 'leafList' && child.presence && !group?.get(key))
2461
+ continue;
2462
+ children[key] = child;
2311
2463
  }
2312
2464
  if (!Object.keys(children).length)
2313
2465
  return null;
@@ -2321,13 +2473,16 @@ class ConfigEditorComponent {
2321
2473
  for (const key of Object.keys(schema.children)) {
2322
2474
  const child = schema.children[key];
2323
2475
  if (child.kind === 'leaf' || child.kind === 'leafList') {
2324
- // Leaves render in the detail pane; an absent presence leaf is offered by the menu.
2325
- if (child.kind === 'leaf' && child.presence && !group.get(key)) {
2476
+ // Leaves render in the detail pane; an absent presence leaf is also
2477
+ // offered by the "+ Optional field" menu (addable inline or from the
2478
+ // tree row). An absent presence *leafList* has no inline affordance, so
2479
+ // it is offered only by the menu (and excluded from the leafOnly slice).
2480
+ if (child.presence && !group.get(key)) {
2326
2481
  optionals.push({ key, schema: child, label: this.labelOf(child, key) });
2327
2482
  }
2328
2483
  continue;
2329
2484
  }
2330
- const presence = child.kind !== 'nodeGroupList' && child.presence;
2485
+ const presence = !!child.presence;
2331
2486
  if (presence && !group.get(key)) {
2332
2487
  optionals.push({ key, schema: child, label: this.labelOf(child, key) });
2333
2488
  continue;
@@ -2336,7 +2491,7 @@ class ConfigEditorComponent {
2336
2491
  if (!node)
2337
2492
  continue;
2338
2493
  if (presence)
2339
- node.presenceRemovable = { key };
2494
+ node.presenceRemovable = { key, schema: child };
2340
2495
  children.push(node);
2341
2496
  }
2342
2497
  const node = { id: path, label, children, group, schema, inherited };
@@ -2446,7 +2601,7 @@ class ConfigEditorComponent {
2446
2601
  return segment.replace(/%/g, '%25').replace(/\//g, '%2F');
2447
2602
  }
2448
2603
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2449
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: ConfigEditorComponent, isStandalone: true, selector: "nff-config-editor", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: true, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, rootTitle: { classPropertyName: "rootTitle", publicName: "rootTitle", isSignal: true, isRequired: false, transformFunction: null }, expandOnClick: { classPropertyName: "expandOnClick", publicName: "expandOnClick", isSignal: true, isRequired: false, transformFunction: null }, showBreadcrumb: { classPropertyName: "showBreadcrumb", publicName: "showBreadcrumb", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { editable: "editableChange" }, ngImport: i0, template: "<div class=\"editor\">\n <nav class=\"tree\" role=\"tree\" aria-label=\"Configuration structure\">\n <ng-template #treeNode let-node let-depth=\"depth\" let-parent=\"parent\">\n <div\n class=\"tree-row\"\n role=\"treeitem\"\n tabindex=\"0\"\n [class.selected]=\"node === selected\"\n [class.error]=\"hasError(node)\"\n [attr.aria-selected]=\"node === selected\"\n [attr.aria-expanded]=\"hasExpandableContent(node) ? expanded.has(node.id) : null\"\n [attr.aria-level]=\"depth + 1\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n (keydown)=\"onRowKeydown($event, node)\"\n >\n @if (hasExpandableContent(node)) {\n <button\n matIconButton\n class=\"twisty\"\n tabindex=\"-1\"\n (click)=\"toggle(node); $event.stopPropagation()\"\n [attr.aria-label]=\"expanded.has(node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ expanded.has(node.id) ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"twisty-spacer\"></span>\n }\n\n <span class=\"tree-label\" [matTooltip]=\"node.label\">{{ node.label }}</span>\n @if (node.choice && activeCaseLabel(node)) {\n <span class=\"tree-sublabel\" [matTooltip]=\"activeCaseLabel(node)\">{{ activeCaseLabel(node) }}</span>\n }\n @if (node === root) {\n <button\n matIconButton\n class=\"row-btn root-toggle\"\n [matTooltip]=\"editable() ? 'Stop editing' : 'Edit'\"\n [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\"\n [attr.aria-pressed]=\"editable()\"\n (click)=\"editable.set(!editable()); $event.stopPropagation()\"\n >\n <mat-icon>{{ editable() ? 'edit_off' : 'edit' }}</mat-icon>\n </button>\n <button\n matIconButton\n class=\"row-btn root-toggle\"\n [matTooltip]=\"allExpanded() ? 'Collapse all' : 'Expand all'\"\n [attr.aria-label]=\"allExpanded() ? 'Collapse all' : 'Expand all'\"\n (click)=\"toggleExpandAll(); $event.stopPropagation()\"\n >\n <mat-icon>{{ allExpanded() ? 'unfold_less' : 'unfold_more' }}</mat-icon>\n </button>\n }\n @if (hasError(node)) {\n <!-- A non-color error signal beside the red row text. -->\n <mat-icon class=\"row-error-icon\" aria-hidden=\"false\" role=\"img\" aria-label=\"Has validation errors\"\n >error_outline</mat-icon\n >\n }\n\n @if (editable() && node.list && !listAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.list.itemLabel + ' #' + (node.list.array.length + 1)\"\n [attr.aria-label]=\"'Add ' + node.list.itemLabel + ' #' + (node.list.array.length + 1)\"\n (click)=\"addItem(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.map?.complex && !mapAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.label + ' entry'\"\n [attr.aria-label]=\"'Add ' + node.label + ' entry'\"\n (click)=\"addTreeMapEntry(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.removable && !listAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n [matTooltip]=\"'Remove ' + node.label\"\n [attr.aria-label]=\"'Remove ' + node.label\"\n (click)=\"removeItem(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.presenceRemovable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n [matTooltip]=\"'Remove ' + node.label\"\n [attr.aria-label]=\"'Remove ' + node.label\"\n (click)=\"removeOptional(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.mapEntry && !mapAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n [matTooltip]=\"'Remove ' + node.label + ' entry'\"\n [attr.aria-label]=\"'Remove ' + node.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n\n @if (expanded.has(node.id)) {\n @for (child of node.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"treeNode; context: { $implicit: child, depth: depth + 1, parent: node }\"\n />\n }\n @if (editable() && node.optionals?.length) {\n <button\n type=\"button\"\n class=\"tree-row optional-row\"\n [style.padding-left.px]=\"8 + (depth + 1) * 16\"\n [matMenuTriggerFor]=\"treeOptionalsMenu\"\n (click)=\"$event.stopPropagation()\"\n >\n <span class=\"twisty-spacer\"></span>\n <mat-icon class=\"optional-icon\">add</mat-icon>\n <span class=\"optional-label\">Optional field</span>\n </button>\n <mat-menu #treeOptionalsMenu=\"matMenu\">\n @for (opt of node.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(node, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n }\n }\n </ng-template>\n\n <ng-container *ngTemplateOutlet=\"treeNode; context: { $implicit: root, depth: 0 }\" />\n </nav>\n\n <section class=\"detail\">\n @if (selected) {\n @if (showBreadcrumb()) {\n <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n @for (crumb of breadcrumb; track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n <!-- A selected member (list item / map entry) is removable from its own\n view; the selection then moves to its container, like a tree-row\n remove \u2014 a deleted node cannot stay selected. -->\n @if (editable() && selected.removable && selectedMemberParent(); as parent) {\n @if (!listAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n [matTooltip]=\"'Remove ' + selected.label\"\n [attr.aria-label]=\"'Remove ' + selected.label\"\n (click)=\"removeItem(parent, selected)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n @if (editable() && selected.mapEntry && selectedMemberParent(); as parent) {\n @if (!mapAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n [matTooltip]=\"'Remove ' + selected.label + ' entry'\"\n [attr.aria-label]=\"'Remove ' + selected.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, selected)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n </nav>\n }\n\n <!-- The selected subtree as a flat section list: each descendant's fields\n under a breadcrumb heading \u2014 the headings, not nesting, mark the\n boundary between one child and the next. -->\n @for (s of sections; track s.node.id + (s.footer ? ':footer' : '')) {\n @if (s.trail.length > 1 && !s.footer) {\n <!-- A heading, not a nav landmark: dozens of identical \"Section\"\n landmarks would flood the assistive-tech landmark list. Its\n accessible name is the trail itself. -->\n <div class=\"section-heading\" [class.continuation]=\"s.continuation\" role=\"heading\" aria-level=\"3\">\n @for (crumb of s.trail; track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n @if (editable() && s.node.removable && memberParent(s); as parent) {\n @if (!listAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n [matTooltip]=\"'Remove ' + s.node.label\"\n [attr.aria-label]=\"'Remove ' + s.node.label\"\n (click)=\"removeItem(parent, s.node, true)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n @if (editable() && s.node.mapEntry && memberParent(s); as parent) {\n @if (!mapAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n [matTooltip]=\"'Remove ' + s.node.label + ' entry'\"\n [attr.aria-label]=\"'Remove ' + s.node.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, s.node, true)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n </div>\n }\n\n @if (s.node.mapEntry; as entry) {\n <mat-form-field class=\"key-field\" [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>{{ entry.mapSchema.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #entryKey\n [readonly]=\"!editable()\"\n [value]=\"entry.key\"\n (change)=\"renameTreeMapEntry(s.node, entryKey.value); entryKey.value = entry.key\"\n />\n </mat-form-field>\n }\n\n @if (s.node.choice; as choice) {\n @if (editable()) {\n <mat-form-field class=\"case-select\" appearance=\"fill\">\n <mat-label>Selected option</mat-label>\n <mat-select [value]=\"activeCase(s.node)\" (selectionChange)=\"switchTreeCase(s.node, $event.value)\">\n @for (caseName of objectKeys(choice.schema.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(choice.schema, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"case-select\" appearance=\"outline\">\n <mat-label>Selected option</mat-label>\n <input matInput readonly [value]=\"activeCaseLabel(s.node) ?? ''\" />\n </mat-form-field>\n }\n }\n\n @if (s.schema && s.group) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"s.node.inherited ?? null\"\n [schema]=\"s.schema\"\n [formGroup]=\"s.group\"\n [editable]=\"editable()\"\n [focusLeaf]=\"s.node.id === focusSectionId ? focusLeafKey : null\"\n />\n }\n\n @if (s.node.map && !s.node.map.complex) {\n <nff-node-map-renderer\n [nodeMap]=\"s.node.map.schema\"\n [formGroup]=\"s.node.map.group\"\n [editable]=\"editable()\"\n />\n }\n\n @if (s.footer && editable() && s.node.map?.complex && !mapAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton [matTooltip]=\"'Add ' + s.node.label + ' entry'\" (click)=\"addTreeMapEntry(s.node, true)\"><mat-icon>add</mat-icon> Add entry</button>\n </div>\n }\n @if (s.footer && editable() && s.node.list; as list) {\n @if (!listAtMax(s.node)) {\n <div class=\"section-actions\">\n <!-- The next entry's number in the label, so the button reads as\n what it does: \"Add Cell #3\" appends the third entry. -->\n <button matButton (click)=\"addItem(s.node, true)\">\n <mat-icon>add</mat-icon> Add {{ list.itemLabel }} #{{ list.array.length + 1 }}\n </button>\n </div>\n }\n }\n @if (!s.footer && emptySectionHint(s); as hint) {\n <p class=\"empty\">{{ hint }}</p>\n }\n @if (!s.footer && presentRangeHint(s); as hint) {\n <p class=\"section-error\" role=\"alert\">{{ hint }}</p>\n }\n }\n\n @if (\n sections.length === 1 &&\n !sections[0].schema &&\n !sections[0].node.choice &&\n !sections[0].node.map &&\n !sections[0].node.list &&\n !sections[0].node.mapEntry\n ) {\n <p class=\"empty\">This node has no fields.</p>\n }\n } @else {\n <p class=\"empty\">Select a node on the left to edit its fields.</p>\n }\n </section>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%}.editor{display:flex;align-items:stretch;min-height:320px}.tree{flex:0 0 360px;min-width:360px;position:sticky;top:var(--nff-tree-sticky-top, 0px);align-self:flex-start;max-height:calc(100vh - var(--nff-tree-sticky-top, 0px));overflow:auto;padding:4px 16px 4px 0}.tree-row{display:flex;align-items:center;gap:4px;height:32px;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.tree-row:hover{background:var(--mat-sys-surface-container-high)}.tree-row.selected{background:var(--mat-sys-secondary-container);font-weight:600}.tree-row.error .tree-label{color:var(--mat-sys-error)}.row-error-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-error)}.tree-row:focus-visible{outline:2px solid var(--mat-sys-primary);outline-offset:-2px}.twisty{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.twisty mat-icon{font-size:18px;width:18px;height:18px}.twisty-spacer{flex:0 0 auto;width:28px}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-sublabel{flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;font-size:.85em;color:var(--mat-sys-on-surface-variant)}.tree-sublabel:before{content:\"\\b7 \"}.optional-row{width:100%;border:0;background:none;font:inherit;text-align:left}.optional-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-primary)}.optional-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;color:var(--mat-sys-on-surface-variant);font-style:italic}.row-btn{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.row-btn mat-icon{font-size:18px;width:18px;height:18px}.root-toggle{color:var(--mat-sys-on-surface-variant)}.row-btn.add{visibility:visible}.row-btn.remove{visibility:hidden}.tree-row:hover .row-btn.remove,.tree-row.selected .row-btn.remove,.tree-row:focus-within .row-btn.remove{visibility:visible}.detail{flex:1 1 auto;min-width:0;overflow:auto;border-left:1px solid var(--mat-sys-outline-variant);padding-left:16px}.breadcrumb{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:4px 0 12px;font-size:1.15rem}.crumb-current{font-weight:600}.crumb-link{font-size:inherit;padding:0}.crumb-sep{color:var(--mat-sys-on-surface-variant)}.section-heading{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:20px 0 8px;font-size:.95rem}.section-heading.continuation{margin-top:12px}.section-heading .crumb-current{font-weight:600}.section-remove{flex:0 0 auto;align-self:center;opacity:.45;transition:opacity .12s ease}.section-remove:hover,.section-remove:focus-visible{opacity:1}.section-actions button{opacity:.6;transition:opacity .12s ease}.section-actions button:hover,.section-actions button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.key-field,.case-select{display:block;max-width:320px;margin-bottom:8px}.section-actions{display:flex;justify-content:flex-start;margin-bottom:8px}.empty{color:var(--mat-sys-on-surface-variant);font-style:italic}.section-error{color:var(--mat-sys-error);font-size:.85rem}.item-link{min-width:0;padding:4px 0;border:0;background:none;text-align:left;cursor:pointer;color:var(--mat-sys-primary);font:inherit;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.item-link:hover{text-decoration:underline}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i3$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i3$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i3$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: DynamicRecursiveFormComponent, selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "component", type: NodeMapRendererComponent, selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable", "showAbsentOptionals", "inheritedAppearance"] }] });
2604
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: ConfigEditorComponent, isStandalone: true, selector: "nff-config-editor", inputs: { schema: { classPropertyName: "schema", publicName: "schema", isSignal: true, isRequired: true, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: true, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, rootTitle: { classPropertyName: "rootTitle", publicName: "rootTitle", isSignal: true, isRequired: false, transformFunction: null }, expandOnClick: { classPropertyName: "expandOnClick", publicName: "expandOnClick", isSignal: true, isRequired: false, transformFunction: null }, showBreadcrumb: { classPropertyName: "showBreadcrumb", publicName: "showBreadcrumb", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { editable: "editableChange" }, ngImport: i0, template: "<div class=\"editor\">\n <nav class=\"tree\" role=\"tree\" aria-label=\"Configuration structure\">\n <ng-template #treeNode let-node let-depth=\"depth\" let-parent=\"parent\">\n <div\n class=\"tree-row\"\n role=\"treeitem\"\n tabindex=\"0\"\n [class.selected]=\"node === selected\"\n [class.error]=\"hasError(node)\"\n [attr.aria-selected]=\"node === selected\"\n [attr.aria-expanded]=\"hasExpandableContent(node) ? expanded.has(node.id) : null\"\n [attr.aria-level]=\"depth + 1\"\n [style.padding-left.px]=\"8 + depth * 16\"\n (click)=\"select(node)\"\n (keydown)=\"onRowKeydown($event, node)\"\n >\n @if (hasExpandableContent(node)) {\n <button\n matIconButton\n class=\"twisty\"\n tabindex=\"-1\"\n (click)=\"toggle(node); $event.stopPropagation()\"\n [attr.aria-label]=\"expanded.has(node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon>{{ expanded.has(node.id) ? 'expand_more' : 'chevron_right' }}</mat-icon>\n </button>\n } @else {\n <span class=\"twisty-spacer\"></span>\n }\n\n <span class=\"tree-label\" [matTooltip]=\"node.label\">{{ node.label }}</span>\n @if (node.choice && activeCaseLabel(node)) {\n <span class=\"tree-sublabel\" [matTooltip]=\"activeCaseLabel(node)\">{{ activeCaseLabel(node) }}</span>\n }\n @if (node === root) {\n <button\n matIconButton\n class=\"row-btn root-toggle\"\n [matTooltip]=\"editable() ? 'Stop editing' : 'Edit'\"\n [attr.aria-label]=\"editable() ? 'Stop editing' : 'Edit'\"\n [attr.aria-pressed]=\"editable()\"\n (click)=\"editable.set(!editable()); $event.stopPropagation()\"\n >\n <mat-icon>{{ editable() ? 'edit_off' : 'edit' }}</mat-icon>\n </button>\n <button\n matIconButton\n class=\"row-btn root-toggle\"\n [matTooltip]=\"allExpanded() ? 'Collapse all' : 'Expand all'\"\n [attr.aria-label]=\"allExpanded() ? 'Collapse all' : 'Expand all'\"\n (click)=\"toggleExpandAll(); $event.stopPropagation()\"\n >\n <mat-icon>{{ allExpanded() ? 'unfold_less' : 'unfold_more' }}</mat-icon>\n </button>\n }\n @if (hasError(node)) {\n <!-- A non-color error signal beside the red row text. -->\n <mat-icon class=\"row-error-icon\" aria-hidden=\"false\" role=\"img\" aria-label=\"Has validation errors\"\n >error_outline</mat-icon\n >\n }\n\n @if (editable() && node.list && !listAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.list.itemLabel + ' #' + (node.list.array.length + 1)\"\n [attr.aria-label]=\"'Add ' + node.list.itemLabel + ' #' + (node.list.array.length + 1)\"\n (click)=\"addItem(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.map?.complex && !mapAtMax(node)) {\n <button\n matIconButton\n class=\"row-btn add\"\n [matTooltip]=\"'Add ' + node.label + ' entry'\"\n [attr.aria-label]=\"'Add ' + node.label + ' entry'\"\n (click)=\"addTreeMapEntry(node); $event.stopPropagation()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n }\n @if (editable() && node.removable && !listAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n [matTooltip]=\"'Remove ' + node.label\"\n [attr.aria-label]=\"'Remove ' + node.label\"\n (click)=\"removeItem(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.presenceRemovable) {\n <button\n matIconButton\n class=\"row-btn remove\"\n [matTooltip]=\"'Remove ' + node.label\"\n [attr.aria-label]=\"'Remove ' + node.label\"\n (click)=\"removeOptional(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n @if (editable() && node.mapEntry && !mapAtMin(parent)) {\n <button\n matIconButton\n class=\"row-btn remove\"\n [matTooltip]=\"'Remove ' + node.label + ' entry'\"\n [attr.aria-label]=\"'Remove ' + node.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, node); $event.stopPropagation()\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n </div>\n\n @if (expanded.has(node.id)) {\n @for (child of node.children; track child.id) {\n <ng-container\n *ngTemplateOutlet=\"treeNode; context: { $implicit: child, depth: depth + 1, parent: node }\"\n />\n }\n @if (editable() && node.optionals?.length) {\n <button\n type=\"button\"\n class=\"tree-row optional-row\"\n [style.padding-left.px]=\"8 + (depth + 1) * 16\"\n [matMenuTriggerFor]=\"treeOptionalsMenu\"\n (click)=\"$event.stopPropagation()\"\n >\n <span class=\"twisty-spacer\"></span>\n <mat-icon class=\"optional-icon\">add</mat-icon>\n <span class=\"optional-label\">Optional field</span>\n </button>\n <mat-menu #treeOptionalsMenu=\"matMenu\">\n @for (opt of node.optionals; track opt.key) {\n <button mat-menu-item (click)=\"addOptional(node, opt)\">{{ opt.label }}</button>\n }\n </mat-menu>\n }\n }\n </ng-template>\n\n <ng-container *ngTemplateOutlet=\"treeNode; context: { $implicit: root, depth: 0 }\" />\n </nav>\n\n <section class=\"detail\">\n @if (selected) {\n @if (showBreadcrumb()) {\n <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n @for (crumb of breadcrumb; track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n <!-- A selected member (list item / map entry) is removable from its own\n view; the selection then moves to its container, like a tree-row\n remove \u2014 a deleted node cannot stay selected. -->\n @if (editable() && selected.removable && selectedMemberParent(); as parent) {\n @if (!listAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n [matTooltip]=\"'Remove ' + selected.label\"\n [attr.aria-label]=\"'Remove ' + selected.label\"\n (click)=\"removeItem(parent, selected)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n @if (editable() && selected.mapEntry && selectedMemberParent(); as parent) {\n @if (!mapAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n [matTooltip]=\"'Remove ' + selected.label + ' entry'\"\n [attr.aria-label]=\"'Remove ' + selected.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, selected)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n </nav>\n }\n\n <!-- The selected subtree as a flat section list: each descendant's fields\n under a breadcrumb heading \u2014 the headings, not nesting, mark the\n boundary between one child and the next. -->\n @for (s of sections; track s.node.id + (s.footer ? ':footer' : '')) {\n @if (s.trail.length > 1 && !s.footer) {\n <!-- A heading, not a nav landmark: dozens of identical \"Section\"\n landmarks would flood the assistive-tech landmark list. Its\n accessible name is the trail itself. -->\n <div class=\"section-heading\" [class.continuation]=\"s.continuation\" role=\"heading\" aria-level=\"3\">\n @for (crumb of s.trail; track crumb.id; let last = $last) {\n @if (last) {\n <span class=\"crumb-current\">{{ crumb.label }}</span>\n } @else {\n <button type=\"button\" class=\"item-link crumb-link\" (click)=\"select(crumb)\">{{ crumb.label }}</button>\n <span class=\"crumb-sep\" aria-hidden=\"true\">/</span>\n }\n }\n @if (editable() && s.node.removable && memberParent(s); as parent) {\n @if (!listAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n [matTooltip]=\"'Remove ' + s.node.label\"\n [attr.aria-label]=\"'Remove ' + s.node.label\"\n (click)=\"removeItem(parent, s.node, true)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n @if (editable() && s.node.mapEntry && memberParent(s); as parent) {\n @if (!mapAtMin(parent)) {\n <button\n matIconButton\n class=\"remove-button small-icon-button section-remove\"\n [matTooltip]=\"'Remove ' + s.node.label + ' entry'\"\n [attr.aria-label]=\"'Remove ' + s.node.label + ' entry'\"\n (click)=\"removeTreeMapEntry(parent, s.node, true)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n }\n }\n </div>\n }\n\n @if (s.node.mapEntry; as entry) {\n <mat-form-field class=\"key-field\" [appearance]=\"editable() ? 'fill' : 'outline'\">\n <mat-label>{{ entry.mapSchema.keyLabel ?? 'Key' }}</mat-label>\n <input\n matInput\n #entryKey\n [readonly]=\"!editable()\"\n [value]=\"entry.key\"\n (change)=\"renameTreeMapEntry(s.node, entryKey.value); entryKey.value = entry.key\"\n />\n </mat-form-field>\n }\n\n @if (s.node.choice; as choice) {\n @if (editable()) {\n <mat-form-field class=\"case-select\" appearance=\"fill\">\n <mat-label>Selected option</mat-label>\n <mat-select [value]=\"activeCase(s.node)\" (selectionChange)=\"switchTreeCase(s.node, $event.value)\">\n @for (caseName of objectKeys(choice.schema.cases); track caseName) {\n <mat-option [value]=\"caseName\">{{ caseLabel(choice.schema, caseName) }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field class=\"case-select\" appearance=\"outline\">\n <mat-label>Selected option</mat-label>\n <input matInput readonly [value]=\"activeCaseLabel(s.node) ?? ''\" />\n </mat-form-field>\n }\n }\n\n @if (s.schema && s.group) {\n <nff-dynamic-recursive-form\n [inheritedAppearance]=\"s.node.inherited ?? null\"\n [schema]=\"s.schema\"\n [formGroup]=\"s.group\"\n [editable]=\"editable()\"\n [focusLeaf]=\"s.node.id === focusSectionId ? focusLeafKey : null\"\n />\n }\n\n @if (s.node.map && !s.node.map.complex) {\n <nff-node-map-renderer\n [nodeMap]=\"s.node.map.schema\"\n [formGroup]=\"s.node.map.group\"\n [editable]=\"editable()\"\n />\n }\n\n @if (s.footer && editable() && s.node.map?.complex && !mapAtMax(s.node)) {\n <div class=\"section-actions\">\n <button matButton [matTooltip]=\"'Add ' + s.node.label + ' entry'\" (click)=\"addTreeMapEntry(s.node, true)\"><mat-icon>add</mat-icon> Add entry</button>\n </div>\n }\n @if (s.footer && editable() && s.node.list; as list) {\n @if (!listAtMax(s.node)) {\n <div class=\"section-actions\">\n <!-- The next entry's number in the label, so the button reads as\n what it does: \"Add Cell #3\" appends the third entry. -->\n <button matButton (click)=\"addItem(s.node, true)\">\n <mat-icon>add</mat-icon> Add {{ list.itemLabel }} #{{ list.array.length + 1 }}\n </button>\n </div>\n }\n }\n @if (!s.footer && emptySectionHint(s); as hint) {\n <p class=\"empty\">{{ hint }}</p>\n }\n @if (!s.footer && presentRangeHint(s); as hint) {\n <p class=\"section-error\" role=\"alert\">{{ hint }}</p>\n }\n }\n\n @if (\n sections.length === 1 &&\n !sections[0].schema &&\n !sections[0].node.choice &&\n !sections[0].node.map &&\n !sections[0].node.list &&\n !sections[0].node.mapEntry\n ) {\n <p class=\"empty\">This node has no fields.</p>\n }\n } @else {\n <p class=\"empty\">Select a node on the left to edit its fields.</p>\n }\n </section>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%}.editor{display:flex;align-items:stretch;min-height:320px}.tree{flex:0 0 360px;min-width:360px;position:sticky;top:var(--nff-tree-sticky-top, 0px);align-self:flex-start;max-height:calc(100vh - var(--nff-tree-sticky-top, 0px));overflow:auto;padding:4px 16px 4px 0}.tree-row{display:flex;align-items:center;gap:4px;height:32px;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.tree-row:hover{background:var(--mat-sys-surface-container-high)}.tree-row.selected{background:var(--mat-sys-secondary-container);font-weight:600}.tree-row.error .tree-label{color:var(--mat-sys-error)}.row-error-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-error)}.tree-row:focus-visible{outline:2px solid var(--mat-sys-primary);outline-offset:-2px}.twisty{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.twisty mat-icon{font-size:18px;width:18px;height:18px}.twisty-spacer{flex:0 0 auto;width:28px}.tree-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis}.tree-sublabel{flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;font-size:.85em;color:var(--mat-sys-on-surface-variant)}.tree-sublabel:before{content:\"\\b7 \"}.optional-row{width:100%;border:0;background:none;font:inherit;text-align:left}.optional-icon{flex:0 0 auto;font-size:18px;width:18px;height:18px;color:var(--mat-sys-primary)}.optional-label{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;color:var(--mat-sys-on-surface-variant);font-style:italic}.row-btn{flex:0 0 auto;--mat-icon-button-state-layer-size: 28px;--mat-icon-button-icon-size: 18px}.row-btn mat-icon{font-size:18px;width:18px;height:18px}.root-toggle{color:var(--mat-sys-on-surface-variant)}.row-btn.add{visibility:visible}.row-btn.remove{visibility:hidden}.tree-row:hover .row-btn.remove,.tree-row.selected .row-btn.remove,.tree-row:focus-within .row-btn.remove{visibility:visible}.detail{flex:1 1 auto;min-width:0;overflow:auto;border-left:1px solid var(--mat-sys-outline-variant);padding-left:16px}.breadcrumb{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:4px 0 12px;font-size:1.15rem}.crumb-current{font-weight:600}.crumb-link{font-size:inherit;padding:0}.crumb-sep{color:var(--mat-sys-on-surface-variant)}.section-heading{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px;margin:20px 0 8px;font-size:.95rem}.section-heading.continuation{margin-top:12px}.section-heading .crumb-current{font-weight:600}.section-remove{flex:0 0 auto;align-self:center;opacity:.45;transition:opacity .12s ease}.section-remove:hover,.section-remove:focus-visible{opacity:1}.section-actions button{opacity:.6;transition:opacity .12s ease}.section-actions button:hover,.section-actions button:focus-visible{opacity:1}.remove-button{background-color:var(--mat-sys-error-container);box-shadow:0 2px 1px -1px #0003,0 1px 1px #00000024,0 1px 3px #0000001f}.small-icon-button{--mdc-icon-button-state-layer-size: 24px;--mdc-icon-button-icon-size: 16px;--mat-icon-button-touch-target-display: none;width:24px;height:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}.small-icon-button>[role=img]{width:16px;height:16px;font-size:16px}.small-icon-button>[role=img] svg{width:16px;height:16px}.key-field,.case-select{display:block;max-width:320px;margin-bottom:8px}.section-actions{display:flex;justify-content:flex-start;margin-bottom:8px}.empty{color:var(--mat-sys-on-surface-variant);font-style:italic}.section-error{color:var(--mat-sys-error);font-size:.85rem}.item-link{min-width:0;padding:4px 0;border:0;background:none;text-align:left;cursor:pointer;color:var(--mat-sys-primary);font:inherit;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.item-link:hover{text-decoration:underline}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i3$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i3$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i3$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: DynamicRecursiveFormComponent, selector: "nff-dynamic-recursive-form", inputs: ["schema", "initialValue", "formGroup", "index", "removable", "title", "editable", "showAbsentOptionals", "addButtonCallback", "addButtonLabel", "focusLeaf", "inheritedAppearance"], outputs: ["remove", "editableChange"] }, { kind: "component", type: NodeMapRendererComponent, selector: "nff-node-map-renderer", inputs: ["nodeMap", "formGroup", "editable", "showAbsentOptionals", "inheritedAppearance"] }] });
2450
2605
  }
2451
2606
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ConfigEditorComponent, decorators: [{
2452
2607
  type: Component,
@@ -2472,5 +2627,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
2472
2627
  * Generated bundle index. Do not edit.
2473
2628
  */
2474
2629
 
2475
- export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, NodeMapRendererComponent, RadixInputDirective, addMapEntry, buildControl, buildFormFromSchema, caseDisplayLabels, caseFields, defineSchema, formatRadix, removeMapEntry, renameMapEntry, resolveChoiceCase, serializeForm, switchChoiceCase, toWireValue };
2630
+ export { CASE_KEY, ConfigEditorComponent, DynamicRecursiveFormComponent, LeafEnumRendererComponent, LeafListRendererComponent, LeafRendererComponent, NodeGroupListRendererComponent, NodeMapRendererComponent, RadixInputDirective, addMapEntry, buildControl, buildFormFromSchema, caseDisplayLabels, caseFields, defineSchema, formatRadix, removeMapEntry, renameMapEntry, resolveChoiceCase, serializeForm, setNodePresence, switchChoiceCase, toWireValue };
2476
2631
  //# sourceMappingURL=ng-form-foundry.mjs.map