@elle96/ng-json-schema-manager 0.0.15 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
2
+ import { Injectable, EventEmitter, Output, Input, ChangeDetectionStrategy, ViewEncapsulation, Component, NgModule } from '@angular/core';
3
3
  import * as i4 from '@angular/forms';
4
4
  import { FormGroup, FormArray, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
5
5
  import Ajv from 'ajv';
@@ -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,121 @@ 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;
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); }
674
+ get simpleKeys() {
675
+ return this.objectKeys.filter(k => this.isSimpleChild(k));
631
676
  }
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);
677
+ get complexKeys() {
678
+ return this.objectKeys.filter(k => !this.isSimpleChild(k));
643
679
  }
644
680
  get canAddProperty() {
681
+ if (!this.allowAdditionalProperties)
682
+ return false;
645
683
  return this.effectiveSchema.additionalProperties !== false || !!this.effectiveSchema.patternProperties;
646
684
  }
647
- get errorsForPath() {
648
- return this.errorsMap.get(this.path) ?? [];
649
- }
685
+ get errorsForPath() { return this.errorsMap.get(this.path) ?? []; }
650
686
  get metaBadges() {
651
687
  const badges = [];
652
- if (this.effectiveSchema.deprecated) {
688
+ if (this.effectiveSchema.deprecated)
653
689
  badges.push('Deprecated');
654
- }
655
- if (this.effectiveSchema.readOnly) {
690
+ if (this.effectiveSchema.readOnly)
656
691
  badges.push('Read-only');
657
- }
658
- if (this.effectiveSchema.writeOnly) {
692
+ if (this.effectiveSchema.writeOnly)
659
693
  badges.push('Write-only');
660
- }
661
- if (this.effectiveSchema.nullable) {
694
+ if (this.effectiveSchema.nullable)
662
695
  badges.push('Nullable');
663
- }
664
- if (this.effectiveSchema.not) {
696
+ if (this.effectiveSchema.not)
665
697
  badges.push('Not constraint');
666
- }
667
698
  return badges;
668
699
  }
669
700
  get activeVariant() {
670
- if (this.schema.oneOf?.length) {
701
+ if (this.schema.oneOf?.length)
671
702
  return this.schema.oneOf[this.selectedOneOf] ?? null;
672
- }
673
703
  if (this.schema.anyOf?.length) {
674
- const selections = Array.from(this.selectedAnyOf).map((index) => this.schema.anyOf[index]);
704
+ const selections = Array.from(this.selectedAnyOf).map((i) => this.schema.anyOf[i]);
675
705
  return selections.length ? this.mergeSchemas(selections) : this.schema.anyOf[0] ?? null;
676
706
  }
677
- if (this.schema.allOf?.length) {
707
+ if (this.schema.allOf?.length)
678
708
  return this.mergeSchemas([this.schema, ...this.schema.allOf]);
679
- }
680
709
  return this.effectiveSchema;
681
710
  }
682
711
  get mergedAllOf() {
683
712
  return this.schema.allOf?.length ? [this.schema, ...this.schema.allOf] : [this.schema];
684
713
  }
685
- isRequired(key) {
686
- return this.effectiveSchema.required?.includes(key) ?? false;
687
- }
714
+ isRequired(key) { return this.effectiveSchema.required?.includes(key) ?? false; }
688
715
  schemaForKey(key) {
689
716
  return (this.effectiveSchema.properties?.[key] ??
690
717
  this.schemaService.resolveDynamicSchema(this.effectiveSchema, key) ??
691
718
  {});
692
719
  }
693
- isDynamicKey(key) {
694
- return !this.effectiveSchema.properties?.[key];
695
- }
720
+ isDynamicKey(key) { return !this.effectiveSchema.properties?.[key]; }
696
721
  pathForChild(key) {
697
- if (this.path === '') {
698
- return `/${key}`;
699
- }
700
- return `${this.path}/${key}`;
722
+ return this.path === '' ? `/${key}` : `${this.path}/${key}`;
701
723
  }
