@elle96/ng-json-schema-manager 0.0.14 → 0.1.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.
@@ -471,6 +471,39 @@ class JsonSchemaValidationService {
471
471
  }
472
472
  return validate.errors ?? [];
473
473
  }
474
+ /**
475
+ * Validates the schema itself against its JSON Schema meta-schema.
476
+ * Returns a list of SchemaError describing what is wrong in the schema definition.
477
+ */
478
+ validateSchema(schema) {
479
+ const draft = this.detectDraft(schema);
480
+ const ajv = this.getAjv(draft);
481
+ try {
482
+ const valid = ajv.validateSchema(schema);
483
+ if (valid)
484
+ return [];
485
+ return (ajv.errors ?? []).map((e) => ({
486
+ path: e.instancePath || '/',
487
+ message: e.message ?? e.keyword,
488
+ keyword: e.keyword,
489
+ }));
490
+ }
491
+ catch (e) {
492
+ // ajv.validateSchema throws when the schema is so broken it cannot be compiled.
493
+ // The error message contains comma-separated individual violations — split them.
494
+ const raw = e instanceof Error ? e.message : String(e);
495
+ // Strip the "schema is invalid: " prefix if present
496
+ const body = raw.replace(/^schema is invalid:\s*/i, '');
497
+ // Each violation looks like "data/some/path must ..." — extract path and message
498
+ return body.split(', ').map((msg) => {
499
+ const trimmed = msg.trim();
500
+ const match = trimmed.match(/^(data[^\s]*)\s+(.+)$/);
501
+ return match
502
+ ? { path: match[1].replace(/^data/, '') || '/', message: match[2], keyword: 'schema' }
503
+ : { path: '/', message: trimmed, keyword: 'schema' };
504
+ });
505
+ }
506
+ }
474
507
  detectDraft(schema) {
475
508
  const schemaId = schema.$schema ?? '';
476
509
  if (schemaId.includes('draft-04')) {
@@ -557,6 +590,7 @@ class JsonSchemaNodeComponent {
557
590
  errorsMap = new Map();
558
591
  parent;
559
592
  controlKey;
593
+ allowAdditionalProperties = false;
560
594
  controlReplaced = new EventEmitter();
561
595
  selectedOneOf = 0;
562
596
  selectedAnyOf = new Set();
@@ -571,133 +605,115 @@ class JsonSchemaNodeComponent {
571
605
  this.selectedAnyOf.add(0);
572
606
  }
573
607
  }
608
+ /**
609
+ * Determina se un figlio di un object è "semplice" (primitive/enum/bool),
610
+ * quindi può stare in griglia 2-col, oppure "complesso" (object/array/combinator)
611
+ * che deve occupare la larghezza piena.
612
+ */
613
+ isSimpleChild(key) {
614
+ const childSchema = this.schemaForKey(key);
615
+ if (childSchema.oneOf?.length || childSchema.anyOf?.length || childSchema.allOf?.length)
616
+ return false;
617
+ const t = this.schemaService.resolveType(childSchema);
618
+ return t !== 'object' && t !== 'array';
619
+ }
620
+ badgeClass(badge) {
621
+ return {
622
+ 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400': badge === 'Deprecated',
623
+ 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-400': badge === 'Read-only' || badge === 'Nullable',
624
+ 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400': badge === 'Write-only',
625
+ 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400': badge === 'Not constraint',
626
+ };
627
+ }
574
628
  get nodeKind() {
575
- if (this.schema.oneOf?.length) {
629
+ if (this.schema.oneOf?.length)
576
630
  return 'oneOf';
577
- }
578
- if (this.schema.anyOf?.length) {
631
+ if (this.schema.anyOf?.length)
579
632
  return 'anyOf';
580
- }
581
- if (this.schema.allOf?.length) {
633
+ if (this.schema.allOf?.length)
582
634
  return 'allOf';
583
- }
584
635
  const resolved = this.resolvedType;
585
- if (resolved === 'object') {
636
+ if (resolved === 'object')
586
637
  return 'object';
587
- }
588
- if (resolved === 'array') {
638
+ if (resolved === 'array')
589
639
  return 'array';
590
- }
591
640
  return 'primitive';
592
641
  }
593
642
  get resolvedType() {
594
643
  return this.schemaService.resolveType(this.effectiveSchema) ?? 'string';
595
644
  }
596
645
  get inputKind() {
597
- if (this.effectiveSchema.enum) {
646
+ if (this.effectiveSchema.enum)
598
647
  return 'select';
599
- }
600
- if (this.resolvedType === 'boolean') {
648
+ if (this.resolvedType === 'boolean')
601
649
  return 'checkbox';
602
- }
603
- if (this.effectiveSchema.format === 'textarea' || (this.effectiveSchema.maxLength ?? 0) > 200) {
650
+ if (this.effectiveSchema.format === 'textarea' || (this.effectiveSchema.maxLength ?? 0) > 200)
604
651
  return 'textarea';
605
- }
606
652
  return 'text';
607
653
  }
608
654
  get inputType() {
609
- if (this.effectiveSchema.format === 'email') {
655
+ if (this.effectiveSchema.format === 'email')
610
656
  return 'email';
611
- }
612
- if (this.effectiveSchema.format === 'date') {
657
+ if (this.effectiveSchema.format === 'date')
613
658
  return 'date';
614
- }
615
- if (this.effectiveSchema.format === 'time') {
659
+ if (this.effectiveSchema.format === 'time')
616
660
  return 'time';
617
- }
618
- if (this.effectiveSchema.format === 'date-time') {
661
+ if (this.effectiveSchema.format === 'date-time')
619
662
  return 'datetime-local';
620
- }
621
- if (this.resolvedType === 'number' || this.resolvedType === 'integer') {
663
+ if (this.resolvedType === 'number' || this.resolvedType === 'integer')
622
664
  return 'number';
623
- }
624
- if (this.effectiveSchema.format === 'password') {
665
+ if (this.effectiveSchema.format === 'password')
625
666
  return 'password';
626
- }
627
667
  return 'text';
628
668
  }
629
- get controlAsGroup() {
630
- return this.control;
631
- }
632
- get controlAsFormControl() {
633
- return this.control;
634
- }
635
- get controlAsArray() {
636
- return this.control;
637
- }
638
- get arrayControls() {
639
- return this.controlAsArray.controls;
640
- }
641
- get objectKeys() {
642
- return Object.keys(this.controlAsGroup.controls);
643
- }
669
+ get controlAsGroup() { return this.control; }
670
+ get controlAsFormControl() { return this.control; }
671
+ get controlAsArray() { return this.control; }
672
+ get arrayControls() { return this.controlAsArray.controls; }
673
+ get objectKeys() { return Object.keys(this.controlAsGroup.controls); }
644
674
  get canAddProperty() {
675
+ if (!this.allowAdditionalProperties)
676
+ return false;
645
677
  return this.effectiveSchema.additionalProperties !== false || !!this.effectiveSchema.patternProperties;
646
678
  }
647
- get errorsForPath() {
648
- return this.errorsMap.get(this.path) ?? [];
649
- }
679
+ get errorsForPath() { return this.errorsMap.get(this.path) ?? []; }
650
680
  get metaBadges() {
651
681
  const badges = [];
652
- if (this.effectiveSchema.deprecated) {
682
+ if (this.effectiveSchema.deprecated)
653
683
  badges.push('Deprecated');
654
- }
655
- if (this.effectiveSchema.readOnly) {
684
+ if (this.effectiveSchema.readOnly)
656
685
  badges.push('Read-only');
657
- }
658
- if (this.effectiveSchema.writeOnly) {
686
+ if (this.effectiveSchema.writeOnly)
659
687
  badges.push('Write-only');
660
- }
661
- if (this.effectiveSchema.nullable) {
688
+ if (this.effectiveSchema.nullable)
662
689
  badges.push('Nullable');
663
- }
664
- if (this.effectiveSchema.not) {
690
+ if (this.effectiveSchema.not)
665
691
  badges.push('Not constraint');
666
- }
667
692
  return badges;
668
693
  }
669
694
  get activeVariant() {
670
- if (this.schema.oneOf?.length) {
695
+ if (this.schema.oneOf?.length)
671
696
  return this.schema.oneOf[this.selectedOneOf] ?? null;
672
- }
673
697
  if (this.schema.anyOf?.length) {
674
- const selections = Array.from(this.selectedAnyOf).map((index) => this.schema.anyOf[index]);
698
+ const selections = Array.from(this.selectedAnyOf).map((i) => this.schema.anyOf[i]);
675
699
  return selections.length ? this.mergeSchemas(selections) : this.schema.anyOf[0] ?? null;
676
700
  }
677
- if (this.schema.allOf?.length) {
701
+ if (this.schema.allOf?.length)
678
702
  return this.mergeSchemas([this.schema, ...this.schema.allOf]);
679
- }
680
703
  return this.effectiveSchema;
681
704
  }
682
705
  get mergedAllOf() {
683
706
  return this.schema.allOf?.length ? [this.schema, ...this.schema.allOf] : [this.schema];
684
707
  }
685
- isRequired(key) {
686
- return this.effectiveSchema.required?.includes(key) ?? false;
687
- }
708
+ isRequired(key) { return this.effectiveSchema.required?.includes(key) ?? false; }
688
709
  schemaForKey(key) {
689
710
  return (this.effectiveSchema.properties?.[key] ??
690
711
  this.schemaService.resolveDynamicSchema(this.effectiveSchema, key) ??
691
712
  {});
692
713
  }
693
- isDynamicKey(key) {
694
- return !this.effectiveSchema.properties?.[key];
695
- }
714
+ isDynamicKey(key) { return !this.effectiveSchema.properties?.[key]; }
696
715
  pathForChild(key) {
697
- if (this.path === '') {
698
- return `/${key}`;
699
- }
700
- return `${this.path}/${key}`;
716
+ return this.path === '' ? `/${key}` : `${this.path}/${key}`;
701
717
  }
702
718
  addDynamicProperty() {
703
719
  this.propertyError = '';
@@ -726,135 +742,107 @@ class JsonSchemaNodeComponent {
726
742
  this.controlAsGroup.addControl(key, control);
727
743
  this.newPropertyKey = '';
728
744
  }
729
- removeDynamicKey(key) {
730
- this.controlAsGroup.removeControl(key);
731
- }
745
+ removeDynamicKey(key) { this.controlAsGroup.removeControl(key); }
732
746
  schemaForIndex(index) {
733
747
  const prefixItems = this.effectiveSchema.prefixItems ?? [];
734
- if (prefixItems[index]) {
748
+ if (prefixItems[index])
735
749
  return prefixItems[index];
736
- }
737
- if (this.effectiveSchema.items && this.effectiveSchema.items !== true) {
750
+ if (this.effectiveSchema.items && this.effectiveSchema.items !== true)
738
751
  return this.effectiveSchema.items;
739
- }
740
- if (this.effectiveSchema.additionalItems && this.effectiveSchema.additionalItems !== true) {
752
+ if (this.effectiveSchema.additionalItems && this.effectiveSchema.additionalItems !== true)
741
753
  return this.effectiveSchema.additionalItems;
742
- }
743
- if (this.effectiveSchema.unevaluatedItems && this.effectiveSchema.unevaluatedItems !== true) {
754
+ if (this.effectiveSchema.unevaluatedItems && this.effectiveSchema.unevaluatedItems !== true)
744
755
  return this.effectiveSchema.unevaluatedItems;
745
- }
746
756
  return {};
747
757
  }
748
758
  get canAddArrayItem() {
749
- if (this.effectiveSchema.items === false ||
759
+ return !(this.effectiveSchema.items === false ||
750
760
  this.effectiveSchema.additionalItems === false ||
751
- this.effectiveSchema.unevaluatedItems === false) {
752
- return false;
753
- }
754
- return true;
761
+ this.effectiveSchema.unevaluatedItems === false);
755
762
  }
756
763
  addArrayItem() {
757
- if (!this.canAddArrayItem) {
764
+ if (!this.canAddArrayItem)
758
765
  return;
759
- }
760
766
  const formArray = this.controlAsArray;
761
767
  const index = formArray.length;
762
- const schemaForItem = this.schemaForIndex(index);
763
- formArray.push(this.schemaService.buildControl(schemaForItem, undefined, false));
768
+ formArray.push(this.schemaService.buildControl(this.schemaForIndex(index), undefined, false));
764
769
  formArray.markAsDirty();
765
770
  }
766
771
  removeArrayItem(index) {
767
- const formArray = this.controlAsArray;
768
- formArray.removeAt(index);
769
- formArray.markAsDirty();
772
+ this.controlAsArray.removeAt(index);
773
+ this.controlAsArray.markAsDirty();
770
774
  }
771
775
  onOneOfChange() {
772
- if (!this.activeVariant) {
776
+ if (!this.activeVariant)
773
777
  return;
774
- }
775
778
  this.replaceControl(this.schemaService.buildControl(this.activeVariant, this.control.value, this.required));
776
779
  }
777
780
  toggleAnyOf(index) {
778
- if (this.selectedAnyOf.has(index)) {
781
+ if (this.selectedAnyOf.has(index))
779
782
  this.selectedAnyOf.delete(index);
780
- }
781
- else {
783
+ else
782
784
  this.selectedAnyOf.add(index);
783
- }
784
785
  const variant = this.activeVariant;
785
- if (variant) {
786
+ if (variant)
786
787
  this.replaceControl(this.schemaService.buildControl(variant, this.control.value, this.required));
787
- }
788
788
  }
789
789
  replaceControl(control) {
790
790
  if (this.parent) {
791
- if (this.parent instanceof FormGroup && typeof this.controlKey === 'string') {
791
+ if (this.parent instanceof FormGroup && typeof this.controlKey === 'string')
792
792
  this.parent.setControl(this.controlKey, control);
793
- }
794
- if (this.parent instanceof FormArray && typeof this.controlKey === 'number') {
793
+ if (this.parent instanceof FormArray && typeof this.controlKey === 'number')
795
794
  this.parent.setControl(this.controlKey, control);
796
- }
797
795
  this.control = control;
798
796
  return;
799
797
  }
800
798
  this.control = control;
801
799
  this.controlReplaced.emit(control);
802
800
  }
803
- childControl(key) {
804
- return this.controlAsGroup.get(key);
805
- }
801
+ childControl(key) { return this.controlAsGroup.get(key); }
806
802
  get effectiveSchema() {
807
803
  let schema = this.schema;
808
- if (schema.allOf?.length) {
804
+ if (schema.allOf?.length)
809
805
  schema = this.mergeSchemas([schema, ...schema.allOf]);
810
- }
811
806
  schema = this.applyConditional(schema);
812
807
  schema = this.applyDependencies(schema);
813
808
  return schema;
814
809
  }
815
810
  applyConditional(schema) {
816
- if (!schema.if) {
811
+ if (!schema.if)
817
812
  return schema;
818
- }
819
813
  const errors = this.validation.validate(schema.if, this.control.value);
820
814
  const branch = errors.length === 0 ? schema.then : schema.else;
821
- if (!branch) {
815
+ if (!branch)
822
816
  return schema;
823
- }
824
817
  return this.mergeSchemas([schema, branch]);
825
818
  }
826
819
  applyDependencies(schema) {
827
820
  const value = this.control.value;
828
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
821
+ if (!value || typeof value !== 'object' || Array.isArray(value))
829
822
  return schema;
830
- }
831
823
  let merged = { ...schema };
832
824
  const valueKeys = Object.keys(value);
833
825
  if (schema.dependentRequired) {
834
826
  for (const key of valueKeys) {
835
827
  const required = schema.dependentRequired[key];
836
- if (required?.length) {
828
+ if (required?.length)
837
829
  merged = this.mergeSchemas([merged, { required }]);
838
- }
839
830
  }
840
831
  }
841
832
  if (schema.dependentSchemas) {
842
833
  for (const key of valueKeys) {
843
834
  const depSchema = schema.dependentSchemas[key];
844
- if (depSchema) {
835
+ if (depSchema)
845
836
  merged = this.mergeSchemas([merged, depSchema]);
846
- }
847
837
  }
848
838
  }
849
839
  if (schema.dependencies) {
850
840
  for (const key of valueKeys) {
851
841
  const dep = schema.dependencies[key];
852
- if (Array.isArray(dep)) {
842
+ if (Array.isArray(dep))
853
843
  merged = this.mergeSchemas([merged, { required: dep }]);
854
- }
855
- else if (dep) {
844
+ else if (dep)
856
845
  merged = this.mergeSchemas([merged, dep]);
857
- }
858
846
  }
859
847
  }
860
848
  return merged;
@@ -877,306 +865,351 @@ class JsonSchemaNodeComponent {
877
865
  }, {});
878
866
  }
879
867
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaNodeComponent, deps: [{ token: JsonSchemaFormService }, { token: JsonSchemaValidationService }], target: i0.ɵɵFactoryTarget.Component });
880
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: JsonSchemaNodeComponent, isStandalone: true, selector: "jsm-schema-node", inputs: { schema: "schema", control: "control", label: "label", required: "required", path: "path", errorsMap: "errorsMap", parent: "parent", controlKey: "controlKey" }, outputs: { controlReplaced: "controlReplaced" }, ngImport: i0, template: `
881
- <ng-container [ngSwitch]="nodeKind">
882
- <div *ngSwitchCase="'oneOf'" class="space-y-3">
883
- <div class="flex items-center justify-between">
884
- <div>
885
- <div class="text-sm font-semibold text-slate-800">{{ schema.title || label }}</div>
886
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
887
- </div>
888
- <span class="text-xs text-slate-400">oneOf</span>
889
- </div>
890
-
891
- <select
892
- class="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-slate-900"
893
- [(ngModel)]="selectedOneOf"
894
- (ngModelChange)="onOneOfChange()"
895
- >
896
- <option *ngFor="let option of schema.oneOf; let i = index" [ngValue]="i">
897
- {{ option.title || ('Option ' + (i + 1)) }}
898
- </option>
899
- </select>
900
-
901
- <jsm-schema-node
902
- *ngIf="activeVariant"
903
- [schema]="activeVariant"
904
- [control]="control"
905
- [parent]="parent"
906
- [controlKey]="controlKey"
907
- [path]="path"
908
- [errorsMap]="errorsMap"
909
- [label]="label"
910
- [required]="required"
911
- (controlReplaced)="controlReplaced.emit($event)"
912
- ></jsm-schema-node>
913
- </div>
914
-
915
- <div *ngSwitchCase="'anyOf'" class="space-y-3">
916
- <div class="flex items-center justify-between">
917
- <div>
918
- <div class="text-sm font-semibold text-slate-800">{{ schema.title || label }}</div>
919
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
920
- </div>
921
- <span class="text-xs text-slate-400">anyOf</span>
922
- </div>
923
-
924
- <div class="flex flex-col gap-2">
925
- <label
926
- *ngFor="let option of schema.anyOf; let i = index"
927
- class="inline-flex items-center gap-2 text-sm text-slate-700"
928
- >
929
- <input
930
- type="checkbox"
931
- class="h-4 w-4 rounded border-slate-300 text-slate-900"
932
- [checked]="selectedAnyOf.has(i)"
933
- (change)="toggleAnyOf(i)"
934
- />
935
- {{ option.title || ('Option ' + (i + 1)) }}
936
- </label>
937
- </div>
938
-
939
- <jsm-schema-node
940
- *ngIf="activeVariant"
941
- [schema]="activeVariant"
942
- [control]="control"
943
- [parent]="parent"
944
- [controlKey]="controlKey"
945
- [path]="path"
946
- [errorsMap]="errorsMap"
947
- [label]="label"
948
- [required]="required"
949
- (controlReplaced)="controlReplaced.emit($event)"
950
- ></jsm-schema-node>
951
- </div>
952
-
953
- <div *ngSwitchCase="'allOf'" class="space-y-4">
954
- <div class="flex items-center justify-between">
955
- <div>
956
- <div class="text-sm font-semibold text-slate-800">{{ schema.title || label }}</div>
957
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
958
- </div>
959
- <span class="text-xs text-slate-400">allOf</span>
960
- </div>
961
-
962
- <div *ngFor="let subSchema of mergedAllOf; let i = index" class="rounded-xl border border-slate-200 p-3">
963
- <jsm-schema-node
964
- [schema]="subSchema"
965
- [control]="control"
966
- [parent]="parent"
967
- [controlKey]="controlKey"
968
- [path]="path"
969
- [errorsMap]="errorsMap"
970
- [label]="subSchema.title || ('Section ' + (i + 1))"
971
- [required]="required"
972
- (controlReplaced)="controlReplaced.emit($event)"
973
- ></jsm-schema-node>
974
- </div>
975
- </div>
976
-
977
- <fieldset *ngSwitchCase="'object'" class="group border border-slate-200 rounded-2xl p-4 bg-white/70 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md">
978
- <legend class="px-2 text-sm font-semibold text-slate-700">{{ schema.title || label }}</legend>
979
- <p *ngIf="schema.description" class="text-xs text-slate-500 mb-4">{{ schema.description }}</p>
980
- <div *ngIf="metaBadges.length" class="flex flex-wrap gap-2 mb-4">
981
- <span
982
- *ngFor="let badge of metaBadges"
983
- class="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600"
984
- >
985
- {{ badge }}
986
- </span>
987
- </div>
988
-
989
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4" [formGroup]="controlAsGroup">
990
- <ng-container *ngFor="let key of objectKeys">
991
- <div class="flex flex-col gap-2">
992
- <div class="flex items-center justify-between" *ngIf="isDynamicKey(key)">
993
- <span class="text-xs font-semibold text-slate-500">{{ key }}</span>
994
- <button
995
- type="button"
996
- class="text-xs font-semibold text-rose-600 transition hover:text-rose-700"
997
- (click)="removeDynamicKey(key)"
998
- >
999
- Remove
1000
- </button>
1001
- </div>
1002
- <jsm-schema-node
1003
- [schema]="schemaForKey(key)"
1004
- [control]="childControl(key)"
1005
- [parent]="controlAsGroup"
1006
- [controlKey]="key"
1007
- [path]="pathForChild(key)"
1008
- [errorsMap]="errorsMap"
1009
- [label]="key"
1010
- [required]="isRequired(key)"
1011
- (controlReplaced)="controlReplaced.emit($event)"
1012
- ></jsm-schema-node>
1013
- </div>
1014
- </ng-container>
1015
- </div>
1016
-
1017
- <div class="mt-4 space-y-2" *ngIf="canAddProperty">
1018
- <div class="text-xs font-semibold text-slate-500">Add property</div>
1019
- <div class="flex flex-col md:flex-row gap-2">
1020
- <input
1021
- class="flex-1 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900 hover:border-slate-400"
1022
- [(ngModel)]="newPropertyKey"
1023
- placeholder="propertyName"
1024
- />
1025
- <button
1026
- type="button"
1027
- class="px-3 py-2 rounded-lg text-sm font-medium bg-slate-900 text-white shadow-sm transition hover:-translate-y-0.5 hover:bg-slate-800 hover:shadow-md"
1028
- (click)="addDynamicProperty()"
1029
- >
1030
- Add
1031
- </button>
1032
- </div>
1033
- <div *ngIf="propertyError" class="text-xs text-rose-600">{{ propertyError }}</div>
1034
- </div>
1035
-
1036
- <div *ngIf="errorsForPath.length" class="mt-3 text-xs text-rose-600 space-y-1">
1037
- <div *ngFor="let error of errorsForPath">{{ error }}</div>
1038
- </div>
1039
- </fieldset>
1040
-
1041
- <div *ngSwitchCase="'array'" class="space-y-3">
1042
- <div class="flex items-center justify-between">
1043
- <div>
1044
- <div class="text-sm font-semibold text-slate-700">{{ schema.title || label }}</div>
1045
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
1046
- </div>
1047
- <button
1048
- type="button"
1049
- class="px-3 py-1.5 rounded-full text-sm font-medium bg-slate-900 text-white shadow-sm transition hover:-translate-y-0.5 hover:bg-slate-800 hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed"
1050
- (click)="addArrayItem()"
1051
- [disabled]="!canAddArrayItem"
1052
- >
1053
- Add
1054
- </button>
1055
- </div>
1056
-
1057
- <div class="text-xs text-slate-500" *ngIf="effectiveSchema.contains">
1058
- Must contain item(s) that match the contains schema.
1059
- <span *ngIf="effectiveSchema.minContains">Min: {{ effectiveSchema.minContains }}</span>
1060
- <span *ngIf="effectiveSchema.maxContains">Max: {{ effectiveSchema.maxContains }}</span>
1061
- </div>
1062
-
1063
- <div *ngIf="metaBadges.length" class="flex flex-wrap gap-2">
1064
- <span
1065
- *ngFor="let badge of metaBadges"
1066
- class="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600"
1067
- >
1068
- {{ badge }}
1069
- </span>
1070
- </div>
1071
-
1072
- <div class="space-y-3" *ngIf="arrayControls.length > 0; else emptyArray">
1073
- <div
1074
- *ngFor="let item of arrayControls; let i = index"
1075
- class="rounded-2xl border border-slate-200 p-3 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md"
1076
- >
1077
- <div class="flex items-center justify-between mb-2">
1078
- <span class="text-xs font-semibold text-slate-500">Item {{ i + 1 }}</span>
1079
- <button
1080
- type="button"
1081
- class="text-xs font-semibold text-rose-600 transition hover:text-rose-700"
1082
- (click)="removeArrayItem(i)"
1083
- >
1084
- Remove
1085
- </button>
1086
- </div>
1087
- <jsm-schema-node
1088
- [schema]="schemaForIndex(i)"
1089
- [control]="item"
1090
- [parent]="controlAsArray"
1091
- [controlKey]="i"
1092
- [path]="pathForChild(i)"
1093
- [errorsMap]="errorsMap"
1094
- [label]="label + ' item'"
1095
- (controlReplaced)="controlReplaced.emit($event)"
1096
- ></jsm-schema-node>
1097
- </div>
1098
- </div>
1099
-
1100
- <ng-template #emptyArray>
1101
- <div class="text-xs text-slate-400 italic">No items yet.</div>
1102
- </ng-template>
1103
-
1104
- <div *ngIf="errorsForPath.length" class="text-xs text-rose-600 space-y-1">
1105
- <div *ngFor="let error of errorsForPath">{{ error }}</div>
1106
- </div>
1107
- </div>
1108
-
1109
- <div *ngSwitchDefault class="space-y-2">
1110
- <ng-container *ngIf="inputKind !== 'checkbox'">
1111
- <label class="text-sm font-medium text-slate-700">
1112
- {{ schema.title || label }}
1113
- <span *ngIf="required" class="text-rose-500">*</span>
1114
- </label>
1115
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
1116
- </ng-container>
1117
-
1118
- <div *ngIf="metaBadges.length" class="flex flex-wrap gap-2">
1119
- <span
1120
- *ngFor="let badge of metaBadges"
1121
- class="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600"
1122
- >
1123
- {{ badge }}
1124
- </span>
1125
- </div>
1126
-
1127
- <ng-container [ngSwitch]="inputKind">
1128
- <input
1129
- *ngSwitchCase="'text'"
1130
- class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900 hover:border-slate-400"
1131
- [attr.type]="inputType"
1132
- [attr.placeholder]="schema.title || label"
1133
- [formControl]="controlAsFormControl"
1134
- />
1135
-
1136
- <textarea
1137
- *ngSwitchCase="'textarea'"
1138
- rows="4"
1139
- class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900 hover:border-slate-400"
1140
- [attr.placeholder]="schema.title || label"
1141
- [formControl]="controlAsFormControl"
1142
- ></textarea>
1143
-
1144
- <select
1145
- *ngSwitchCase="'select'"
1146
- class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900 hover:border-slate-400"
1147
- [formControl]="controlAsFormControl"
1148
- >
1149
- <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1150
- </select>
1151
-
1152
- <label *ngSwitchCase="'checkbox'" class="inline-flex items-center gap-2 text-sm text-slate-700">
1153
- <input type="checkbox" class="h-4 w-4 rounded border-slate-300 text-slate-900 transition" [formControl]="controlAsFormControl" />
1154
- <span>
1155
- {{ schema.title || label }}
1156
- <span *ngIf="required" class="text-rose-500">*</span>
1157
- </span>
1158
- </label>
1159
- </ng-container>
1160
-
1161
- <p *ngIf="inputKind === 'checkbox' && schema.description" class="text-xs text-slate-500">
1162
- {{ schema.description }}
1163
- </p>
1164
-
1165
- <div *ngIf="effectiveSchema.examples?.length" class="text-xs text-slate-400">
1166
- Example: {{ effectiveSchema.examples?.[0] }}
1167
- </div>
1168
-
1169
- <div *ngIf="effectiveSchema.contentMediaType" class="text-xs text-slate-400">
1170
- Content type: {{ effectiveSchema.contentMediaType }}
1171
- <span *ngIf="effectiveSchema.contentEncoding">({{ effectiveSchema.contentEncoding }})</span>
1172
- </div>
1173
-
1174
- <div *ngIf="errorsForPath.length" class="text-xs text-rose-600 space-y-1">
1175
- <div *ngFor="let error of errorsForPath">{{ error }}</div>
1176
- </div>
1177
- </div>
1178
- </ng-container>
1179
- `, isInline: true, dependencies: [{ kind: "component", type: JsonSchemaNodeComponent, selector: "jsm-schema-node", inputs: ["schema", "control", "label", "required", "path", "errorsMap", "parent", "controlKey"], outputs: ["controlReplaced"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i3.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i3.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.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: i4.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i4.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i4.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i4.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
868
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: JsonSchemaNodeComponent, isStandalone: true, selector: "jsm-schema-node", inputs: { schema: "schema", control: "control", label: "label", required: "required", path: "path", errorsMap: "errorsMap", parent: "parent", controlKey: "controlKey", allowAdditionalProperties: "allowAdditionalProperties" }, outputs: { controlReplaced: "controlReplaced" }, ngImport: i0, template: `
869
+ <ng-container [ngSwitch]="nodeKind">
870
+
871
+ <!-- ═══════════════════════════════════════════════════════════
872
+ oneOf — tab bar: ogni opzione è una tab, nessuna icona
873
+ ═══════════════════════════════════════════════════════════ -->
874
+ <div *ngSwitchCase="'oneOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
875
+ <div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
876
+ <div>
877
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
878
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
879
+ </div>
880
+ <span class="ml-3 shrink-0 rounded-full bg-blue-100 dark:bg-blue-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-blue-700 dark:text-blue-400">oneOf</span>
881
+ </div>
882
+ <div class="flex overflow-x-auto border-b border-slate-200 dark:border-slate-700">
883
+ <button
884
+ *ngFor="let option of schema.oneOf; let i = index"
885
+ type="button"
886
+ class="px-4 py-2.5 text-xs font-medium whitespace-nowrap border-b-2 transition-colors"
887
+ [class.border-indigo-500]="selectedOneOf === i"
888
+ [class.text-indigo-600]="selectedOneOf === i"
889
+ [class.dark:text-indigo-400]="selectedOneOf === i"
890
+ [class.font-semibold]="selectedOneOf === i"
891
+ [class.border-transparent]="selectedOneOf !== i"
892
+ [class.text-slate-500]="selectedOneOf !== i"
893
+ [class.dark:text-slate-400]="selectedOneOf !== i"
894
+ (click)="selectedOneOf = i; onOneOfChange()"
895
+ >{{ option.title || ('Option ' + (i + 1)) }}</button>
896
+ </div>
897
+ <div class="p-4" *ngIf="activeVariant">
898
+ <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
899
+ [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
900
+ [allowAdditionalProperties]="allowAdditionalProperties"
901
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
902
+ </div>
903
+ </div>
904
+
905
+ <!-- ═══════════════════════════════════════════════════════════
906
+ anyOf — pill toggle, nessuna icona
907
+ ═══════════════════════════════════════════════════════════ -->
908
+ <div *ngSwitchCase="'anyOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
909
+ <div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
910
+ <div>
911
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
912
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
913
+ </div>
914
+ <span class="ml-3 shrink-0 rounded-full bg-violet-100 dark:bg-violet-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-violet-700 dark:text-violet-400">anyOf</span>
915
+ </div>
916
+ <div class="flex flex-wrap gap-2 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
917
+ <label
918
+ *ngFor="let option of schema.anyOf; let i = index"
919
+ class="inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium cursor-pointer transition-colors select-none"
920
+ [class.border-indigo-400]="selectedAnyOf.has(i)"
921
+ [class.bg-indigo-50]="selectedAnyOf.has(i)"
922
+ [class.dark:bg-indigo-900/30]="selectedAnyOf.has(i)"
923
+ [class.text-indigo-700]="selectedAnyOf.has(i)"
924
+ [class.dark:text-indigo-400]="selectedAnyOf.has(i)"
925
+ [class.border-slate-200]="!selectedAnyOf.has(i)"
926
+ [class.dark:border-slate-600]="!selectedAnyOf.has(i)"
927
+ [class.text-slate-600]="!selectedAnyOf.has(i)"
928
+ [class.dark:text-slate-400]="!selectedAnyOf.has(i)"
929
+ >
930
+ <input type="checkbox" class="h-3.5 w-3.5 accent-indigo-600" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
931
+ {{ option.title || ('Option ' + (i + 1)) }}
932
+ </label>
933
+ </div>
934
+ <div class="p-4" *ngIf="activeVariant">
935
+ <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
936
+ [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
937
+ [allowAdditionalProperties]="allowAdditionalProperties"
938
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
939
+ </div>
940
+ </div>
941
+
942
+ <!-- ═══════════════════════════════════════════════════════════
943
+ allOf — sezioni in colonna con bordo sinistro
944
+ ═══════════════════════════════════════════════════════════ -->
945
+ <div *ngSwitchCase="'allOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
946
+ <div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
947
+ <div>
948
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
949
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
950
+ </div>
951
+ <span class="ml-3 shrink-0 rounded-full bg-teal-100 dark:bg-teal-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-teal-700 dark:text-teal-400">allOf</span>
952
+ </div>
953
+ <div class="p-4 space-y-4">
954
+ <div *ngFor="let subSchema of mergedAllOf; let i = index" class="border-l-2 border-indigo-200 dark:border-indigo-700 pl-4">
955
+ <jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
956
+ [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
957
+ [allowAdditionalProperties]="allowAdditionalProperties"
958
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
959
+ </div>
960
+ </div>
961
+ </div>
962
+
963
+ <!-- ═══════════════════════════════════════════════════════════
964
+ object — fieldset con griglia SOLO per campi primitivi.
965
+ Logica layout:
966
+ - I figli vengono classificati come "semplici" (primitive/enum/bool)
967
+ o "complessi" (object/array/oneOf/anyOf/allOf).
968
+ - I semplici vanno in griglia 2 colonne (sm:grid-cols-2).
969
+ - I complessi vanno sempre in colonna piena (col-span-2).
970
+ ═══════════════════════════════════════════════════════════ -->
971
+ <fieldset *ngSwitchCase="'object'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm p-4 transition-shadow focus-within:border-indigo-300 dark:focus-within:border-indigo-600 focus-within:ring-2 focus-within:ring-indigo-100 dark:focus-within:ring-indigo-900/40">
972
+ <legend class="px-2 text-sm font-bold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</legend>
973
+
974
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mb-3 mt-1">{{ schema.description }}</p>
975
+
976
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5 mb-3">
977
+ <span *ngFor="let badge of metaBadges"
978
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
979
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
980
+ </div>
981
+
982
+ <!-- griglia 2 col: i figli semplici occupano 1 col, i complessi 2 col -->
983
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4" [formGroup]="controlAsGroup">
984
+ <ng-container *ngFor="let key of objectKeys">
985
+ <div class="flex flex-col gap-1"
986
+ [class.sm:col-span-2]="!isSimpleChild(key)">
987
+ <div *ngIf="isDynamicKey(key)"
988
+ class="flex items-center justify-between rounded-md border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/50 px-2 py-1 mb-1">
989
+ <span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
990
+ <button type="button"
991
+ class="rounded px-1.5 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
992
+ (click)="removeDynamicKey(key)">Remove</button>
993
+ </div>
994
+ <jsm-schema-node
995
+ [schema]="schemaForKey(key)"
996
+ [control]="childControl(key)"
997
+ [parent]="controlAsGroup"
998
+ [controlKey]="key"
999
+ [path]="pathForChild(key)"
1000
+ [errorsMap]="errorsMap"
1001
+ [label]="key"
1002
+ [required]="isRequired(key)"
1003
+ [allowAdditionalProperties]="allowAdditionalProperties"
1004
+ (controlReplaced)="controlReplaced.emit($event)">
1005
+ </jsm-schema-node>
1006
+ </div>
1007
+ </ng-container>
1008
+ </div>
1009
+
1010
+ <!-- add dynamic property -->
1011
+ <div *ngIf="canAddProperty" class="mt-4 rounded-lg border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/40 p-3 space-y-2">
1012
+ <p class="text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500">Add property</p>
1013
+ <div class="flex gap-2">
1014
+ <input
1015
+ class="flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-1.5 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:focus:ring-indigo-900/40"
1016
+ [(ngModel)]="newPropertyKey" placeholder="propertyName" />
1017
+ <button type="button"
1018
+ class="rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700 active:scale-95"
1019
+ (click)="addDynamicProperty()">Add</button>
1020
+ </div>
1021
+ <p *ngIf="propertyError" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1022
+ <!-- icona errore: unico caso in cui ha senso, segnala un problema -->
1023
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1024
+ {{ propertyError }}
1025
+ </p>
1026
+ </div>
1027
+
1028
+ <div *ngIf="errorsForPath.length" class="mt-3 space-y-1">
1029
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1030
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1031
+ {{ error }}
1032
+ </p>
1033
+ </div>
1034
+ </fieldset>
1035
+
1036
+ <!-- ═══════════════════════════════════════════════════════════
1037
+ array — sempre in colonna piena, items in colonna
1038
+ ═══════════════════════════════════════════════════════════ -->
1039
+ <div *ngSwitchCase="'array'" class="space-y-3">
1040
+ <div class="flex items-start justify-between gap-2">
1041
+ <div>
1042
+ <p class="text-sm font-semibold text-slate-800">
1043
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1044
+ </p>
1045
+ <p *ngIf="schema.description" class="text-xs text-slate-500 mt-0.5">{{ schema.description }}</p>
1046
+ </div>
1047
+ <!-- icona + nel bottone: indica l'azione "aggiungi", non decorativa -->
1048
+ <button type="button"
1049
+ class="shrink-0 flex items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm transition hover:bg-indigo-700 active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed"
1050
+ (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1051
+ <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1052
+ Add item
1053
+ </button>
1054
+ </div>
1055
+
1056
+ <div *ngIf="effectiveSchema.contains" class="rounded-md bg-indigo-50 dark:bg-indigo-900/30 px-3 py-1.5 text-xs text-indigo-700 dark:text-indigo-400">
1057
+ Must contain matching item(s)
1058
+ <span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
1059
+ <span *ngIf="effectiveSchema.maxContains"> · max {{ effectiveSchema.maxContains }}</span>
1060
+ </div>
1061
+
1062
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
1063
+ <span *ngFor="let badge of metaBadges"
1064
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1065
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1066
+ </div>
1067
+
1068
+ <div class="space-y-2" *ngIf="arrayControls.length > 0; else emptyArray">
1069
+ <div *ngFor="let item of arrayControls; let i = index"
1070
+ class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-3 shadow-sm transition focus-within:border-indigo-300 dark:focus-within:border-indigo-600 focus-within:ring-2 focus-within:ring-indigo-100 dark:focus-within:ring-indigo-900/40">
1071
+ <div class="flex items-center justify-between mb-3">
1072
+ <span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-indigo-100 dark:bg-indigo-900/50 text-[11px] font-bold text-indigo-600 dark:text-indigo-400">{{ i + 1 }}</span>
1073
+ <button type="button"
1074
+ class="rounded-md border border-red-200 dark:border-red-800 px-2 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 transition hover:bg-red-50 dark:hover:bg-red-900/20"
1075
+ (click)="removeArrayItem(i)">Remove</button>
1076
+ </div>
1077
+ <jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
1078
+ [controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
1079
+ [allowAdditionalProperties]="allowAdditionalProperties"
1080
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1081
+ </div>
1082
+ </div>
1083
+
1084
+ <ng-template #emptyArray>
1085
+ <div class="flex flex-col items-center gap-2 rounded-xl border border-dashed border-slate-200 dark:border-slate-700 py-8 text-slate-400 dark:text-slate-500 text-xs">
1086
+ No items yet — click "Add item" to start.
1087
+ </div>
1088
+ </ng-template>
1089
+
1090
+ <div *ngIf="errorsForPath.length" class="space-y-1">
1091
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1092
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1093
+ {{ error }}
1094
+ </p>
1095
+ </div>
1096
+ </div>
1097
+
1098
+ <!-- ═══════════════════════════════════════════════════════════
1099
+ primitive — sempre in colonna, nessuna icona decorativa.
1100
+ Icona errore solo nei messaggi di validazione.
1101
+ Icona chevron nel select: funzionale (indica dropdown).
1102
+ ═══════════════════════════════════════════════════════════ -->
1103
+ <div *ngSwitchDefault class="space-y-1.5">
1104
+ <ng-container *ngIf="inputKind !== 'checkbox'">
1105
+ <label class="block text-[13px] font-semibold text-slate-700 dark:text-slate-300">
1106
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1107
+ </label>
1108
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 -mt-0.5">{{ schema.description }}</p>
1109
+ </ng-container>
1110
+
1111
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
1112
+ <span *ngFor="let badge of metaBadges"
1113
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1114
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1115
+ </div>
1116
+
1117
+ <ng-container [ngSwitch]="inputKind">
1118
+
1119
+ <input *ngSwitchCase="'text'"
1120
+ class="w-full rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-2 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:ring-2"
1121
+ [class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
1122
+ [class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
1123
+ [class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
1124
+ [class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
1125
+ [class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
1126
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1127
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1128
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1129
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1130
+ [class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
1131
+ [class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
1132
+ [attr.type]="inputType"
1133
+ [attr.placeholder]="schema.title || label"
1134
+ [formControl]="controlAsFormControl"
1135
+ />
1136
+
1137
+ <textarea *ngSwitchCase="'textarea'" rows="4"
1138
+ class="w-full resize-y rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-2 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:ring-2"
1139
+ [class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
1140
+ [class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
1141
+ [class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
1142
+ [class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
1143
+ [class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
1144
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1145
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1146
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1147
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1148
+ [class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
1149
+ [class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
1150
+ [attr.placeholder]="schema.title || label"
1151
+ [formControl]="controlAsFormControl"
1152
+ ></textarea>
1153
+
1154
+ <!-- select: il chevron SVG è funzionale, indica che è un dropdown -->
1155
+ <div *ngSwitchCase="'select'" class="relative">
1156
+ <select
1157
+ class="w-full appearance-none rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 px-3 py-2 pr-8 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:focus:ring-indigo-900/40"
1158
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1159
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1160
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1161
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1162
+ [formControl]="controlAsFormControl">
1163
+ <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1164
+ </select>
1165
+ <svg class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500"
1166
+ viewBox="0 0 16 16" fill="none">
1167
+ <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1168
+ </svg>
1169
+ </div>
1170
+
1171
+ <!-- checkbox come toggle switch -->
1172
+ <label *ngSwitchCase="'checkbox'" class="inline-flex cursor-pointer items-center gap-2.5">
1173
+ <span class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors"
1174
+ [class.bg-indigo-600]="controlAsFormControl.value"
1175
+ [class.bg-slate-300]="!controlAsFormControl.value"
1176
+ [class.dark:bg-slate-600]="!controlAsFormControl.value">
1177
+ <input type="checkbox" class="sr-only" [formControl]="controlAsFormControl" />
1178
+ <span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform"
1179
+ [class.translate-x-4]="controlAsFormControl.value"
1180
+ [class.translate-x-0.5]="!controlAsFormControl.value"></span>
1181
+ </span>
1182
+ <span class="text-sm font-medium text-slate-700 dark:text-slate-300">
1183
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1184
+ </span>
1185
+ </label>
1186
+
1187
+ </ng-container>
1188
+
1189
+ <p *ngIf="inputKind === 'checkbox' && schema.description" class="text-xs text-slate-500 dark:text-slate-400">{{ schema.description }}</p>
1190
+
1191
+ <p *ngIf="effectiveSchema.examples?.length" class="text-[11px] text-slate-400 dark:text-slate-500">
1192
+ e.g. {{ effectiveSchema.examples?.[0] }}
1193
+ </p>
1194
+
1195
+ <p *ngIf="effectiveSchema.contentMediaType" class="text-[11px] text-slate-400 dark:text-slate-500">
1196
+ {{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
1197
+ </p>
1198
+
1199
+ <!-- icona errore: funzionale, segnala un problema di validazione -->
1200
+ <div *ngIf="errorsForPath.length && controlAsFormControl.dirty" class="space-y-1 pt-0.5">
1201
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1202
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
1203
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
1204
+ <path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1205
+ </svg>
1206
+ {{ error }}
1207
+ </p>
1208
+ </div>
1209
+ </div>
1210
+
1211
+ </ng-container>
1212
+ `, isInline: true, dependencies: [{ kind: "component", type: JsonSchemaNodeComponent, selector: "jsm-schema-node", inputs: ["schema", "control", "label", "required", "path", "errorsMap", "parent", "controlKey", "allowAdditionalProperties"], outputs: ["controlReplaced"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i3.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i3.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.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: i4.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i4.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i4.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i4.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1180
1213
  }
1181
1214
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaNodeComponent, decorators: [{
1182
1215
  type: Component,
@@ -1184,305 +1217,350 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1184
1217
  selector: 'jsm-schema-node',
1185
1218
  standalone: true,
1186
1219
  imports: [CommonModule, FormsModule, ReactiveFormsModule],
1187
- template: `
1188
- <ng-container [ngSwitch]="nodeKind">
1189
- <div *ngSwitchCase="'oneOf'" class="space-y-3">
1190
- <div class="flex items-center justify-between">
1191
- <div>
1192
- <div class="text-sm font-semibold text-slate-800">{{ schema.title || label }}</div>
1193
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
1194
- </div>
1195
- <span class="text-xs text-slate-400">oneOf</span>
1196
- </div>
1197
-
1198
- <select
1199
- class="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-slate-900"
1200
- [(ngModel)]="selectedOneOf"
1201
- (ngModelChange)="onOneOfChange()"
1202
- >
1203
- <option *ngFor="let option of schema.oneOf; let i = index" [ngValue]="i">
1204
- {{ option.title || ('Option ' + (i + 1)) }}
1205
- </option>
1206
- </select>
1207
-
1208
- <jsm-schema-node
1209
- *ngIf="activeVariant"
1210
- [schema]="activeVariant"
1211
- [control]="control"
1212
- [parent]="parent"
1213
- [controlKey]="controlKey"
1214
- [path]="path"
1215
- [errorsMap]="errorsMap"
1216
- [label]="label"
1217
- [required]="required"
1218
- (controlReplaced)="controlReplaced.emit($event)"
1219
- ></jsm-schema-node>
1220
- </div>
1221
-
1222
- <div *ngSwitchCase="'anyOf'" class="space-y-3">
1223
- <div class="flex items-center justify-between">
1224
- <div>
1225
- <div class="text-sm font-semibold text-slate-800">{{ schema.title || label }}</div>
1226
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
1227
- </div>
1228
- <span class="text-xs text-slate-400">anyOf</span>
1229
- </div>
1230
-
1231
- <div class="flex flex-col gap-2">
1232
- <label
1233
- *ngFor="let option of schema.anyOf; let i = index"
1234
- class="inline-flex items-center gap-2 text-sm text-slate-700"
1235
- >
1236
- <input
1237
- type="checkbox"
1238
- class="h-4 w-4 rounded border-slate-300 text-slate-900"
1239
- [checked]="selectedAnyOf.has(i)"
1240
- (change)="toggleAnyOf(i)"
1241
- />
1242
- {{ option.title || ('Option ' + (i + 1)) }}
1243
- </label>
1244
- </div>
1245
-
1246
- <jsm-schema-node
1247
- *ngIf="activeVariant"
1248
- [schema]="activeVariant"
1249
- [control]="control"
1250
- [parent]="parent"
1251
- [controlKey]="controlKey"
1252
- [path]="path"
1253
- [errorsMap]="errorsMap"
1254
- [label]="label"
1255
- [required]="required"
1256
- (controlReplaced)="controlReplaced.emit($event)"
1257
- ></jsm-schema-node>
1258
- </div>
1259
-
1260
- <div *ngSwitchCase="'allOf'" class="space-y-4">
1261
- <div class="flex items-center justify-between">
1262
- <div>
1263
- <div class="text-sm font-semibold text-slate-800">{{ schema.title || label }}</div>
1264
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
1265
- </div>
1266
- <span class="text-xs text-slate-400">allOf</span>
1267
- </div>
1268
-
1269
- <div *ngFor="let subSchema of mergedAllOf; let i = index" class="rounded-xl border border-slate-200 p-3">
1270
- <jsm-schema-node
1271
- [schema]="subSchema"
1272
- [control]="control"
1273
- [parent]="parent"
1274
- [controlKey]="controlKey"
1275
- [path]="path"
1276
- [errorsMap]="errorsMap"
1277
- [label]="subSchema.title || ('Section ' + (i + 1))"
1278
- [required]="required"
1279
- (controlReplaced)="controlReplaced.emit($event)"
1280
- ></jsm-schema-node>
1281
- </div>
1282
- </div>
1283
-
1284
- <fieldset *ngSwitchCase="'object'" class="group border border-slate-200 rounded-2xl p-4 bg-white/70 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md">
1285
- <legend class="px-2 text-sm font-semibold text-slate-700">{{ schema.title || label }}</legend>
1286
- <p *ngIf="schema.description" class="text-xs text-slate-500 mb-4">{{ schema.description }}</p>
1287
- <div *ngIf="metaBadges.length" class="flex flex-wrap gap-2 mb-4">
1288
- <span
1289
- *ngFor="let badge of metaBadges"
1290
- class="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600"
1291
- >
1292
- {{ badge }}
1293
- </span>
1294
- </div>
1295
-
1296
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4" [formGroup]="controlAsGroup">
1297
- <ng-container *ngFor="let key of objectKeys">
1298
- <div class="flex flex-col gap-2">
1299
- <div class="flex items-center justify-between" *ngIf="isDynamicKey(key)">
1300
- <span class="text-xs font-semibold text-slate-500">{{ key }}</span>
1301
- <button
1302
- type="button"
1303
- class="text-xs font-semibold text-rose-600 transition hover:text-rose-700"
1304
- (click)="removeDynamicKey(key)"
1305
- >
1306
- Remove
1307
- </button>
1308
- </div>
1309
- <jsm-schema-node
1310
- [schema]="schemaForKey(key)"
1311
- [control]="childControl(key)"
1312
- [parent]="controlAsGroup"
1313
- [controlKey]="key"
1314
- [path]="pathForChild(key)"
1315
- [errorsMap]="errorsMap"
1316
- [label]="key"
1317
- [required]="isRequired(key)"
1318
- (controlReplaced)="controlReplaced.emit($event)"
1319
- ></jsm-schema-node>
1320
- </div>
1321
- </ng-container>
1322
- </div>
1323
-
1324
- <div class="mt-4 space-y-2" *ngIf="canAddProperty">
1325
- <div class="text-xs font-semibold text-slate-500">Add property</div>
1326
- <div class="flex flex-col md:flex-row gap-2">
1327
- <input
1328
- class="flex-1 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900 hover:border-slate-400"
1329
- [(ngModel)]="newPropertyKey"
1330
- placeholder="propertyName"
1331
- />
1332
- <button
1333
- type="button"
1334
- class="px-3 py-2 rounded-lg text-sm font-medium bg-slate-900 text-white shadow-sm transition hover:-translate-y-0.5 hover:bg-slate-800 hover:shadow-md"
1335
- (click)="addDynamicProperty()"
1336
- >
1337
- Add
1338
- </button>
1339
- </div>
1340
- <div *ngIf="propertyError" class="text-xs text-rose-600">{{ propertyError }}</div>
1341
- </div>
1342
-
1343
- <div *ngIf="errorsForPath.length" class="mt-3 text-xs text-rose-600 space-y-1">
1344
- <div *ngFor="let error of errorsForPath">{{ error }}</div>
1345
- </div>
1346
- </fieldset>
1347
-
1348
- <div *ngSwitchCase="'array'" class="space-y-3">
1349
- <div class="flex items-center justify-between">
1350
- <div>
1351
- <div class="text-sm font-semibold text-slate-700">{{ schema.title || label }}</div>
1352
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
1353
- </div>
1354
- <button
1355
- type="button"
1356
- class="px-3 py-1.5 rounded-full text-sm font-medium bg-slate-900 text-white shadow-sm transition hover:-translate-y-0.5 hover:bg-slate-800 hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed"
1357
- (click)="addArrayItem()"
1358
- [disabled]="!canAddArrayItem"
1359
- >
1360
- Add
1361
- </button>
1362
- </div>
1363
-
1364
- <div class="text-xs text-slate-500" *ngIf="effectiveSchema.contains">
1365
- Must contain item(s) that match the contains schema.
1366
- <span *ngIf="effectiveSchema.minContains">Min: {{ effectiveSchema.minContains }}</span>
1367
- <span *ngIf="effectiveSchema.maxContains">Max: {{ effectiveSchema.maxContains }}</span>
1368
- </div>
1369
-
1370
- <div *ngIf="metaBadges.length" class="flex flex-wrap gap-2">
1371
- <span
1372
- *ngFor="let badge of metaBadges"
1373
- class="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600"
1374
- >
1375
- {{ badge }}
1376
- </span>
1377
- </div>
1378
-
1379
- <div class="space-y-3" *ngIf="arrayControls.length > 0; else emptyArray">
1380
- <div
1381
- *ngFor="let item of arrayControls; let i = index"
1382
- class="rounded-2xl border border-slate-200 p-3 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md"
1383
- >
1384
- <div class="flex items-center justify-between mb-2">
1385
- <span class="text-xs font-semibold text-slate-500">Item {{ i + 1 }}</span>
1386
- <button
1387
- type="button"
1388
- class="text-xs font-semibold text-rose-600 transition hover:text-rose-700"
1389
- (click)="removeArrayItem(i)"
1390
- >
1391
- Remove
1392
- </button>
1393
- </div>
1394
- <jsm-schema-node
1395
- [schema]="schemaForIndex(i)"
1396
- [control]="item"
1397
- [parent]="controlAsArray"
1398
- [controlKey]="i"
1399
- [path]="pathForChild(i)"
1400
- [errorsMap]="errorsMap"
1401
- [label]="label + ' item'"
1402
- (controlReplaced)="controlReplaced.emit($event)"
1403
- ></jsm-schema-node>
1404
- </div>
1405
- </div>
1406
-
1407
- <ng-template #emptyArray>
1408
- <div class="text-xs text-slate-400 italic">No items yet.</div>
1409
- </ng-template>
1410
-
1411
- <div *ngIf="errorsForPath.length" class="text-xs text-rose-600 space-y-1">
1412
- <div *ngFor="let error of errorsForPath">{{ error }}</div>
1413
- </div>
1414
- </div>
1415
-
1416
- <div *ngSwitchDefault class="space-y-2">
1417
- <ng-container *ngIf="inputKind !== 'checkbox'">
1418
- <label class="text-sm font-medium text-slate-700">
1419
- {{ schema.title || label }}
1420
- <span *ngIf="required" class="text-rose-500">*</span>
1421
- </label>
1422
- <p *ngIf="schema.description" class="text-xs text-slate-500">{{ schema.description }}</p>
1423
- </ng-container>
1424
-
1425
- <div *ngIf="metaBadges.length" class="flex flex-wrap gap-2">
1426
- <span
1427
- *ngFor="let badge of metaBadges"
1428
- class="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600"
1429
- >
1430
- {{ badge }}
1431
- </span>
1432
- </div>
1433
-
1434
- <ng-container [ngSwitch]="inputKind">
1435
- <input
1436
- *ngSwitchCase="'text'"
1437
- class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900 hover:border-slate-400"
1438
- [attr.type]="inputType"
1439
- [attr.placeholder]="schema.title || label"
1440
- [formControl]="controlAsFormControl"
1441
- />
1442
-
1443
- <textarea
1444
- *ngSwitchCase="'textarea'"
1445
- rows="4"
1446
- class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900 hover:border-slate-400"
1447
- [attr.placeholder]="schema.title || label"
1448
- [formControl]="controlAsFormControl"
1449
- ></textarea>
1450
-
1451
- <select
1452
- *ngSwitchCase="'select'"
1453
- class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm shadow-sm transition focus:outline-none focus:ring-2 focus:ring-slate-900 hover:border-slate-400"
1454
- [formControl]="controlAsFormControl"
1455
- >
1456
- <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1457
- </select>
1458
-
1459
- <label *ngSwitchCase="'checkbox'" class="inline-flex items-center gap-2 text-sm text-slate-700">
1460
- <input type="checkbox" class="h-4 w-4 rounded border-slate-300 text-slate-900 transition" [formControl]="controlAsFormControl" />
1461
- <span>
1462
- {{ schema.title || label }}
1463
- <span *ngIf="required" class="text-rose-500">*</span>
1464
- </span>
1465
- </label>
1466
- </ng-container>
1467
-
1468
- <p *ngIf="inputKind === 'checkbox' && schema.description" class="text-xs text-slate-500">
1469
- {{ schema.description }}
1470
- </p>
1471
-
1472
- <div *ngIf="effectiveSchema.examples?.length" class="text-xs text-slate-400">
1473
- Example: {{ effectiveSchema.examples?.[0] }}
1474
- </div>
1475
-
1476
- <div *ngIf="effectiveSchema.contentMediaType" class="text-xs text-slate-400">
1477
- Content type: {{ effectiveSchema.contentMediaType }}
1478
- <span *ngIf="effectiveSchema.contentEncoding">({{ effectiveSchema.contentEncoding }})</span>
1479
- </div>
1480
-
1481
- <div *ngIf="errorsForPath.length" class="text-xs text-rose-600 space-y-1">
1482
- <div *ngFor="let error of errorsForPath">{{ error }}</div>
1483
- </div>
1484
- </div>
1485
- </ng-container>
1220
+ template: `
1221
+ <ng-container [ngSwitch]="nodeKind">
1222
+
1223
+ <!-- ═══════════════════════════════════════════════════════════
1224
+ oneOf — tab bar: ogni opzione è una tab, nessuna icona
1225
+ ═══════════════════════════════════════════════════════════ -->
1226
+ <div *ngSwitchCase="'oneOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
1227
+ <div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
1228
+ <div>
1229
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
1230
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
1231
+ </div>
1232
+ <span class="ml-3 shrink-0 rounded-full bg-blue-100 dark:bg-blue-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-blue-700 dark:text-blue-400">oneOf</span>
1233
+ </div>
1234
+ <div class="flex overflow-x-auto border-b border-slate-200 dark:border-slate-700">
1235
+ <button
1236
+ *ngFor="let option of schema.oneOf; let i = index"
1237
+ type="button"
1238
+ class="px-4 py-2.5 text-xs font-medium whitespace-nowrap border-b-2 transition-colors"
1239
+ [class.border-indigo-500]="selectedOneOf === i"
1240
+ [class.text-indigo-600]="selectedOneOf === i"
1241
+ [class.dark:text-indigo-400]="selectedOneOf === i"
1242
+ [class.font-semibold]="selectedOneOf === i"
1243
+ [class.border-transparent]="selectedOneOf !== i"
1244
+ [class.text-slate-500]="selectedOneOf !== i"
1245
+ [class.dark:text-slate-400]="selectedOneOf !== i"
1246
+ (click)="selectedOneOf = i; onOneOfChange()"
1247
+ >{{ option.title || ('Option ' + (i + 1)) }}</button>
1248
+ </div>
1249
+ <div class="p-4" *ngIf="activeVariant">
1250
+ <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
1251
+ [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
1252
+ [allowAdditionalProperties]="allowAdditionalProperties"
1253
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1254
+ </div>
1255
+ </div>
1256
+
1257
+ <!-- ═══════════════════════════════════════════════════════════
1258
+ anyOf — pill toggle, nessuna icona
1259
+ ═══════════════════════════════════════════════════════════ -->
1260
+ <div *ngSwitchCase="'anyOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
1261
+ <div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
1262
+ <div>
1263
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
1264
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
1265
+ </div>
1266
+ <span class="ml-3 shrink-0 rounded-full bg-violet-100 dark:bg-violet-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-violet-700 dark:text-violet-400">anyOf</span>
1267
+ </div>
1268
+ <div class="flex flex-wrap gap-2 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
1269
+ <label
1270
+ *ngFor="let option of schema.anyOf; let i = index"
1271
+ class="inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium cursor-pointer transition-colors select-none"
1272
+ [class.border-indigo-400]="selectedAnyOf.has(i)"
1273
+ [class.bg-indigo-50]="selectedAnyOf.has(i)"
1274
+ [class.dark:bg-indigo-900/30]="selectedAnyOf.has(i)"
1275
+ [class.text-indigo-700]="selectedAnyOf.has(i)"
1276
+ [class.dark:text-indigo-400]="selectedAnyOf.has(i)"
1277
+ [class.border-slate-200]="!selectedAnyOf.has(i)"
1278
+ [class.dark:border-slate-600]="!selectedAnyOf.has(i)"
1279
+ [class.text-slate-600]="!selectedAnyOf.has(i)"
1280
+ [class.dark:text-slate-400]="!selectedAnyOf.has(i)"
1281
+ >
1282
+ <input type="checkbox" class="h-3.5 w-3.5 accent-indigo-600" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
1283
+ {{ option.title || ('Option ' + (i + 1)) }}
1284
+ </label>
1285
+ </div>
1286
+ <div class="p-4" *ngIf="activeVariant">
1287
+ <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
1288
+ [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
1289
+ [allowAdditionalProperties]="allowAdditionalProperties"
1290
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1291
+ </div>
1292
+ </div>
1293
+
1294
+ <!-- ═══════════════════════════════════════════════════════════
1295
+ allOf — sezioni in colonna con bordo sinistro
1296
+ ═══════════════════════════════════════════════════════════ -->
1297
+ <div *ngSwitchCase="'allOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
1298
+ <div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
1299
+ <div>
1300
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
1301
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
1302
+ </div>
1303
+ <span class="ml-3 shrink-0 rounded-full bg-teal-100 dark:bg-teal-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-teal-700 dark:text-teal-400">allOf</span>
1304
+ </div>
1305
+ <div class="p-4 space-y-4">
1306
+ <div *ngFor="let subSchema of mergedAllOf; let i = index" class="border-l-2 border-indigo-200 dark:border-indigo-700 pl-4">
1307
+ <jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
1308
+ [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
1309
+ [allowAdditionalProperties]="allowAdditionalProperties"
1310
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1311
+ </div>
1312
+ </div>
1313
+ </div>
1314
+
1315
+ <!-- ═══════════════════════════════════════════════════════════
1316
+ object — fieldset con griglia SOLO per campi primitivi.
1317
+ Logica layout:
1318
+ - I figli vengono classificati come "semplici" (primitive/enum/bool)
1319
+ o "complessi" (object/array/oneOf/anyOf/allOf).
1320
+ - I semplici vanno in griglia 2 colonne (sm:grid-cols-2).
1321
+ - I complessi vanno sempre in colonna piena (col-span-2).
1322
+ ═══════════════════════════════════════════════════════════ -->
1323
+ <fieldset *ngSwitchCase="'object'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm p-4 transition-shadow focus-within:border-indigo-300 dark:focus-within:border-indigo-600 focus-within:ring-2 focus-within:ring-indigo-100 dark:focus-within:ring-indigo-900/40">
1324
+ <legend class="px-2 text-sm font-bold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</legend>
1325
+
1326
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mb-3 mt-1">{{ schema.description }}</p>
1327
+
1328
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5 mb-3">
1329
+ <span *ngFor="let badge of metaBadges"
1330
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1331
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1332
+ </div>
1333
+
1334
+ <!-- griglia 2 col: i figli semplici occupano 1 col, i complessi 2 col -->
1335
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4" [formGroup]="controlAsGroup">
1336
+ <ng-container *ngFor="let key of objectKeys">
1337
+ <div class="flex flex-col gap-1"
1338
+ [class.sm:col-span-2]="!isSimpleChild(key)">
1339
+ <div *ngIf="isDynamicKey(key)"
1340
+ class="flex items-center justify-between rounded-md border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/50 px-2 py-1 mb-1">
1341
+ <span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
1342
+ <button type="button"
1343
+ class="rounded px-1.5 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
1344
+ (click)="removeDynamicKey(key)">Remove</button>
1345
+ </div>
1346
+ <jsm-schema-node
1347
+ [schema]="schemaForKey(key)"
1348
+ [control]="childControl(key)"
1349
+ [parent]="controlAsGroup"
1350
+ [controlKey]="key"
1351
+ [path]="pathForChild(key)"
1352
+ [errorsMap]="errorsMap"
1353
+ [label]="key"
1354
+ [required]="isRequired(key)"
1355
+ [allowAdditionalProperties]="allowAdditionalProperties"
1356
+ (controlReplaced)="controlReplaced.emit($event)">
1357
+ </jsm-schema-node>
1358
+ </div>
1359
+ </ng-container>
1360
+ </div>
1361
+
1362
+ <!-- add dynamic property -->
1363
+ <div *ngIf="canAddProperty" class="mt-4 rounded-lg border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/40 p-3 space-y-2">
1364
+ <p class="text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500">Add property</p>
1365
+ <div class="flex gap-2">
1366
+ <input
1367
+ class="flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-1.5 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:focus:ring-indigo-900/40"
1368
+ [(ngModel)]="newPropertyKey" placeholder="propertyName" />
1369
+ <button type="button"
1370
+ class="rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700 active:scale-95"
1371
+ (click)="addDynamicProperty()">Add</button>
1372
+ </div>
1373
+ <p *ngIf="propertyError" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1374
+ <!-- icona errore: unico caso in cui ha senso, segnala un problema -->
1375
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1376
+ {{ propertyError }}
1377
+ </p>
1378
+ </div>
1379
+
1380
+ <div *ngIf="errorsForPath.length" class="mt-3 space-y-1">
1381
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1382
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1383
+ {{ error }}
1384
+ </p>
1385
+ </div>
1386
+ </fieldset>
1387
+
1388
+ <!-- ═══════════════════════════════════════════════════════════
1389
+ array — sempre in colonna piena, items in colonna
1390
+ ═══════════════════════════════════════════════════════════ -->
1391
+ <div *ngSwitchCase="'array'" class="space-y-3">
1392
+ <div class="flex items-start justify-between gap-2">
1393
+ <div>
1394
+ <p class="text-sm font-semibold text-slate-800">
1395
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1396
+ </p>
1397
+ <p *ngIf="schema.description" class="text-xs text-slate-500 mt-0.5">{{ schema.description }}</p>
1398
+ </div>
1399
+ <!-- icona + nel bottone: indica l'azione "aggiungi", non decorativa -->
1400
+ <button type="button"
1401
+ class="shrink-0 flex items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm transition hover:bg-indigo-700 active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed"
1402
+ (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1403
+ <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1404
+ Add item
1405
+ </button>
1406
+ </div>
1407
+
1408
+ <div *ngIf="effectiveSchema.contains" class="rounded-md bg-indigo-50 dark:bg-indigo-900/30 px-3 py-1.5 text-xs text-indigo-700 dark:text-indigo-400">
1409
+ Must contain matching item(s)
1410
+ <span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
1411
+ <span *ngIf="effectiveSchema.maxContains"> · max {{ effectiveSchema.maxContains }}</span>
1412
+ </div>
1413
+
1414
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
1415
+ <span *ngFor="let badge of metaBadges"
1416
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1417
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1418
+ </div>
1419
+
1420
+ <div class="space-y-2" *ngIf="arrayControls.length > 0; else emptyArray">
1421
+ <div *ngFor="let item of arrayControls; let i = index"
1422
+ class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-3 shadow-sm transition focus-within:border-indigo-300 dark:focus-within:border-indigo-600 focus-within:ring-2 focus-within:ring-indigo-100 dark:focus-within:ring-indigo-900/40">
1423
+ <div class="flex items-center justify-between mb-3">
1424
+ <span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-indigo-100 dark:bg-indigo-900/50 text-[11px] font-bold text-indigo-600 dark:text-indigo-400">{{ i + 1 }}</span>
1425
+ <button type="button"
1426
+ class="rounded-md border border-red-200 dark:border-red-800 px-2 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 transition hover:bg-red-50 dark:hover:bg-red-900/20"
1427
+ (click)="removeArrayItem(i)">Remove</button>
1428
+ </div>
1429
+ <jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
1430
+ [controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
1431
+ [allowAdditionalProperties]="allowAdditionalProperties"
1432
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1433
+ </div>
1434
+ </div>
1435
+
1436
+ <ng-template #emptyArray>
1437
+ <div class="flex flex-col items-center gap-2 rounded-xl border border-dashed border-slate-200 dark:border-slate-700 py-8 text-slate-400 dark:text-slate-500 text-xs">
1438
+ No items yet — click "Add item" to start.
1439
+ </div>
1440
+ </ng-template>
1441
+
1442
+ <div *ngIf="errorsForPath.length" class="space-y-1">
1443
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1444
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1445
+ {{ error }}
1446
+ </p>
1447
+ </div>
1448
+ </div>
1449
+
1450
+ <!-- ═══════════════════════════════════════════════════════════
1451
+ primitive — sempre in colonna, nessuna icona decorativa.
1452
+ Icona errore solo nei messaggi di validazione.
1453
+ Icona chevron nel select: funzionale (indica dropdown).
1454
+ ═══════════════════════════════════════════════════════════ -->
1455
+ <div *ngSwitchDefault class="space-y-1.5">
1456
+ <ng-container *ngIf="inputKind !== 'checkbox'">
1457
+ <label class="block text-[13px] font-semibold text-slate-700 dark:text-slate-300">
1458
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1459
+ </label>
1460
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 -mt-0.5">{{ schema.description }}</p>
1461
+ </ng-container>
1462
+
1463
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
1464
+ <span *ngFor="let badge of metaBadges"
1465
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1466
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1467
+ </div>
1468
+
1469
+ <ng-container [ngSwitch]="inputKind">
1470
+
1471
+ <input *ngSwitchCase="'text'"
1472
+ class="w-full rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-2 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:ring-2"
1473
+ [class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
1474
+ [class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
1475
+ [class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
1476
+ [class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
1477
+ [class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
1478
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1479
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1480
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1481
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1482
+ [class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
1483
+ [class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
1484
+ [attr.type]="inputType"
1485
+ [attr.placeholder]="schema.title || label"
1486
+ [formControl]="controlAsFormControl"
1487
+ />
1488
+
1489
+ <textarea *ngSwitchCase="'textarea'" rows="4"
1490
+ class="w-full resize-y rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-2 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:ring-2"
1491
+ [class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
1492
+ [class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
1493
+ [class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
1494
+ [class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
1495
+ [class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
1496
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1497
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1498
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1499
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1500
+ [class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
1501
+ [class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
1502
+ [attr.placeholder]="schema.title || label"
1503
+ [formControl]="controlAsFormControl"
1504
+ ></textarea>
1505
+
1506
+ <!-- select: il chevron SVG è funzionale, indica che è un dropdown -->
1507
+ <div *ngSwitchCase="'select'" class="relative">
1508
+ <select
1509
+ class="w-full appearance-none rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 px-3 py-2 pr-8 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:focus:ring-indigo-900/40"
1510
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1511
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1512
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1513
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1514
+ [formControl]="controlAsFormControl">
1515
+ <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1516
+ </select>
1517
+ <svg class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500"
1518
+ viewBox="0 0 16 16" fill="none">
1519
+ <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1520
+ </svg>
1521
+ </div>
1522
+
1523
+ <!-- checkbox come toggle switch -->
1524
+ <label *ngSwitchCase="'checkbox'" class="inline-flex cursor-pointer items-center gap-2.5">
1525
+ <span class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors"
1526
+ [class.bg-indigo-600]="controlAsFormControl.value"
1527
+ [class.bg-slate-300]="!controlAsFormControl.value"
1528
+ [class.dark:bg-slate-600]="!controlAsFormControl.value">
1529
+ <input type="checkbox" class="sr-only" [formControl]="controlAsFormControl" />
1530
+ <span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform"
1531
+ [class.translate-x-4]="controlAsFormControl.value"
1532
+ [class.translate-x-0.5]="!controlAsFormControl.value"></span>
1533
+ </span>
1534
+ <span class="text-sm font-medium text-slate-700 dark:text-slate-300">
1535
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1536
+ </span>
1537
+ </label>
1538
+
1539
+ </ng-container>
1540
+
1541
+ <p *ngIf="inputKind === 'checkbox' && schema.description" class="text-xs text-slate-500 dark:text-slate-400">{{ schema.description }}</p>
1542
+
1543
+ <p *ngIf="effectiveSchema.examples?.length" class="text-[11px] text-slate-400 dark:text-slate-500">
1544
+ e.g. {{ effectiveSchema.examples?.[0] }}
1545
+ </p>
1546
+
1547
+ <p *ngIf="effectiveSchema.contentMediaType" class="text-[11px] text-slate-400 dark:text-slate-500">
1548
+ {{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
1549
+ </p>
1550
+
1551
+ <!-- icona errore: funzionale, segnala un problema di validazione -->
1552
+ <div *ngIf="errorsForPath.length && controlAsFormControl.dirty" class="space-y-1 pt-0.5">
1553
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1554
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
1555
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
1556
+ <path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1557
+ </svg>
1558
+ {{ error }}
1559
+ </p>
1560
+ </div>
1561
+ </div>
1562
+
1563
+ </ng-container>
1486
1564
  `,
1487
1565
  changeDetection: ChangeDetectionStrategy.OnPush,
1488
1566
  }]
@@ -1504,6 +1582,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1504
1582
  type: Input
1505
1583
  }], controlKey: [{
1506
1584
  type: Input
1585
+ }], allowAdditionalProperties: [{
1586
+ type: Input
1507
1587
  }], controlReplaced: [{
1508
1588
  type: Output
1509
1589
  }] } });
@@ -1515,13 +1595,16 @@ class JsonSchemaFormComponent {
1515
1595
  schema;
1516
1596
  value;
1517
1597
  data;
1598
+ allowAdditionalProperties = false;
1518
1599
  formReady = new EventEmitter();
1519
1600
  valueChange = new EventEmitter();
1520
1601
  schemaReady = new EventEmitter();
1602
+ schemaErrors = new EventEmitter();
1521
1603
  form;
1522
1604
  rootControl;
1523
1605
  resolvedSchema;
1524
1606
  errorsMap = new Map();
1607
+ schemaErrorList = [];
1525
1608
  loading = false;
1526
1609
  valueSub;
1527
1610
  constructor(schemaService, resolver, validation) {
@@ -1541,7 +1624,17 @@ class JsonSchemaFormComponent {
1541
1624
  this.valueSub?.unsubscribe();
1542
1625
  this.loading = true;
1543
1626
  this.resolvedSchema = await this.resolver.resolve(this.schema, this.schema?.$id);
1627
+ // Validate the schema itself and emit any structural errors
1628
+ const schemaErrs = this.validation.validateSchema(this.resolvedSchema);
1629
+ this.schemaErrorList = schemaErrs;
1630
+ this.schemaErrors.emit(schemaErrs);
1544
1631
  this.schemaReady.emit(this.resolvedSchema);
1632
+ // If the schema has structural errors, stop here — don't try to build the form
1633
+ if (schemaErrs.length > 0) {
1634
+ this.form = undefined;
1635
+ this.loading = false;
1636
+ return;
1637
+ }
1545
1638
  const initialData = this.value !== undefined ? this.value : this.data;
1546
1639
  this.rootControl = this.schemaService.buildControl(this.resolvedSchema, initialData);
1547
1640
  this.form = this.rootControl instanceof FormGroup ? this.rootControl : new FormGroup({ value: this.rootControl });
@@ -1588,29 +1681,60 @@ class JsonSchemaFormComponent {
1588
1681
  });
1589
1682
  }
1590
1683
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaFormComponent, deps: [{ token: JsonSchemaFormService }, { token: JsonSchemaResolverService }, { token: JsonSchemaValidationService }], target: i0.ɵɵFactoryTarget.Component });
1591
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: JsonSchemaFormComponent, isStandalone: true, selector: "jsm-json-schema-form", inputs: { schema: "schema", value: "value", data: "data" }, outputs: { formReady: "formReady", valueChange: "valueChange", schemaReady: "schemaReady" }, usesOnChanges: true, ngImport: i0, template: `
1592
- <form class="w-full max-w-4xl mx-auto p-4 md:p-6 space-y-6">
1593
- <div class="space-y-1">
1594
- <h2 class="text-xl font-semibold text-slate-900">{{ resolvedSchema?.title || 'Generated Form' }}</h2>
1595
- <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500">
1596
- {{ resolvedSchema?.description }}
1597
- </p>
1598
- </div>
1599
-
1600
- <div *ngIf="loading" class="text-sm text-slate-500">Resolving schema...</div>
1601
-
1602
- <div *ngIf="form && resolvedSchema" class="space-y-6" [formGroup]="form">
1603
- <jsm-schema-node
1604
- [schema]="resolvedSchema"
1605
- [control]="rootControl"
1606
- [path]="''"
1607
- [errorsMap]="errorsMap"
1608
- label="Root"
1609
- (controlReplaced)="onRootReplaced($event)"
1610
- ></jsm-schema-node>
1611
- </div>
1612
- </form>
1613
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: JsonSchemaNodeComponent, selector: "jsm-schema-node", inputs: ["schema", "control", "label", "required", "path", "errorsMap", "parent", "controlKey"], outputs: ["controlReplaced"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1684
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: JsonSchemaFormComponent, isStandalone: true, selector: "jsm-json-schema-form", inputs: { schema: "schema", value: "value", data: "data", allowAdditionalProperties: "allowAdditionalProperties" }, outputs: { formReady: "formReady", valueChange: "valueChange", schemaReady: "schemaReady", schemaErrors: "schemaErrors" }, usesOnChanges: true, ngImport: i0, template: `
1685
+ <form class="w-full space-y-5">
1686
+ <div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
1687
+ <h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || 'Form' }}</h2>
1688
+ <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
1689
+ </div>
1690
+
1691
+ <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
1692
+ <svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none">
1693
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
1694
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
1695
+ </svg>
1696
+ Resolving schema&hellip;
1697
+ </div>
1698
+
1699
+ <!-- schema structural errors panel -->
1700
+ <div *ngIf="schemaErrorList.length > 0 && !loading"
1701
+ class="rounded-xl border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4 space-y-3"
1702
+ role="alert">
1703
+ <div class="flex items-center gap-2">
1704
+ <svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none">
1705
+ <path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
1706
+ <path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1707
+ </svg>
1708
+ <span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
1709
+ Schema errors ({{ schemaErrorList.length }})
1710
+ </span>
1711
+ </div>
1712
+ <p class="text-xs text-amber-700 dark:text-amber-400">
1713
+ The schema has structural issues. Fix them to get a valid form.
1714
+ </p>
1715
+ <div class="space-y-2">
1716
+ <div *ngFor="let err of schemaErrorList"
1717
+ class="rounded-lg border border-amber-200 dark:border-amber-800 bg-white dark:bg-slate-900 px-3 py-2 space-y-0.5">
1718
+ <p class="font-mono text-[11px] font-semibold text-amber-700 dark:text-amber-400">{{ err.path }}</p>
1719
+ <p class="text-xs text-slate-700 dark:text-slate-300">{{ err.message }}</p>
1720
+ <p class="text-[10px] uppercase tracking-wide text-slate-400 dark:text-slate-500">keyword: {{ err.keyword }}</p>
1721
+ </div>
1722
+ </div>
1723
+ </div>
1724
+
1725
+ <div *ngIf="form && resolvedSchema && !loading" class="space-y-5" [formGroup]="form">
1726
+ <jsm-schema-node
1727
+ [schema]="resolvedSchema"
1728
+ [control]="rootControl"
1729
+ [path]="''"
1730
+ [errorsMap]="errorsMap"
1731
+ [allowAdditionalProperties]="allowAdditionalProperties"
1732
+ label="Root"
1733
+ (controlReplaced)="onRootReplaced($event)"
1734
+ ></jsm-schema-node>
1735
+ </div>
1736
+ </form>
1737
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: JsonSchemaNodeComponent, selector: "jsm-schema-node", inputs: ["schema", "control", "label", "required", "path", "errorsMap", "parent", "controlKey", "allowAdditionalProperties"], outputs: ["controlReplaced"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1614
1738
  }
1615
1739
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaFormComponent, decorators: [{
1616
1740
  type: Component,
@@ -1618,28 +1742,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1618
1742
  selector: 'jsm-json-schema-form',
1619
1743
  standalone: true,
1620
1744
  imports: [CommonModule, ReactiveFormsModule, JsonSchemaNodeComponent],
1621
- template: `
1622
- <form class="w-full max-w-4xl mx-auto p-4 md:p-6 space-y-6">
1623
- <div class="space-y-1">
1624
- <h2 class="text-xl font-semibold text-slate-900">{{ resolvedSchema?.title || 'Generated Form' }}</h2>
1625
- <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500">
1626
- {{ resolvedSchema?.description }}
1627
- </p>
1628
- </div>
1629
-
1630
- <div *ngIf="loading" class="text-sm text-slate-500">Resolving schema...</div>
1631
-
1632
- <div *ngIf="form && resolvedSchema" class="space-y-6" [formGroup]="form">
1633
- <jsm-schema-node
1634
- [schema]="resolvedSchema"
1635
- [control]="rootControl"
1636
- [path]="''"
1637
- [errorsMap]="errorsMap"
1638
- label="Root"
1639
- (controlReplaced)="onRootReplaced($event)"
1640
- ></jsm-schema-node>
1641
- </div>
1642
- </form>
1745
+ template: `
1746
+ <form class="w-full space-y-5">
1747
+ <div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
1748
+ <h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || 'Form' }}</h2>
1749
+ <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
1750
+ </div>
1751
+
1752
+ <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
1753
+ <svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none">
1754
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
1755
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
1756
+ </svg>
1757
+ Resolving schema&hellip;
1758
+ </div>
1759
+
1760
+ <!-- schema structural errors panel -->
1761
+ <div *ngIf="schemaErrorList.length > 0 && !loading"
1762
+ class="rounded-xl border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4 space-y-3"
1763
+ role="alert">
1764
+ <div class="flex items-center gap-2">
1765
+ <svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none">
1766
+ <path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
1767
+ <path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1768
+ </svg>
1769
+ <span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
1770
+ Schema errors ({{ schemaErrorList.length }})
1771
+ </span>
1772
+ </div>
1773
+ <p class="text-xs text-amber-700 dark:text-amber-400">
1774
+ The schema has structural issues. Fix them to get a valid form.
1775
+ </p>
1776
+ <div class="space-y-2">
1777
+ <div *ngFor="let err of schemaErrorList"
1778
+ class="rounded-lg border border-amber-200 dark:border-amber-800 bg-white dark:bg-slate-900 px-3 py-2 space-y-0.5">
1779
+ <p class="font-mono text-[11px] font-semibold text-amber-700 dark:text-amber-400">{{ err.path }}</p>
1780
+ <p class="text-xs text-slate-700 dark:text-slate-300">{{ err.message }}</p>
1781
+ <p class="text-[10px] uppercase tracking-wide text-slate-400 dark:text-slate-500">keyword: {{ err.keyword }}</p>
1782
+ </div>
1783
+ </div>
1784
+ </div>
1785
+
1786
+ <div *ngIf="form && resolvedSchema && !loading" class="space-y-5" [formGroup]="form">
1787
+ <jsm-schema-node
1788
+ [schema]="resolvedSchema"
1789
+ [control]="rootControl"
1790
+ [path]="''"
1791
+ [errorsMap]="errorsMap"
1792
+ [allowAdditionalProperties]="allowAdditionalProperties"
1793
+ label="Root"
1794
+ (controlReplaced)="onRootReplaced($event)"
1795
+ ></jsm-schema-node>
1796
+ </div>
1797
+ </form>
1643
1798
  `,
1644
1799
  changeDetection: ChangeDetectionStrategy.OnPush,
1645
1800
  }]
@@ -1650,12 +1805,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1650
1805
  type: Input
1651
1806
  }], data: [{
1652
1807
  type: Input
1808
+ }], allowAdditionalProperties: [{
1809
+ type: Input
1653
1810
  }], formReady: [{
1654
1811
  type: Output
1655
1812
  }], valueChange: [{
1656
1813
  type: Output
1657
1814
  }], schemaReady: [{
1658
1815
  type: Output
1816
+ }], schemaErrors: [{
1817
+ type: Output
1659
1818
  }] } });
1660
1819
 
1661
1820
  class JsonSchemaFormModule {