702
724
  addDynamicProperty() {
703
725
  this.propertyError = '';
@@ -726,135 +748,107 @@ class JsonSchemaNodeComponent {
726
748
  this.controlAsGroup.addControl(key, control);
727
749
  this.newPropertyKey = '';
728
750
  }
729
- removeDynamicKey(key) {
730
- this.controlAsGroup.removeControl(key);
731
- }
751
+ removeDynamicKey(key) { this.controlAsGroup.removeControl(key); }
732
752
  schemaForIndex(index) {
733
753
  const prefixItems = this.effectiveSchema.prefixItems ?? [];
734
- if (prefixItems[index]) {
754
+ if (prefixItems[index])
735
755
  return prefixItems[index];
736
- }
737
- if (this.effectiveSchema.items && this.effectiveSchema.items !== true) {
756
+ if (this.effectiveSchema.items && this.effectiveSchema.items !== true)
738
757
  return this.effectiveSchema.items;
739
- }
740
- if (this.effectiveSchema.additionalItems && this.effectiveSchema.additionalItems !== true) {
758
+ if (this.effectiveSchema.additionalItems && this.effectiveSchema.additionalItems !== true)
741
759
  return this.effectiveSchema.additionalItems;
742
- }
743
- if (this.effectiveSchema.unevaluatedItems && this.effectiveSchema.unevaluatedItems !== true) {
760
+ if (this.effectiveSchema.unevaluatedItems && this.effectiveSchema.unevaluatedItems !== true)
744
761
  return this.effectiveSchema.unevaluatedItems;
745
- }
746
762
  return {};
747
763
  }
748
764
  get canAddArrayItem() {
749
- if (this.effectiveSchema.items === false ||
765
+ return !(this.effectiveSchema.items === false ||
750
766
  this.effectiveSchema.additionalItems === false ||
751
- this.effectiveSchema.unevaluatedItems === false) {
752
- return false;
753
- }
754
- return true;
767
+ this.effectiveSchema.unevaluatedItems === false);
755
768
  }
756
769
  addArrayItem() {
757
- if (!this.canAddArrayItem) {
770
+ if (!this.canAddArrayItem)
758
771
  return;
759
- }
760
772
  const formArray = this.controlAsArray;
761
773
  const index = formArray.length;
762
- const schemaForItem = this.schemaForIndex(index);
763
- formArray.push(this.schemaService.buildControl(schemaForItem, undefined, false));
774
+ formArray.push(this.schemaService.buildControl(this.schemaForIndex(index), undefined, false));
764
775
  formArray.markAsDirty();
765
776
  }
766
777
  removeArrayItem(index) {
767
- const formArray = this.controlAsArray;
768
- formArray.removeAt(index);
769
- formArray.markAsDirty();
778
+ this.controlAsArray.removeAt(index);
779
+ this.controlAsArray.markAsDirty();
770
780
  }
771
781
  onOneOfChange() {
772
- if (!this.activeVariant) {
782
+ if (!this.activeVariant)
773
783
  return;
774
- }
775
784
  this.replaceControl(this.schemaService.buildControl(this.activeVariant, this.control.value, this.required));
776
785
  }
777
786
  toggleAnyOf(index) {
778
- if (this.selectedAnyOf.has(index)) {
787
+ if (this.selectedAnyOf.has(index))
779
788
  this.selectedAnyOf.delete(index);
780
- }
781
- else {
789
+ else
782
790
  this.selectedAnyOf.add(index);
783
- }
784
791
  const variant = this.activeVariant;
785
- if (variant) {
792
+ if (variant)
786
793
  this.replaceControl(this.schemaService.buildControl(variant, this.control.value, this.required));
787
- }
788
794
  }
789
795
  replaceControl(control) {
790
796
  if (this.parent) {
791
- if (this.parent instanceof FormGroup && typeof this.controlKey === 'string') {
797
+ if (this.parent instanceof FormGroup && typeof this.controlKey === 'string')
792
798
  this.parent.setControl(this.controlKey, control);
793
- }
794
- if (this.parent instanceof FormArray && typeof this.controlKey === 'number') {
799
+ if (this.parent instanceof FormArray && typeof this.controlKey === 'number')
795
800
  this.parent.setControl(this.controlKey, control);
796
- }
797
801
  this.control = control;
798
802
  return;
799
803
  }
800
804
  this.control = control;
801
805
  this.controlReplaced.emit(control);
802
806
  }
803
- childControl(key) {
804
- return this.controlAsGroup.get(key);
805
- }
807
+ childControl(key) { return this.controlAsGroup.get(key); }
806
808
  get effectiveSchema() {
807
809
  let schema = this.schema;
808
- if (schema.allOf?.length) {
810
+ if (schema.allOf?.length)
809
811
  schema = this.mergeSchemas([schema, ...schema.allOf]);
810
- }
811
812
  schema = this.applyConditional(schema);
812
813
  schema = this.applyDependencies(schema);
813
814
  return schema;
814
815
  }
815
816
  applyConditional(schema) {
816
- if (!schema.if) {
817
+ if (!schema.if)
817
818
  return schema;
818
- }
819
819
  const errors = this.validation.validate(schema.if, this.control.value);
820
820
  const branch = errors.length === 0 ? schema.then : schema.else;
821
- if (!branch) {
821
+ if (!branch)
822
822
  return schema;
823
- }
824
823
  return this.mergeSchemas([schema, branch]);
825
824
  }
826
825
  applyDependencies(schema) {
827
826
  const value = this.control.value;
828
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
827
+ if (!value || typeof value !== 'object' || Array.isArray(value))
829
828
  return schema;
830
- }
831
829
  let merged = { ...schema };
832
830
  const valueKeys = Object.keys(value);
833
831
  if (schema.dependentRequired) {
834
832
  for (const key of valueKeys) {
835
833
  const required = schema.dependentRequired[key];
836
- if (required?.length) {
834
+ if (required?.length)
837
835
  merged = this.mergeSchemas([merged, { required }]);
838
- }
839
836
  }
840
837
  }
841
838
  if (schema.dependentSchemas) {
842
839
  for (const key of valueKeys) {
843
840
  const depSchema = schema.dependentSchemas[key];
844
- if (depSchema) {
841
+ if (depSchema)
845
842
  merged = this.mergeSchemas([merged, depSchema]);
846
- }
847
843
  }
848
844
  }
849
845
  if (schema.dependencies) {
850
846
  for (const key of valueKeys) {
851
847
  const dep = schema.dependencies[key];
852
- if (Array.isArray(dep)) {
848
+ if (Array.isArray(dep))
853
849
  merged = this.mergeSchemas([merged, { required: dep }]);
854
- }
855
- else if (dep) {
850
+ else if (dep)
856
851
  merged = this.mergeSchemas([merged, dep]);
857
- }
858
852
  }
859
853
  }
860
854
  return merged;
@@ -877,306 +871,377 @@ class JsonSchemaNodeComponent {
877
871
  }, {});
878
872
  }
879
873
  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 });
874
+ 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: `
875
+ <ng-container [ngSwitch]="nodeKind">
876
+
877
+ <!-- ═══════════════════════════════════════════════════════════
878
+ oneOf — tab bar: ogni opzione è una tab, nessuna icona
879
+ ═══════════════════════════════════════════════════════════ -->
880
+ <div *ngSwitchCase="'oneOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
881
+ <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">
882
+ <div>
883
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
884
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
885
+ </div>
886
+ <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>
887
+ </div>
888
+ <div class="flex overflow-x-auto border-b border-slate-200 dark:border-slate-700">
889
+ <button
890
+ *ngFor="let option of schema.oneOf; let i = index"
891
+ type="button"
892
+ class="px-4 py-2.5 text-xs font-medium whitespace-nowrap border-b-2 transition-colors"
893
+ [class.border-indigo-500]="selectedOneOf === i"
894
+ [class.text-indigo-600]="selectedOneOf === i"
895
+ [class.dark:text-indigo-400]="selectedOneOf === i"
896
+ [class.font-semibold]="selectedOneOf === i"
897
+ [class.border-transparent]="selectedOneOf !== i"
898
+ [class.text-slate-500]="selectedOneOf !== i"
899
+ [class.dark:text-slate-400]="selectedOneOf !== i"
900
+ (click)="selectedOneOf = i; onOneOfChange()"
901
+ >{{ option.title || ('Option ' + (i + 1)) }}</button>
902
+ </div>
903
+ <div class="p-4" *ngIf="activeVariant">
904
+ <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
905
+ [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
906
+ [allowAdditionalProperties]="allowAdditionalProperties"
907
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
908
+ </div>
909
+ </div>
910
+
911
+ <!-- ═══════════════════════════════════════════════════════════
912
+ anyOf — pill toggle, nessuna icona
913
+ ═══════════════════════════════════════════════════════════ -->
914
+ <div *ngSwitchCase="'anyOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
915
+ <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">
916
+ <div>
917
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
918
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
919
+ </div>
920
+ <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>
921
+ </div>
922
+ <div class="flex flex-wrap gap-2 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
923
+ <label
924
+ *ngFor="let option of schema.anyOf; let i = index"
925
+ 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"
926
+ [class.border-indigo-400]="selectedAnyOf.has(i)"
927
+ [class.bg-indigo-50]="selectedAnyOf.has(i)"
928
+ [class.dark:bg-indigo-900/30]="selectedAnyOf.has(i)"
929
+ [class.text-indigo-700]="selectedAnyOf.has(i)"
930
+ [class.dark:text-indigo-400]="selectedAnyOf.has(i)"
931
+ [class.border-slate-200]="!selectedAnyOf.has(i)"
932
+ [class.dark:border-slate-600]="!selectedAnyOf.has(i)"
933
+ [class.text-slate-600]="!selectedAnyOf.has(i)"
934
+ [class.dark:text-slate-400]="!selectedAnyOf.has(i)"
935
+ >
936
+ <input type="checkbox" class="h-3.5 w-3.5 accent-indigo-600" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
937
+ {{ option.title || ('Option ' + (i + 1)) }}
938
+ </label>
939
+ </div>
940
+ <div class="p-4" *ngIf="activeVariant">
941
+ <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
942
+ [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
943
+ [allowAdditionalProperties]="allowAdditionalProperties"
944
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
945
+ </div>
946
+ </div>
947
+
948
+ <!-- ═══════════════════════════════════════════════════════════
949
+ allOf — sezioni in colonna con bordo sinistro
950
+ ═══════════════════════════════════════════════════════════ -->
951
+ <div *ngSwitchCase="'allOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
952
+ <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">
953
+ <div>
954
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
955
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
956
+ </div>
957
+ <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>
958
+ </div>
959
+ <div class="p-4 space-y-4">
960
+ <div *ngFor="let subSchema of mergedAllOf; let i = index" class="border-l-2 border-indigo-200 dark:border-indigo-700 pl-4">
961
+ <jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
962
+ [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
963
+ [allowAdditionalProperties]="allowAdditionalProperties"
964
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
965
+ </div>
966
+ </div>
967
+ </div>
968
+
969
+ <!-- ═══════════════════════════════════════════════════════════
970
+ object — fieldset con griglia SOLO per campi primitivi.
971
+ Logica layout:
972
+ - I figli vengono classificati come "semplici" (primitive/enum/bool)
973
+ o "complessi" (object/array/oneOf/anyOf/allOf).
974
+ - I semplici vanno in griglia 2 colonne (sm:grid-cols-2).
975
+ - I complessi vanno sempre in colonna piena (col-span-2).
976
+ ═══════════════════════════════════════════════════════════ -->
977
+ <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">
978
+ <legend class="px-2 text-sm font-bold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</legend>
979
+
980
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mb-3 mt-1">{{ schema.description }}</p>
981
+
982
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5 mb-3">
983
+ <span *ngFor="let badge of metaBadges"
984
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
985
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
986
+ </div>
987
+
988
+ <!-- campi semplici (string/number/bool/enum) → griglia 2 colonne -->
989
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4" [formGroup]="controlAsGroup">
990
+ <ng-container *ngFor="let key of simpleKeys">
991
+ <div class="flex flex-col gap-1">
992
+ <div *ngIf="isDynamicKey(key)"
993
+ 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">
994
+ <span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
995
+ <button type="button"
996
+ 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"
997
+ (click)="removeDynamicKey(key)">Remove</button>
998
+ </div>
999
+ <jsm-schema-node
1000
+ [schema]="schemaForKey(key)"
1001
+ [control]="childControl(key)"
1002
+ [parent]="controlAsGroup"
1003
+ [controlKey]="key"
1004
+ [path]="pathForChild(key)"
1005
+ [errorsMap]="errorsMap"
1006
+ [label]="key"
1007
+ [required]="isRequired(key)"
1008
+ [allowAdditionalProperties]="allowAdditionalProperties"
1009
+ (controlReplaced)="controlReplaced.emit($event)">
1010
+ </jsm-schema-node>
1011
+ </div>
1012
+ </ng-container>
1013
+ </div>
1014
+
1015
+ <!-- campi complessi (object/array/combinator) → colonna piena -->
1016
+ <div class="space-y-4 mt-4" *ngIf="complexKeys.length > 0" [formGroup]="controlAsGroup">
1017
+ <ng-container *ngFor="let key of complexKeys">
1018
+ <div class="flex flex-col gap-1">
1019
+ <div *ngIf="isDynamicKey(key)"
1020
+ 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">
1021
+ <span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
1022
+ <button type="button"
1023
+ 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"
1024
+ (click)="removeDynamicKey(key)">Remove</button>
1025
+ </div>
1026
+ <jsm-schema-node
1027
+ [schema]="schemaForKey(key)"
1028
+ [control]="childControl(key)"
1029
+ [parent]="controlAsGroup"
1030
+ [controlKey]="key"
1031
+ [path]="pathForChild(key)"
1032
+ [errorsMap]="errorsMap"
1033
+ [label]="key"
1034
+ [required]="isRequired(key)"
1035
+ [allowAdditionalProperties]="allowAdditionalProperties"
1036
+ (controlReplaced)="controlReplaced.emit($event)">
1037
+ </jsm-schema-node>
1038
+ </div>
1039
+ </ng-container>
1040
+ </div>
1041
+
1042
+ <!-- add dynamic property -->
1043
+ <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">
1044
+ <p class="text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500">Add property</p>
1045
+ <div class="flex gap-2">
1046
+ <input
1047
+ 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"
1048
+ [(ngModel)]="newPropertyKey" placeholder="propertyName" />
1049
+ <button type="button"
1050
+ 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"
1051
+ (click)="addDynamicProperty()">Add</button>
1052
+ </div>
1053
+ <p *ngIf="propertyError" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1054
+ <!-- icona errore: unico caso in cui ha senso, segnala un problema -->
1055
+ <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>
1056
+ {{ propertyError }}
1057
+ </p>
1058
+ </div>
1059
+
1060
+ <div *ngIf="errorsForPath.length" class="mt-3 space-y-1">
1061
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1062
+ <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>
1063
+ {{ error }}
1064
+ </p>
1065
+ </div>
1066
+ </fieldset>
1067
+
1068
+ <!-- ═══════════════════════════════════════════════════════════
1069
+ array — sempre in colonna piena, items in colonna
1070
+ ═══════════════════════════════════════════════════════════ -->
1071
+ <div *ngSwitchCase="'array'" class="space-y-3">
1072
+ <div class="flex items-start justify-between gap-2">
1073
+ <div>
1074
+ <p class="text-sm font-semibold text-slate-800">
1075
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1076
+ </p>
1077
+ <p *ngIf="schema.description" class="text-xs text-slate-500 mt-0.5">{{ schema.description }}</p>
1078
+ </div>
1079
+ <!-- icona + nel bottone: indica l'azione "aggiungi", non decorativa -->
1080
+ <button type="button"
1081
+ 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"
1082
+ (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1083
+ <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>
1084
+ Add item
1085
+ </button>
1086
+ </div>
1087
+
1088
+ <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">
1089
+ Must contain matching item(s)
1090
+ <span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
1091
+ <span *ngIf="effectiveSchema.maxContains"> · max {{ effectiveSchema.maxContains }}</span>
1092
+ </div>
1093
+
1094
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
1095
+ <span *ngFor="let badge of metaBadges"
1096
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1097
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1098
+ </div>
1099
+
1100
+ <div class="space-y-2" *ngIf="arrayControls.length > 0; else emptyArray">
1101
+ <div *ngFor="let item of arrayControls; let i = index"
1102
+ 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">
1103
+ <div class="flex items-center justify-between mb-3">
1104
+ <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>
1105
+ <button type="button"
1106
+ 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"
1107
+ (click)="removeArrayItem(i)">Remove</button>
1108
+ </div>
1109
+ <jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
1110
+ [controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
1111
+ [allowAdditionalProperties]="allowAdditionalProperties"
1112
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1113
+ </div>
1114
+ </div>
1115
+
1116
+ <ng-template #emptyArray>
1117
+ <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">
1118
+ No items yet — click "Add item" to start.
1119
+ </div>
1120
+ </ng-template>
1121
+
1122
+ <div *ngIf="errorsForPath.length" class="space-y-1">
1123
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1124
+ <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>
1125
+ {{ error }}
1126
+ </p>
1127
+ </div>
1128
+ </div>
1129
+
1130
+ <!-- ═══════════════════════════════════════════════════════════
1131
+ primitive — sempre in colonna, nessuna icona decorativa.
1132
+ Icona errore solo nei messaggi di validazione.
1133
+ Icona chevron nel select: funzionale (indica dropdown).
1134
+ ═══════════════════════════════════════════════════════════ -->
1135
+ <div *ngSwitchDefault class="space-y-1.5">
1136
+ <ng-container *ngIf="inputKind !== 'checkbox'">
1137
+ <label class="block text-[13px] font-semibold text-slate-700 dark:text-slate-300">
1138
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1139
+ </label>
1140
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 -mt-0.5">{{ schema.description }}</p>
1141
+ </ng-container>
1142
+
1143
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
1144
+ <span *ngFor="let badge of metaBadges"
1145
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1146
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1147
+ </div>
1148
+
1149
+ <ng-container [ngSwitch]="inputKind">
1150
+
1151
+ <input *ngSwitchCase="'text'"
1152
+ 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"
1153
+ [class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
1154
+ [class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
1155
+ [class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
1156
+ [class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
1157
+ [class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
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
+ [class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
1163
+ [class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
1164
+ [attr.type]="inputType"
1165
+ [attr.placeholder]="schema.title || label"
1166
+ [formControl]="controlAsFormControl"
1167
+ />
1168
+
1169
+ <textarea *ngSwitchCase="'textarea'" rows="4"
1170
+ 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"
1171
+ [class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
1172
+ [class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
1173
+ [class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
1174
+ [class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
1175
+ [class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
1176
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1177
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1178
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1179
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1180
+ [class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
1181
+ [class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
1182
+ [attr.placeholder]="schema.title || label"
1183
+ [formControl]="controlAsFormControl"
1184
+ ></textarea>
1185
+
1186
+ <!-- select: il chevron SVG è funzionale, indica che è un dropdown -->
1187
+ <div *ngSwitchCase="'select'" class="relative">
1188
+ <select
1189
+ 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"
1190
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1191
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1192
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1193
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1194
+ [formControl]="controlAsFormControl">
1195
+ <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1196
+ </select>
1197
+ <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"
1198
+ viewBox="0 0 16 16" fill="none">
1199
+ <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1200
+ </svg>
1201
+ </div>
1202
+
1203
+ <!-- checkbox come toggle switch -->
1204
+ <label *ngSwitchCase="'checkbox'" class="inline-flex cursor-pointer items-center gap-2.5">
1205
+ <span class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors"
1206
+ [class.bg-indigo-600]="controlAsFormControl.value"
1207
+ [class.bg-slate-300]="!controlAsFormControl.value"
1208
+ [class.dark:bg-slate-600]="!controlAsFormControl.value">
1209
+ <input type="checkbox" class="sr-only" [formControl]="controlAsFormControl" />
1210
+ <span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform"
1211
+ [class.translate-x-4]="controlAsFormControl.value"
1212
+ [class.translate-x-0.5]="!controlAsFormControl.value"></span>
1213
+ </span>
1214
+ <span class="text-sm font-medium text-slate-700 dark:text-slate-300">
1215
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1216
+ </span>
1217
+ </label>
1218
+
1219
+ </ng-container>
1220
+
1221
+ <p *ngIf="inputKind === 'checkbox' && schema.description" class="text-xs text-slate-500 dark:text-slate-400">{{ schema.description }}</p>
1222
+
1223
+ <p *ngIf="effectiveSchema.examples?.length" class="text-[11px] text-slate-400 dark:text-slate-500">
1224
+ e.g. {{ effectiveSchema.examples?.[0] }}
1225
+ </p>
1226
+
1227
+ <p *ngIf="effectiveSchema.contentMediaType" class="text-[11px] text-slate-400 dark:text-slate-500">
1228
+ {{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
1229
+ </p>
1230
+
1231
+ <!-- icona errore: funzionale, segnala un problema di validazione -->
1232
+ <div *ngIf="errorsForPath.length && controlAsFormControl.dirty" class="space-y-1 pt-0.5">
1233
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1234
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
1235
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
1236
+ <path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1237
+ </svg>
1238
+ {{ error }}
1239
+ </p>
1240
+ </div>
1241
+ </div>
1242
+
1243
+ </ng-container>
1244
+ `, 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, encapsulation: i0.ViewEncapsulation.None });
1180
1245
  }
1181
1246
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaNodeComponent, decorators: [{
1182
1247
  type: Component,
@@ -1184,305 +1249,377 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1184
1249
  selector: 'jsm-schema-node',
1185
1250
  standalone: true,
1186
1251
  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>
1252
+ encapsulation: ViewEncapsulation.None,
1253
+ template: `
1254
+ <ng-container [ngSwitch]="nodeKind">
1255
+
1256
+ <!-- ═══════════════════════════════════════════════════════════
1257
+ oneOf — tab bar: ogni opzione è una tab, nessuna icona
1258
+ ═══════════════════════════════════════════════════════════ -->
1259
+ <div *ngSwitchCase="'oneOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
1260
+ <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">
1261
+ <div>
1262
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
1263
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
1264
+ </div>
1265
+ <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>
1266
+ </div>
1267
+ <div class="flex overflow-x-auto border-b border-slate-200 dark:border-slate-700">
1268
+ <button
1269
+ *ngFor="let option of schema.oneOf; let i = index"
1270
+ type="button"
1271
+ class="px-4 py-2.5 text-xs font-medium whitespace-nowrap border-b-2 transition-colors"
1272
+ [class.border-indigo-500]="selectedOneOf === i"
1273
+ [class.text-indigo-600]="selectedOneOf === i"
1274
+ [class.dark:text-indigo-400]="selectedOneOf === i"
1275
+ [class.font-semibold]="selectedOneOf === i"
1276
+ [class.border-transparent]="selectedOneOf !== i"
1277
+ [class.text-slate-500]="selectedOneOf !== i"
1278
+ [class.dark:text-slate-400]="selectedOneOf !== i"
1279
+ (click)="selectedOneOf = i; onOneOfChange()"
1280
+ >{{ option.title || ('Option ' + (i + 1)) }}</button>
1281
+ </div>
1282
+ <div class="p-4" *ngIf="activeVariant">
1283
+ <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
1284
+ [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
1285
+ [allowAdditionalProperties]="allowAdditionalProperties"
1286
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1287
+ </div>
1288
+ </div>
1289
+
1290
+ <!-- ═══════════════════════════════════════════════════════════
1291
+ anyOf — pill toggle, nessuna icona
1292
+ ═══════════════════════════════════════════════════════════ -->
1293
+ <div *ngSwitchCase="'anyOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
1294
+ <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">
1295
+ <div>
1296
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
1297
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
1298
+ </div>
1299
+ <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>
1300
+ </div>
1301
+ <div class="flex flex-wrap gap-2 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
1302
+ <label
1303
+ *ngFor="let option of schema.anyOf; let i = index"
1304
+ 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"
1305
+ [class.border-indigo-400]="selectedAnyOf.has(i)"
1306
+ [class.bg-indigo-50]="selectedAnyOf.has(i)"
1307
+ [class.dark:bg-indigo-900/30]="selectedAnyOf.has(i)"
1308
+ [class.text-indigo-700]="selectedAnyOf.has(i)"
1309
+ [class.dark:text-indigo-400]="selectedAnyOf.has(i)"
1310
+ [class.border-slate-200]="!selectedAnyOf.has(i)"
1311
+ [class.dark:border-slate-600]="!selectedAnyOf.has(i)"
1312
+ [class.text-slate-600]="!selectedAnyOf.has(i)"
1313
+ [class.dark:text-slate-400]="!selectedAnyOf.has(i)"
1314
+ >
1315
+ <input type="checkbox" class="h-3.5 w-3.5 accent-indigo-600" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
1316
+ {{ option.title || ('Option ' + (i + 1)) }}
1317
+ </label>
1318
+ </div>
1319
+ <div class="p-4" *ngIf="activeVariant">
1320
+ <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
1321
+ [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
1322
+ [allowAdditionalProperties]="allowAdditionalProperties"
1323
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1324
+ </div>
1325
+ </div>
1326
+
1327
+ <!-- ═══════════════════════════════════════════════════════════
1328
+ allOf — sezioni in colonna con bordo sinistro
1329
+ ═══════════════════════════════════════════════════════════ -->
1330
+ <div *ngSwitchCase="'allOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
1331
+ <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">
1332
+ <div>
1333
+ <p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
1334
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
1335
+ </div>
1336
+ <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>
1337
+ </div>
1338
+ <div class="p-4 space-y-4">
1339
+ <div *ngFor="let subSchema of mergedAllOf; let i = index" class="border-l-2 border-indigo-200 dark:border-indigo-700 pl-4">
1340
+ <jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
1341
+ [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
1342
+ [allowAdditionalProperties]="allowAdditionalProperties"
1343
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1344
+ </div>
1345
+ </div>
1346
+ </div>
1347
+
1348
+ <!-- ═══════════════════════════════════════════════════════════
1349
+ object — fieldset con griglia SOLO per campi primitivi.
1350
+ Logica layout:
1351
+ - I figli vengono classificati come "semplici" (primitive/enum/bool)
1352
+ o "complessi" (object/array/oneOf/anyOf/allOf).
1353
+ - I semplici vanno in griglia 2 colonne (sm:grid-cols-2).
1354
+ - I complessi vanno sempre in colonna piena (col-span-2).
1355
+ ═══════════════════════════════════════════════════════════ -->
1356
+ <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">
1357
+ <legend class="px-2 text-sm font-bold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</legend>
1358
+
1359
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mb-3 mt-1">{{ schema.description }}</p>
1360
+
1361
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5 mb-3">
1362
+ <span *ngFor="let badge of metaBadges"
1363
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1364
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1365
+ </div>
1366
+
1367
+ <!-- campi semplici (string/number/bool/enum) → griglia 2 colonne -->
1368
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4" [formGroup]="controlAsGroup">
1369
+ <ng-container *ngFor="let key of simpleKeys">
1370
+ <div class="flex flex-col gap-1">
1371
+ <div *ngIf="isDynamicKey(key)"
1372
+ 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">
1373
+ <span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
1374
+ <button type="button"
1375
+ 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"
1376
+ (click)="removeDynamicKey(key)">Remove</button>
1377
+ </div>
1378
+ <jsm-schema-node
1379
+ [schema]="schemaForKey(key)"
1380
+ [control]="childControl(key)"
1381
+ [parent]="controlAsGroup"
1382
+ [controlKey]="key"
1383
+ [path]="pathForChild(key)"
1384
+ [errorsMap]="errorsMap"
1385
+ [label]="key"
1386
+ [required]="isRequired(key)"
1387
+ [allowAdditionalProperties]="allowAdditionalProperties"
1388
+ (controlReplaced)="controlReplaced.emit($event)">
1389
+ </jsm-schema-node>
1390
+ </div>
1391
+ </ng-container>
1392
+ </div>
1393
+
1394
+ <!-- campi complessi (object/array/combinator) → colonna piena -->
1395
+ <div class="space-y-4 mt-4" *ngIf="complexKeys.length > 0" [formGroup]="controlAsGroup">
1396
+ <ng-container *ngFor="let key of complexKeys">
1397
+ <div class="flex flex-col gap-1">
1398
+ <div *ngIf="isDynamicKey(key)"
1399
+ 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">
1400
+ <span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
1401
+ <button type="button"
1402
+ 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"
1403
+ (click)="removeDynamicKey(key)">Remove</button>
1404
+ </div>
1405
+ <jsm-schema-node
1406
+ [schema]="schemaForKey(key)"
1407
+ [control]="childControl(key)"
1408
+ [parent]="controlAsGroup"
1409
+ [controlKey]="key"
1410
+ [path]="pathForChild(key)"
1411
+ [errorsMap]="errorsMap"
1412
+ [label]="key"
1413
+ [required]="isRequired(key)"
1414
+ [allowAdditionalProperties]="allowAdditionalProperties"
1415
+ (controlReplaced)="controlReplaced.emit($event)">
1416
+ </jsm-schema-node>
1417
+ </div>
1418
+ </ng-container>
1419
+ </div>
1420
+
1421
+ <!-- add dynamic property -->
1422
+ <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">
1423
+ <p class="text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500">Add property</p>
1424
+ <div class="flex gap-2">
1425
+ <input
1426
+ 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"
1427
+ [(ngModel)]="newPropertyKey" placeholder="propertyName" />
1428
+ <button type="button"
1429
+ 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"
1430
+ (click)="addDynamicProperty()">Add</button>
1431
+ </div>
1432
+ <p *ngIf="propertyError" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1433
+ <!-- icona errore: unico caso in cui ha senso, segnala un problema -->
1434
+ <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>
1435
+ {{ propertyError }}
1436
+ </p>
1437
+ </div>
1438
+
1439
+ <div *ngIf="errorsForPath.length" class="mt-3 space-y-1">
1440
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1441
+ <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>
1442
+ {{ error }}
1443
+ </p>
1444
+ </div>
1445
+ </fieldset>
1446
+
1447
+ <!-- ═══════════════════════════════════════════════════════════
1448
+ array — sempre in colonna piena, items in colonna
1449
+ ═══════════════════════════════════════════════════════════ -->
1450
+ <div *ngSwitchCase="'array'" class="space-y-3">
1451
+ <div class="flex items-start justify-between gap-2">
1452
+ <div>
1453
+ <p class="text-sm font-semibold text-slate-800">
1454
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1455
+ </p>
1456
+ <p *ngIf="schema.description" class="text-xs text-slate-500 mt-0.5">{{ schema.description }}</p>
1457
+ </div>
1458
+ <!-- icona + nel bottone: indica l'azione "aggiungi", non decorativa -->
1459
+ <button type="button"
1460
+ 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"
1461
+ (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1462
+ <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>
1463
+ Add item
1464
+ </button>
1465
+ </div>
1466
+
1467
+ <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">
1468
+ Must contain matching item(s)
1469
+ <span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
1470
+ <span *ngIf="effectiveSchema.maxContains"> · max {{ effectiveSchema.maxContains }}</span>
1471
+ </div>
1472
+
1473
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
1474
+ <span *ngFor="let badge of metaBadges"
1475
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1476
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1477
+ </div>
1478
+
1479
+ <div class="space-y-2" *ngIf="arrayControls.length > 0; else emptyArray">
1480
+ <div *ngFor="let item of arrayControls; let i = index"
1481
+ 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">
1482
+ <div class="flex items-center justify-between mb-3">
1483
+ <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>
1484
+ <button type="button"
1485
+ 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"
1486
+ (click)="removeArrayItem(i)">Remove</button>
1487
+ </div>
1488
+ <jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
1489
+ [controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
1490
+ [allowAdditionalProperties]="allowAdditionalProperties"
1491
+ (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1492
+ </div>
1493
+ </div>
1494
+
1495
+ <ng-template #emptyArray>
1496
+ <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">
1497
+ No items yet — click "Add item" to start.
1498
+ </div>
1499
+ </ng-template>
1500
+
1501
+ <div *ngIf="errorsForPath.length" class="space-y-1">
1502
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1503
+ <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>
1504
+ {{ error }}
1505
+ </p>
1506
+ </div>
1507
+ </div>
1508
+
1509
+ <!-- ═══════════════════════════════════════════════════════════
1510
+ primitive — sempre in colonna, nessuna icona decorativa.
1511
+ Icona errore solo nei messaggi di validazione.
1512
+ Icona chevron nel select: funzionale (indica dropdown).
1513
+ ═══════════════════════════════════════════════════════════ -->
1514
+ <div *ngSwitchDefault class="space-y-1.5">
1515
+ <ng-container *ngIf="inputKind !== 'checkbox'">
1516
+ <label class="block text-[13px] font-semibold text-slate-700 dark:text-slate-300">
1517
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1518
+ </label>
1519
+ <p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 -mt-0.5">{{ schema.description }}</p>
1520
+ </ng-container>
1521
+
1522
+ <div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
1523
+ <span *ngFor="let badge of metaBadges"
1524
+ class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
1525
+ [ngClass]="badgeClass(badge)">{{ badge }}</span>
1526
+ </div>
1527
+
1528
+ <ng-container [ngSwitch]="inputKind">
1529
+
1530
+ <input *ngSwitchCase="'text'"
1531
+ 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"
1532
+ [class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
1533
+ [class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
1534
+ [class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
1535
+ [class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
1536
+ [class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
1537
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1538
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1539
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1540
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1541
+ [class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
1542
+ [class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
1543
+ [attr.type]="inputType"
1544
+ [attr.placeholder]="schema.title || label"
1545
+ [formControl]="controlAsFormControl"
1546
+ />
1547
+
1548
+ <textarea *ngSwitchCase="'textarea'" rows="4"
1549
+ 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"
1550
+ [class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
1551
+ [class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
1552
+ [class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
1553
+ [class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
1554
+ [class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
1555
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1556
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1557
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1558
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1559
+ [class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
1560
+ [class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
1561
+ [attr.placeholder]="schema.title || label"
1562
+ [formControl]="controlAsFormControl"
1563
+ ></textarea>
1564
+
1565
+ <!-- select: il chevron SVG è funzionale, indica che è un dropdown -->
1566
+ <div *ngSwitchCase="'select'" class="relative">
1567
+ <select
1568
+ 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"
1569
+ [class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
1570
+ [class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
1571
+ [class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
1572
+ [class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
1573
+ [formControl]="controlAsFormControl">
1574
+ <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1575
+ </select>
1576
+ <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"
1577
+ viewBox="0 0 16 16" fill="none">
1578
+ <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1579
+ </svg>
1580
+ </div>
1581
+
1582
+ <!-- checkbox come toggle switch -->
1583
+ <label *ngSwitchCase="'checkbox'" class="inline-flex cursor-pointer items-center gap-2.5">
1584
+ <span class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors"
1585
+ [class.bg-indigo-600]="controlAsFormControl.value"
1586
+ [class.bg-slate-300]="!controlAsFormControl.value"
1587
+ [class.dark:bg-slate-600]="!controlAsFormControl.value">
1588
+ <input type="checkbox" class="sr-only" [formControl]="controlAsFormControl" />
1589
+ <span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform"
1590
+ [class.translate-x-4]="controlAsFormControl.value"
1591
+ [class.translate-x-0.5]="!controlAsFormControl.value"></span>
1592
+ </span>
1593
+ <span class="text-sm font-medium text-slate-700 dark:text-slate-300">
1594
+ {{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
1595
+ </span>
1596
+ </label>
1597
+
1598
+ </ng-container>
1599
+
1600
+ <p *ngIf="inputKind === 'checkbox' && schema.description" class="text-xs text-slate-500 dark:text-slate-400">{{ schema.description }}</p>
1601
+
1602
+ <p *ngIf="effectiveSchema.examples?.length" class="text-[11px] text-slate-400 dark:text-slate-500">
1603
+ e.g. {{ effectiveSchema.examples?.[0] }}
1604
+ </p>
1605
+
1606
+ <p *ngIf="effectiveSchema.contentMediaType" class="text-[11px] text-slate-400 dark:text-slate-500">
1607
+ {{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
1608
+ </p>
1609
+
1610
+ <!-- icona errore: funzionale, segnala un problema di validazione -->
1611
+ <div *ngIf="errorsForPath.length && controlAsFormControl.dirty" class="space-y-1 pt-0.5">
1612
+ <p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
1613
+ <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
1614
+ <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
1615
+ <path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1616
+ </svg>
1617
+ {{ error }}
1618
+ </p>
1619
+ </div>
1620
+ </div>
1621
+
1622
+ </ng-container>
1486
1623
  `,
1487
1624
  changeDetection: ChangeDetectionStrategy.OnPush,
1488
1625
  }]
@@ -1504,6 +1641,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1504
1641
  type: Input
1505
1642
  }], controlKey: [{
1506
1643
  type: Input
1644
+ }], allowAdditionalProperties: [{
1645
+ type: Input
1507
1646
  }], controlReplaced: [{
1508
1647
  type: Output
1509
1648
  }] } });
@@ -1515,13 +1654,16 @@ class JsonSchemaFormComponent {
1515
1654
  schema;
1516
1655
  value;
1517
1656
  data;
1657
+ allowAdditionalProperties = false;
1518
1658
  formReady = new EventEmitter();
1519
1659
  valueChange = new EventEmitter();
1520
1660
  schemaReady = new EventEmitter();
1661
+ schemaErrors = new EventEmitter();
1521
1662
  form;
1522
1663
  rootControl;
1523
1664
  resolvedSchema;
1524
1665
  errorsMap = new Map();
1666
+ schemaErrorList = [];
1525
1667
  loading = false;
1526
1668
  valueSub;
1527
1669
  constructor(schemaService, resolver, validation) {
@@ -1541,7 +1683,17 @@ class JsonSchemaFormComponent {
1541
1683
  this.valueSub?.unsubscribe();
1542
1684
  this.loading = true;
1543
1685
  this.resolvedSchema = await this.resolver.resolve(this.schema, this.schema?.$id);
1686
+ // Validate the schema itself and emit any structural errors
1687
+ const schemaErrs = this.validation.validateSchema(this.resolvedSchema);
1688
+ this.schemaErrorList = schemaErrs;
1689
+ this.schemaErrors.emit(schemaErrs);
1544
1690
  this.schemaReady.emit(this.resolvedSchema);
1691
+ // If the schema has structural errors, stop here — don't try to build the form
1692
+ if (schemaErrs.length > 0) {
1693
+ this.form = undefined;
1694
+ this.loading = false;
1695
+ return;
1696
+ }
1545
1697
  const initialData = this.value !== undefined ? this.value : this.data;
1546
1698
  this.rootControl = this.schemaService.buildControl(this.resolvedSchema, initialData);
1547
1699
  this.form = this.rootControl instanceof FormGroup ? this.rootControl : new FormGroup({ value: this.rootControl });
@@ -1588,61 +1740,121 @@ class JsonSchemaFormComponent {
1588
1740
  });
1589
1741
  }
1590
1742
  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 });
1743
+ 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: `
1744
+ <div class="jsm-root">
1745
+ <form class="w-full space-y-5">
1746
+ <div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
1747
+ <h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || 'Form' }}</h2>
1748
+ <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
1749
+ </div>
1750
+
1751
+ <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
1752
+ <svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none">
1753
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
1754
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
1755
+ </svg>
1756
+ Resolving schema&hellip;
1757
+ </div>
1758
+
1759
+ <!-- schema structural errors panel -->
1760
+ <div *ngIf="schemaErrorList.length > 0 && !loading"
1761
+ class="rounded-xl border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4 space-y-3"
1762
+ role="alert">
1763
+ <div class="flex items-center gap-2">
1764
+ <svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none">
1765
+ <path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
1766
+ <path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1767
+ </svg>
1768
+ <span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
1769
+ Schema errors ({{ schemaErrorList.length }})
1770
+ </span>
1771
+ </div>
1772
+ <p class="text-xs text-amber-700 dark:text-amber-400">
1773
+ The schema has structural issues. Fix them to get a valid form.
1774
+ </p>
1775
+ <div class="space-y-2">
1776
+ <div *ngFor="let err of schemaErrorList"
1777
+ 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">
1778
+ <p class="font-mono text-[11px] font-semibold text-amber-700 dark:text-amber-400">{{ err.path }}</p>
1779
+ <p class="text-xs text-slate-700 dark:text-slate-300">{{ err.message }}</p>
1780
+ <p class="text-[10px] uppercase tracking-wide text-slate-400 dark:text-slate-500">keyword: {{ err.keyword }}</p>
1781
+ </div>
1782
+ </div>
1783
+ </div>
1784
+
1785
+ <div *ngIf="form && resolvedSchema && !loading" class="space-y-5" [formGroup]="form">
1786
+ <jsm-schema-node
1787
+ [schema]="resolvedSchema"
1788
+ [control]="rootControl"
1789
+ [path]="''"
1790
+ [errorsMap]="errorsMap"
1791
+ [allowAdditionalProperties]="allowAdditionalProperties"
1792
+ label="Root"
1793
+ (controlReplaced)="onRootReplaced($event)"
1794
+ ></jsm-schema-node>
1795
+ </div>
1796
+ </form>
1797
+ </div>
1798
+ `, isInline: true, styles: [".jsm-root *,.jsm-root *:before,.jsm-root *:after{box-sizing:border-box}.jsm-root input,.jsm-root textarea,.jsm-root select,.jsm-root button{font-family:inherit;font-size:inherit;line-height:inherit}.jsm-root input[type=number]::-webkit-inner-spin-button,.jsm-root input[type=number]::-webkit-outer-spin-button{opacity:1}.jsm-root input:not([type=checkbox]):not([type=radio]):not(.sr-only),.jsm-root textarea,.jsm-root select{border-width:1.5px!important;border-style:solid!important}\n"], 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, encapsulation: i0.ViewEncapsulation.None });
1614
1799
  }
1615
1800
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaFormComponent, decorators: [{
1616
1801
  type: Component,
1617
- args: [{
1618
- selector: 'jsm-json-schema-form',
1619
- standalone: true,
1620
- 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>
1643
- `,
1644
- changeDetection: ChangeDetectionStrategy.OnPush,
1645
- }]
1802
+ args: [{ selector: 'jsm-json-schema-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, JsonSchemaNodeComponent], encapsulation: ViewEncapsulation.None, template: `
1803
+ <div class="jsm-root">
1804
+ <form class="w-full space-y-5">
1805
+ <div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
1806
+ <h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || 'Form' }}</h2>
1807
+ <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
1808
+ </div>
1809
+
1810
+ <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
1811
+ <svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none">
1812
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
1813
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
1814
+ </svg>
1815
+ Resolving schema&hellip;
1816
+ </div>
1817
+
1818
+ <!-- schema structural errors panel -->
1819
+ <div *ngIf="schemaErrorList.length > 0 && !loading"
1820
+ class="rounded-xl border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4 space-y-3"
1821
+ role="alert">
1822
+ <div class="flex items-center gap-2">
1823
+ <svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none">
1824
+ <path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
1825
+ <path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1826
+ </svg>
1827
+ <span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
1828
+ Schema errors ({{ schemaErrorList.length }})
1829
+ </span>
1830
+ </div>
1831
+ <p class="text-xs text-amber-700 dark:text-amber-400">
1832
+ The schema has structural issues. Fix them to get a valid form.
1833
+ </p>
1834
+ <div class="space-y-2">
1835
+ <div *ngFor="let err of schemaErrorList"
1836
+ 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">
1837
+ <p class="font-mono text-[11px] font-semibold text-amber-700 dark:text-amber-400">{{ err.path }}</p>
1838
+ <p class="text-xs text-slate-700 dark:text-slate-300">{{ err.message }}</p>
1839
+ <p class="text-[10px] uppercase tracking-wide text-slate-400 dark:text-slate-500">keyword: {{ err.keyword }}</p>
1840
+ </div>
1841
+ </div>
1842
+ </div>
1843
+
1844
+ <div *ngIf="form && resolvedSchema && !loading" class="space-y-5" [formGroup]="form">
1845
+ <jsm-schema-node
1846
+ [schema]="resolvedSchema"
1847
+ [control]="rootControl"
1848
+ [path]="''"
1849
+ [errorsMap]="errorsMap"
1850
+ [allowAdditionalProperties]="allowAdditionalProperties"
1851
+ label="Root"
1852
+ (controlReplaced)="onRootReplaced($event)"
1853
+ ></jsm-schema-node>
1854
+ </div>
1855
+ </form>
1856
+ </div>
1857
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".jsm-root *,.jsm-root *:before,.jsm-root *:after{box-sizing:border-box}.jsm-root input,.jsm-root textarea,.jsm-root select,.jsm-root button{font-family:inherit;font-size:inherit;line-height:inherit}.jsm-root input[type=number]::-webkit-inner-spin-button,.jsm-root input[type=number]::-webkit-outer-spin-button{opacity:1}.jsm-root input:not([type=checkbox]):not([type=radio]):not(.sr-only),.jsm-root textarea,.jsm-root select{border-width:1.5px!important;border-style:solid!important}\n"] }]
1646
1858
  }], ctorParameters: () => [{ type: JsonSchemaFormService }, { type: JsonSchemaResolverService }, { type: JsonSchemaValidationService }], propDecorators: { schema: [{
1647
1859
  type: Input,
1648
1860
  args: [{ required: true }]
@@ -1650,12 +1862,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
1650
1862
  type: Input
1651
1863
  }], data: [{
1652
1864
  type: Input
1865
+ }], allowAdditionalProperties: [{
1866
+ type: Input
1653
1867
  }], formReady: [{
1654
1868
  type: Output
1655
1869
  }], valueChange: [{
1656
1870
  type: Output
1657
1871
  }], schemaReady: [{
1658
1872
  type: Output
1873
+ }], schemaErrors: [{
1874
+ type: Output
1659
1875
  }] } });
1660
1876
 
1661
1877
  class JsonSchemaFormModule {