@elle96/ng-json-schema-manager 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -579,11 +579,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
579
579
  }]
580
580
  }] });
581
581
 
582
+ const DEFAULT_LABELS = {
583
+ formTitle: 'Form',
584
+ resolving: 'Resolving schema…',
585
+ schemaErrorsTitle: 'Schema errors',
586
+ schemaErrorsHint: 'The schema has structural issues. Fix them to get a valid form.',
587
+ addItem: 'Add item',
588
+ arrayItem: 'Item {n}',
589
+ remove: 'Remove',
590
+ add: 'Add',
591
+ addProperty: 'Add property',
592
+ propertyName: 'Property name',
593
+ noItems: 'No items yet — click "Add item" to start.',
594
+ mustContain: 'Must contain matching item(s)',
595
+ min: 'min {n}',
596
+ max: 'max {n}',
597
+ option: 'Option {n}',
598
+ section: 'Section {n}',
599
+ example: 'e.g.',
600
+ propertyNameRequired: 'Property name is required.',
601
+ propertyNameInvalid: 'Property name does not match schema constraints.',
602
+ propertyExists: 'Property already exists.',
603
+ propertyNotAllowed: 'This property is not allowed by the schema.',
604
+ errorRequired: 'This field is required',
605
+ errorMinLength: 'Minimum length is {n}',
606
+ errorMaxLength: 'Maximum length is {n}',
607
+ errorMin: 'Minimum value is {n}',
608
+ errorMax: 'Maximum value is {n}',
609
+ errorEmail: 'Invalid email address',
610
+ errorPattern: 'Value does not match the required pattern',
611
+ errorInteger: 'Value must be an integer',
612
+ errorExclusiveMin: 'Value must be greater than the minimum',
613
+ errorExclusiveMax: 'Value must be less than the maximum',
614
+ errorMultipleOf: 'Value must be a multiple of the required number',
615
+ errorMinItems: 'Minimum {n} items required',
616
+ errorMaxItems: 'Maximum {n} items allowed',
617
+ errorUniqueItems: 'Items must be unique',
618
+ };
619
+ /** Replaces `{key}` placeholders in a label template. */
620
+ function formatLabel(template, values = {}) {
621
+ return template.replace(/\{(\w+)\}/g, (match, key) => (key in values ? String(values[key]) : match));
622
+ }
623
+
582
624
  /**
583
625
  * Options shared by every node of a form. Provided by `jsm-json-schema-form`, so each form instance has its own.
584
626
  */
585
627
  class JsonSchemaFormOptions {
586
628
  decimalSeparator = '.';
629
+ labels = DEFAULT_LABELS;
587
630
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: JsonSchemaFormOptions, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
588
631
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: JsonSchemaFormOptions });
589
632
  }
@@ -721,6 +764,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
721
764
  type: Input
722
765
  }] } });
723
766
 
767
+ let nextNodeId = 0;
724
768
  class JsonSchemaNodeComponent {
725
769
  schemaService;
726
770
  validation;
@@ -744,6 +788,8 @@ class JsonSchemaNodeComponent {
744
788
  propertyError = '';
745
789
  /** Tracks whether this primitive control has been touched (for OnPush CD). */
746
790
  isTouched = false;
791
+ /** Unique DOM id base of this node: links labels, hints and errors to the control (WCAG 1.3.1, 4.1.2). */
792
+ nodeId = `jsm-n${++nextNodeId}`;
747
793
  valueSub;
748
794
  constructor(schemaService, validation, cdr, options) {
749
795
  this.schemaService = schemaService;
@@ -804,6 +850,74 @@ class JsonSchemaNodeComponent {
804
850
  get decimalSeparator() {
805
851
  return this.options?.decimalSeparator ?? '.';
806
852
  }
853
+ /** Texts shown by the form (see `labels` of `jsm-json-schema-form`). */
854
+ get l() {
855
+ return this.options?.labels ?? DEFAULT_LABELS;
856
+ }
857
+ fmt(template, values = {}) {
858
+ return formatLabel(template, values);
859
+ }
860
+ localize(message) {
861
+ return localizeNumbers(message, this.decimalSeparator);
862
+ }
863
+ optionLabel(index) {
864
+ return this.fmt(this.l.option, { n: index + 1 });
865
+ }
866
+ sectionLabel(index) {
867
+ return this.fmt(this.l.section, { n: index + 1 });
868
+ }
869
+ /** DOM id of a dynamic key label (keys may contain characters that are not valid in ids). */
870
+ keyId(key) {
871
+ return `${this.nodeId}-key-${key.replace(/[^A-Za-z0-9_-]/g, (c) => `_${c.charCodeAt(0).toString(16)}`)}`;
872
+ }
873
+ /** True when the error list of this primitive is shown (same gate as the error styling). */
874
+ get visibleError() {
875
+ return this.allErrors.length > 0 && (this.control.dirty || this.isTouched || this.showErrors);
876
+ }
877
+ /** Ids of the hint, example, media type and error texts that describe this control. */
878
+ get describedBy() {
879
+ const ids = [];
880
+ if (this.schema.description)
881
+ ids.push(`${this.nodeId}-hint`);
882
+ if (this.effectiveSchema.examples?.length)
883
+ ids.push(`${this.nodeId}-example`);
884
+ if (this.effectiveSchema.contentMediaType)
885
+ ids.push(`${this.nodeId}-media`);
886
+ if (this.visibleError)
887
+ ids.push(`${this.nodeId}-error`);
888
+ return ids.length ? ids.join(' ') : null;
889
+ }
890
+ selectOneOf(index) {
891
+ this.selectedOneOf = index;
892
+ this.onOneOfChange();
893
+ }
894
+ /** WAI-ARIA tabs keyboard support: arrows, Home and End move between the oneOf options. */
895
+ onOneOfKeydown(event) {
896
+ const count = this.schema.oneOf?.length ?? 0;
897
+ if (!count)
898
+ return;
899
+ let next = this.selectedOneOf;
900
+ switch (event.key) {
901
+ case 'ArrowRight':
902
+ case 'ArrowDown':
903
+ next = (next + 1) % count;
904
+ break;
905
+ case 'ArrowLeft':
906
+ case 'ArrowUp':
907
+ next = (next - 1 + count) % count;
908
+ break;
909
+ case 'Home':
910
+ next = 0;
911
+ break;
912
+ case 'End':
913
+ next = count - 1;
914
+ break;
915
+ default: return;
916
+ }
917
+ event.preventDefault();
918
+ this.selectOneOf(next);
919
+ setTimeout(() => document.getElementById(`${this.nodeId}-tab-${next}`)?.focus());
920
+ }
807
921
  /** `multipleOf` of this node's own schema, exposed as step metadata. */
808
922
  get numericStep() {
809
923
  return this.effectiveSchema.multipleOf ? String(this.effectiveSchema.multipleOf) : 'any';
@@ -855,34 +969,35 @@ class JsonSchemaNodeComponent {
855
969
  return ajvErrors;
856
970
  const angularErrors = [];
857
971
  const errs = ctrl.errors;
972
+ const l = this.l;
858
973
  if (errs['required'])
859
- angularErrors.push('This field is required');
974
+ angularErrors.push(l.errorRequired);
860
975
  if (errs['minlength'])
861
- angularErrors.push(`Minimum length is ${errs['minlength'].requiredLength}`);
976
+ angularErrors.push(this.fmt(l.errorMinLength, { n: errs['minlength'].requiredLength }));
862
977
  if (errs['maxlength'])
863
- angularErrors.push(`Maximum length is ${errs['maxlength'].requiredLength}`);
978
+ angularErrors.push(this.fmt(l.errorMaxLength, { n: errs['maxlength'].requiredLength }));
864
979
  if (errs['min'])
865
- angularErrors.push(`Minimum value is ${errs['min'].min}`);
980
+ angularErrors.push(this.fmt(l.errorMin, { n: errs['min'].min }));
866
981
  if (errs['max'])
867
- angularErrors.push(`Maximum value is ${errs['max'].max}`);
982
+ angularErrors.push(this.fmt(l.errorMax, { n: errs['max'].max }));
868
983
  if (errs['email'])
869
- angularErrors.push('Invalid email address');
984
+ angularErrors.push(l.errorEmail);
870
985
  if (errs['pattern'])
871
- angularErrors.push('Value does not match the required pattern');
986
+ angularErrors.push(l.errorPattern);
872
987
  if (errs['integer'])
873
- angularErrors.push('Value must be an integer');
988
+ angularErrors.push(l.errorInteger);
874
989
  if (errs['exclusiveMinimum'])
875
- angularErrors.push('Value must be greater than the minimum');
990
+ angularErrors.push(l.errorExclusiveMin);
876
991
  if (errs['exclusiveMaximum'])
877
- angularErrors.push('Value must be less than the maximum');
992
+ angularErrors.push(l.errorExclusiveMax);
878
993
  if (errs['multipleOf'])
879
- angularErrors.push('Value must be a multiple of the required number');
994
+ angularErrors.push(l.errorMultipleOf);
880
995
  if (errs['minItems'])
881
- angularErrors.push(`Minimum ${errs['minItems'].requiredLength ?? ''} items required`);
996
+ angularErrors.push(this.fmt(l.errorMinItems, { n: errs['minItems'].requiredLength ?? '' }));
882
997
  if (errs['maxItems'])
883
- angularErrors.push(`Maximum ${errs['maxItems'].requiredLength ?? ''} items allowed`);
998
+ angularErrors.push(this.fmt(l.errorMaxItems, { n: errs['maxItems'].requiredLength ?? '' }));
884
999
  if (errs['uniqueItems'])
885
- angularErrors.push('Items must be unique');
1000
+ angularErrors.push(l.errorUniqueItems);
886
1001
  // Merge: prefer Ajv messages, fall back to Angular messages for errors not covered by Ajv
887
1002
  const messages = ajvErrors.length > 0 ? ajvErrors : angularErrors;
888
1003
  return messages.map((message) => localizeNumbers(message, this.decimalSeparator));
@@ -912,8 +1027,16 @@ class JsonSchemaNodeComponent {
912
1027
  return this.mergeSchemas([this.schema, ...this.schema.allOf]);
913
1028
  return this.effectiveSchema;
914
1029
  }
1030
+ /**
1031
+ * Sections rendered for an allOf: the schema's own keywords (without the allOf itself, which would render
1032
+ * this node again forever) when it declares any, followed by every allOf entry.
1033
+ */
915
1034
  get mergedAllOf() {
916
- return this.schema.allOf?.length ? [this.schema, ...this.schema.allOf] : [this.schema];
1035
+ const { allOf, ...own } = this.schema;
1036
+ if (!allOf?.length)
1037
+ return [this.schema];
1038
+ const declaresOwn = Object.keys(own).some((key) => !['title', 'description', '$id', '$schema', '$comment'].includes(key));
1039
+ return declaresOwn ? [own, ...allOf] : [...allOf];
917
1040
  }
918
1041
  isRequired(key) { return this.effectiveSchema.required?.includes(key) ?? false; }
919
1042
  schemaForKey(key) {
@@ -927,22 +1050,22 @@ class JsonSchemaNodeComponent {
927
1050
  this.propertyError = '';
928
1051
  const key = this.newPropertyKey.trim();
929
1052
  if (!key) {
930
- this.propertyError = 'Property name is required.';
1053
+ this.propertyError = this.l.propertyNameRequired;
931
1054
  return;
932
1055
  }
933
1056
  if (this.effectiveSchema.propertyNames) {
934
1057
  if (this.validation.validate(this.effectiveSchema.propertyNames, key).length) {
935
- this.propertyError = 'Property name does not match schema constraints.';
1058
+ this.propertyError = this.l.propertyNameInvalid;
936
1059
  return;
937
1060
  }
938
1061
  }
939
1062
  if (this.controlAsGroup.contains(key)) {
940
- this.propertyError = 'Property already exists.';
1063
+ this.propertyError = this.l.propertyExists;
941
1064
  return;
942
1065
  }
943
1066
  const s = this.schemaService.resolveDynamicSchema(this.effectiveSchema, key);
944
1067
  if (!s) {
945
- this.propertyError = 'This property is not allowed by the schema.';
1068
+ this.propertyError = this.l.propertyNotAllowed;
946
1069
  return;
947
1070
  }
948
1071
  this.controlAsGroup.addControl(key, this.schemaService.buildControl(s, undefined, false));
@@ -1077,14 +1200,17 @@ class JsonSchemaNodeComponent {
1077
1200
  </div>
1078
1201
  <span class="jsm-combinator-badge jsm-badge--oneof">oneOf</span>
1079
1202
  </div>
1080
- <div class="jsm-oneof-tabs">
1081
- <button *ngFor="let option of schema.oneOf; let i = index" type="button"
1203
+ <div class="jsm-oneof-tabs" role="tablist" [attr.aria-label]="schema.title || label || null" (keydown)="onOneOfKeydown($event)">
1204
+ <button *ngFor="let option of schema.oneOf; let i = index" type="button" role="tab"
1082
1205
  class="jsm-tab" [class.jsm-tab--active]="selectedOneOf === i"
1083
- (click)="selectedOneOf = i; onOneOfChange()">
1084
- {{ option.title || ('Option ' + (i + 1)) }}
1206
+ [id]="nodeId + '-tab-' + i" [attr.aria-selected]="selectedOneOf === i"
1207
+ [attr.aria-controls]="nodeId + '-panel'" [attr.tabindex]="selectedOneOf === i ? 0 : -1"
1208
+ (click)="selectOneOf(i)">
1209
+ {{ option.title || optionLabel(i) }}
1085
1210
  </button>
1086
1211
  </div>
1087
- <div class="jsm-combinator-body" *ngIf="activeVariant">
1212
+ <div class="jsm-combinator-body" *ngIf="activeVariant" role="tabpanel" [id]="nodeId + '-panel'"
1213
+ [attr.aria-labelledby]="nodeId + '-tab-' + selectedOneOf">
1088
1214
  <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
1089
1215
  [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
1090
1216
  [allowAdditionalProperties]="allowAdditionalProperties" [showErrors]="showErrors"
@@ -1101,11 +1227,11 @@ class JsonSchemaNodeComponent {
1101
1227
  </div>
1102
1228
  <span class="jsm-combinator-badge jsm-badge--anyof">anyOf</span>
1103
1229
  </div>
1104
- <div class="jsm-anyof-options">
1230
+ <div class="jsm-anyof-options" role="group" [attr.aria-label]="schema.title || label || null">
1105
1231
  <label *ngFor="let option of schema.anyOf; let i = index"
1106
1232
  class="jsm-anyof-option" [class.jsm-anyof-option--active]="selectedAnyOf.has(i)">
1107
1233
  <input type="checkbox" class="sr-only" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
1108
- {{ option.title || ('Option ' + (i + 1)) }}
1234
+ {{ option.title || optionLabel(i) }}
1109
1235
  </label>
1110
1236
  </div>
1111
1237
  <div class="jsm-combinator-body" *ngIf="activeVariant">
@@ -1128,7 +1254,7 @@ class JsonSchemaNodeComponent {
1128
1254
  <div class="jsm-combinator-body">
1129
1255
  <div *ngFor="let subSchema of mergedAllOf; let i = index" class="jsm-allof-section">
1130
1256
  <jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
1131
- [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
1257
+ [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || sectionLabel(i)" [required]="required"
1132
1258
  [allowAdditionalProperties]="allowAdditionalProperties" [showErrors]="showErrors"
1133
1259
  (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1134
1260
  </div>
@@ -1148,8 +1274,9 @@ class JsonSchemaNodeComponent {
1148
1274
  <ng-container *ngFor="let key of simpleKeys">
1149
1275
  <div style="display:flex;flex-direction:column;gap:.25rem;">
1150
1276
  <div *ngIf="isDynamicKey(key)" class="jsm-dynamic-key-header">
1151
- <span class="jsm-dynamic-key-label">{{ key }}</span>
1152
- <button type="button" class="jsm-btn-danger" (click)="removeDynamicKey(key)">Remove</button>
1277
+ <span class="jsm-dynamic-key-label" [id]="keyId(key)">{{ key }}</span>
1278
+ <button type="button" class="jsm-btn-danger" [id]="keyId(key) + '-remove'"
1279
+ [attr.aria-labelledby]="keyId(key) + '-remove ' + keyId(key)" (click)="removeDynamicKey(key)">{{ l.remove }}</button>
1153
1280
  </div>
1154
1281
  <jsm-schema-node [schema]="schemaForKey(key)" [control]="childControl(key)" [parent]="controlAsGroup"
1155
1282
  [controlKey]="key" [path]="pathForChild(key)" [errorsMap]="errorsMap" [label]="key"
@@ -1163,8 +1290,9 @@ class JsonSchemaNodeComponent {
1163
1290
  <ng-container *ngFor="let key of complexKeys">
1164
1291
  <div style="display:flex;flex-direction:column;gap:.25rem;">
1165
1292
  <div *ngIf="isDynamicKey(key)" class="jsm-dynamic-key-header">
1166
- <span class="jsm-dynamic-key-label">{{ key }}</span>
1167
- <button type="button" class="jsm-btn-danger" (click)="removeDynamicKey(key)">Remove</button>
1293
+ <span class="jsm-dynamic-key-label" [id]="keyId(key)">{{ key }}</span>
1294
+ <button type="button" class="jsm-btn-danger" [id]="keyId(key) + '-remove'"
1295
+ [attr.aria-labelledby]="keyId(key) + '-remove ' + keyId(key)" (click)="removeDynamicKey(key)">{{ l.remove }}</button>
1168
1296
  </div>
1169
1297
  <jsm-schema-node [schema]="schemaForKey(key)" [control]="childControl(key)" [parent]="controlAsGroup"
1170
1298
  [controlKey]="key" [path]="pathForChild(key)" [errorsMap]="errorsMap" [label]="key"
@@ -1174,45 +1302,48 @@ class JsonSchemaNodeComponent {
1174
1302
  </ng-container>
1175
1303
  </div>
1176
1304
 
1177
- <div *ngIf="canAddProperty" class="jsm-add-prop">
1178
- <p class="jsm-add-prop-label">Add property</p>
1305
+ <div *ngIf="canAddProperty" class="jsm-add-prop" role="group" [attr.aria-labelledby]="nodeId + '-addprop'">
1306
+ <p class="jsm-add-prop-label" [id]="nodeId + '-addprop'">{{ l.addProperty }}</p>
1179
1307
  <div class="jsm-add-prop-row">
1180
- <input class="jsm-input jsm-input--sm" style="flex:1;" [(ngModel)]="newPropertyKey" placeholder="propertyName" />
1181
- <button type="button" class="jsm-btn-primary" (click)="addDynamicProperty()">Add</button>
1308
+ <input class="jsm-input jsm-input--sm" style="flex:1;" [(ngModel)]="newPropertyKey" [ngModelOptions]="{standalone: true}"
1309
+ [attr.aria-label]="l.propertyName" [attr.placeholder]="l.propertyName"
1310
+ [attr.aria-invalid]="propertyError ? 'true' : null" [attr.aria-describedby]="propertyError ? nodeId + '-prop-error' : null" />
1311
+ <button type="button" class="jsm-btn-primary" (click)="addDynamicProperty()">{{ l.add }}</button>
1182
1312
  </div>
1183
- <p *ngIf="propertyError" class="jsm-error-msg">
1184
- <svg class="jsm-error-icon" 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>
1313
+ <p *ngIf="propertyError" class="jsm-error-msg" role="alert" [id]="nodeId + '-prop-error'">
1314
+ <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><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>
1185
1315
  {{ propertyError }}
1186
1316
  </p>
1187
1317
  </div>
1188
1318
 
1189
- <div *ngIf="errorsForPath.length" class="jsm-error-list" style="margin-top:.75rem;">
1319
+ <div *ngIf="errorsForPath.length" class="jsm-error-list" role="alert" style="margin-top:.75rem;">
1190
1320
  <p *ngFor="let error of errorsForPath" class="jsm-error-msg">
1191
- <svg class="jsm-error-icon" 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>
1192
- {{ error }}
1321
+ <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><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>
1322
+ {{ localize(error) }}
1193
1323
  </p>
1194
1324
  </div>
1195
1325
  </fieldset>
1196
1326
 
1197
1327
  <!-- ═══ array ═══ -->
1198
- <div *ngSwitchCase="'array'" class="jsm-array-block">
1328
+ <div *ngSwitchCase="'array'" class="jsm-array-block" role="group" [attr.aria-labelledby]="nodeId + '-title'">
1199
1329
  <div class="jsm-array-header">
1200
1330
  <div>
1201
- <p class="jsm-array-title">
1202
- {{ schema.title || label }}<span *ngIf="required" style="color:#ef4444;margin-left:.125rem;">*</span>
1331
+ <p class="jsm-array-title" [id]="nodeId + '-title'">
1332
+ {{ schema.title || label }}<span *ngIf="required" class="jsm-required" aria-hidden="true">*</span>
1203
1333
  </p>
1204
1334
  <p *ngIf="schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1205
1335
  </div>
1206
- <button type="button" class="jsm-btn-primary" (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1207
- <svg style="width:.875rem;height:.875rem;flex-shrink:0;" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1208
- Add item
1336
+ <button type="button" class="jsm-btn-primary" [id]="nodeId + '-add'"
1337
+ [attr.aria-labelledby]="nodeId + '-add ' + nodeId + '-title'" (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1338
+ <svg style="width:.875rem;height:.875rem;flex-shrink:0;" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1339
+ {{ l.addItem }}
1209
1340
  </button>
1210
1341
  </div>
1211
1342
 
1212
1343
  <div *ngIf="effectiveSchema.contains" class="jsm-contains-hint">
1213
- Must contain matching item(s)
1214
- <span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
1215
- <span *ngIf="effectiveSchema.maxContains"> · max {{ effectiveSchema.maxContains }}</span>
1344
+ {{ l.mustContain }}
1345
+ <span *ngIf="effectiveSchema.minContains"> · {{ fmt(l.min, { n: effectiveSchema.minContains }) }}</span>
1346
+ <span *ngIf="effectiveSchema.maxContains"> · {{ fmt(l.max, { n: effectiveSchema.maxContains }) }}</span>
1216
1347
  </div>
1217
1348
 
1218
1349
  <div *ngIf="metaBadges.length" class="jsm-badge-row">
@@ -1220,10 +1351,12 @@ class JsonSchemaNodeComponent {
1220
1351
  </div>
1221
1352
 
1222
1353
  <div class="jsm-array-items" *ngIf="arrayControls.length > 0; else emptyArray">
1223
- <div *ngFor="let item of arrayControls; let i = index" class="jsm-array-item">
1354
+ <div *ngFor="let item of arrayControls; let i = index" class="jsm-array-item" role="group" [attr.aria-labelledby]="nodeId + '-item-' + i">
1224
1355
  <div class="jsm-array-item-header">
1225
- <span class="jsm-array-index">{{ i + 1 }}</span>
1226
- <button type="button" class="jsm-btn-danger" (click)="removeArrayItem(i)">Remove</button>
1356
+ <span class="jsm-array-index" aria-hidden="true">{{ i + 1 }}</span>
1357
+ <span class="sr-only" [id]="nodeId + '-item-' + i">{{ fmt(l.arrayItem, { n: i + 1 }) }}</span>
1358
+ <button type="button" class="jsm-btn-danger" [id]="nodeId + '-remove-' + i"
1359
+ [attr.aria-labelledby]="nodeId + '-remove-' + i + ' ' + nodeId + '-item-' + i" (click)="removeArrayItem(i)">{{ l.remove }}</button>
1227
1360
  </div>
1228
1361
  <jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
1229
1362
  [controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
@@ -1233,13 +1366,13 @@ class JsonSchemaNodeComponent {
1233
1366
  </div>
1234
1367
 
1235
1368
  <ng-template #emptyArray>
1236
- <div class="jsm-empty-array">No items yet — click "Add item" to start.</div>
1369
+ <div class="jsm-empty-array">{{ l.noItems }}</div>
1237
1370
  </ng-template>
1238
1371
 
1239
- <div *ngIf="errorsForPath.length" class="jsm-error-list">
1372
+ <div *ngIf="errorsForPath.length" class="jsm-error-list" role="alert">
1240
1373
  <p *ngFor="let error of errorsForPath" class="jsm-error-msg">
1241
- <svg class="jsm-error-icon" 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>
1242
- {{ error }}
1374
+ <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><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>
1375
+ {{ localize(error) }}
1243
1376
  </p>
1244
1377
  </div>
1245
1378
  </div>
@@ -1247,10 +1380,10 @@ class JsonSchemaNodeComponent {
1247
1380
  <!-- ═══ primitive ═══ -->
1248
1381
  <div *ngSwitchDefault class="jsm-field">
1249
1382
  <ng-container *ngIf="inputKind !== 'checkbox'">
1250
- <label class="jsm-field-label">
1251
- {{ schema.title || label }}<span *ngIf="required" class="jsm-required">*</span>
1383
+ <label class="jsm-field-label" [attr.for]="nodeId">
1384
+ {{ schema.title || label }}<span *ngIf="required" class="jsm-required" aria-hidden="true">*</span>
1252
1385
  </label>
1253
- <p *ngIf="schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1386
+ <p *ngIf="schema.description" class="jsm-field-hint" [id]="nodeId + '-hint'">{{ schema.description }}</p>
1254
1387
  </ng-container>
1255
1388
 
1256
1389
  <div *ngIf="metaBadges.length" class="jsm-badge-row">
@@ -1258,57 +1391,65 @@ class JsonSchemaNodeComponent {
1258
1391
  </div>
1259
1392
 
1260
1393
  <ng-container>
1261
- <input *ngIf="inputKind === 'number'" jsmNumeric type="text" inputmode="decimal" class="jsm-input"
1394
+ <input *ngIf="inputKind === 'number'" jsmNumeric type="text" inputmode="decimal" class="jsm-input" [id]="nodeId"
1262
1395
  [class.jsm-input--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1263
1396
  [jsmDecimalSeparator]="decimalSeparator" [attr.data-step]="numericStep"
1264
- [attr.placeholder]="schema.title || label"
1397
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1398
+ [attr.aria-describedby]="describedBy"
1265
1399
  [formControl]="controlAsFormControl"
1266
1400
  (blur)="onBlur()" />
1267
1401
 
1268
- <input *ngIf="inputKind === 'text'" class="jsm-input"
1402
+ <input *ngIf="inputKind === 'text'" class="jsm-input" [id]="nodeId"
1269
1403
  [class.jsm-input--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1270
- [attr.type]="inputType" [attr.placeholder]="schema.title || label"
1404
+ [attr.type]="inputType"
1405
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1406
+ [attr.aria-describedby]="describedBy"
1271
1407
  [formControl]="controlAsFormControl"
1272
1408
  (blur)="onBlur()" />
1273
1409
 
1274
- <textarea *ngIf="inputKind === 'textarea'" rows="4" class="jsm-textarea"
1410
+ <textarea *ngIf="inputKind === 'textarea'" rows="4" class="jsm-textarea" [id]="nodeId"
1275
1411
  [class.jsm-textarea--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1276
- [attr.placeholder]="schema.title || label"
1412
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1413
+ [attr.aria-describedby]="describedBy"
1277
1414
  [formControl]="controlAsFormControl"
1278
1415
  (blur)="onBlur()"></textarea>
1279
1416
 
1280
1417
  <div *ngIf="inputKind === 'select'" class="jsm-select-wrap">
1281
- <select class="jsm-select"
1418
+ <select class="jsm-select" [id]="nodeId"
1282
1419
  [class.jsm-select--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1420
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1421
+ [attr.aria-describedby]="describedBy"
1283
1422
  [formControl]="controlAsFormControl"
1284
1423
  (blur)="onBlur()">
1285
1424
  <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1286
1425
  </select>
1287
- <svg class="jsm-select-chevron" viewBox="0 0 16 16" fill="none">
1426
+ <svg class="jsm-select-chevron" viewBox="0 0 16 16" fill="none" aria-hidden="true">
1288
1427
  <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1289
1428
  </svg>
1290
1429
  </div>
1291
1430
 
1292
1431
  <label *ngIf="inputKind === 'checkbox'" class="jsm-toggle-label">
1293
1432
  <span class="jsm-toggle-track" [class.jsm-on]="controlAsFormControl.value">
1294
- <input type="checkbox" class="sr-only" [formControl]="controlAsFormControl" />
1433
+ <input type="checkbox" class="sr-only" [id]="nodeId" [formControl]="controlAsFormControl"
1434
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1435
+ [attr.aria-describedby]="describedBy" (blur)="onBlur()" />
1295
1436
  <span class="jsm-toggle-thumb"></span>
1296
1437
  </span>
1297
1438
  <span class="jsm-toggle-text">
1298
- {{ schema.title || label }}<span *ngIf="required" class="jsm-required">*</span>
1439
+ {{ schema.title || label }}<span *ngIf="required" class="jsm-required" aria-hidden="true">*</span>
1299
1440
  </span>
1300
1441
  </label>
1301
1442
  </ng-container>
1302
1443
 
1303
- <p *ngIf="inputKind === 'checkbox' && schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1304
- <p *ngIf="effectiveSchema.examples?.length" class="jsm-example">e.g. {{ effectiveSchema.examples?.[0] }}</p>
1305
- <p *ngIf="effectiveSchema.contentMediaType" class="jsm-example">
1444
+ <p *ngIf="inputKind === 'checkbox' && schema.description" class="jsm-field-hint" [id]="nodeId + '-hint'">{{ schema.description }}</p>
1445
+ <p *ngIf="effectiveSchema.examples?.length" class="jsm-example" [id]="nodeId + '-example'">{{ l.example }} {{ effectiveSchema.examples?.[0] }}</p>
1446
+ <p *ngIf="effectiveSchema.contentMediaType" class="jsm-example" [id]="nodeId + '-media'">
1306
1447
  {{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
1307
1448
  </p>
1308
1449
 
1309
- <div *ngIf="allErrors.length && (controlAsFormControl.dirty || isTouched || showErrors)" class="jsm-error-list">
1450
+ <div *ngIf="visibleError" class="jsm-error-list" role="alert" [id]="nodeId + '-error'">
1310
1451
  <p *ngFor="let error of allErrors" class="jsm-error-msg">
1311
- <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none">
1452
+ <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
1312
1453
  <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
1313
1454
  <path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1314
1455
  </svg>
@@ -1318,7 +1459,7 @@ class JsonSchemaNodeComponent {
1318
1459
  </div>
1319
1460
 
1320
1461
  </ng-container>
1321
- `, isInline: true, styles: [".jsm-field{display:flex;flex-direction:column;gap:.375rem}.jsm-field-label{display:block;font-size:.8125rem;font-weight:600;color:#374151}.jsm-field-hint{font-size:.75rem;color:#6b7280;margin:0;line-height:1.4}.jsm-required{color:#ef4444;margin-left:.125rem}.jsm-example{font-size:.6875rem;color:#9ca3af}.jsm-input,.jsm-textarea,.jsm-select{width:100%;border-radius:.5rem;border:1.5px solid #d1d5db;background:Canvas;padding:.5rem .75rem;font-size:.875rem;color:CanvasText;outline:none;box-shadow:0 1px 2px #0000000a;transition:border-color .15s,box-shadow .15s;font-family:inherit;box-sizing:border-box}.jsm-input:hover,.jsm-textarea:hover,.jsm-select:hover{border-color:#9ca3af}.jsm-input:focus,.jsm-textarea:focus,.jsm-select:focus{border-color:#6366f1;box-shadow:0 0 0 3px #6366f126}.jsm-input--error,.jsm-textarea--error,.jsm-select--error{border-color:#f87171!important;background:#fff5f5}.jsm-input--error:focus,.jsm-textarea--error:focus,.jsm-select--error:focus{box-shadow:0 0 0 3px #f8717133!important}.jsm-textarea{resize:vertical;min-height:6rem}.jsm-select-wrap{position:relative}.jsm-select{appearance:none;padding-right:2.25rem;cursor:pointer}.jsm-select-chevron{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);width:1rem;height:1rem;color:#9ca3af;pointer-events:none}.jsm-toggle-label{display:inline-flex;align-items:center;gap:.625rem;cursor:pointer;-webkit-user-select:none;user-select:none}.jsm-toggle-track{position:relative;display:inline-flex;align-items:center;width:2.25rem;height:1.25rem;border-radius:9999px;flex-shrink:0;background:#d1d5db;transition:background .2s}.jsm-toggle-track.jsm-on{background:#4f46e5}.jsm-toggle-thumb{display:inline-block;width:1rem;height:1rem;border-radius:9999px;background:#fff;box-shadow:0 1px 3px #0003;transform:translate(.125rem);transition:transform .2s}.jsm-toggle-track.jsm-on .jsm-toggle-thumb{transform:translate(1.125rem)}.jsm-toggle-text{font-size:.875rem;font-weight:500;color:#374151}.jsm-btn-primary{display:inline-flex;align-items:center;gap:.25rem;padding:.375rem .75rem;border-radius:.5rem;font-size:.8125rem;font-weight:600;background:#4f46e5;color:#fff;border:none;cursor:pointer;box-shadow:0 1px 2px #4f46e54d;transition:background .15s,transform .1s,box-shadow .15s;white-space:nowrap;font-family:inherit}.jsm-btn-primary:hover{background:#4338ca;transform:translateY(-1px);box-shadow:0 3px 8px #4f46e54d}.jsm-btn-primary:active{transform:translateY(0)}.jsm-btn-primary:disabled{opacity:.45;cursor:not-allowed;transform:none}.jsm-btn-danger{display:inline-flex;align-items:center;gap:.25rem;padding:.25rem .5rem;border-radius:.375rem;font-size:.75rem;font-weight:600;background:transparent;color:#dc2626;border:1.5px solid #fca5a5;cursor:pointer;transition:background .15s,border-color .15s;font-family:inherit}.jsm-btn-danger:hover{background:#fee2e2;border-color:#f87171}.jsm-error-list{display:flex;flex-direction:column;gap:.25rem;margin-top:.25rem}.jsm-error-msg{display:flex;align-items:center;gap:.375rem;font-size:.75rem;color:#dc2626;font-weight:500}.jsm-error-icon{width:.875rem;height:.875rem;flex-shrink:0}.jsm-badge-row{display:flex;flex-wrap:wrap;gap:.375rem}.jsm-badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .5rem;font-size:.6875rem;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.jsm-badge--deprecated{background:#fef3c7;color:#92400e}.jsm-badge--readonly{background:#f1f5f9;color:#475569}.jsm-badge--writeonly{background:#dbeafe;color:#1e40af}.jsm-badge--not{background:#fee2e2;color:#991b1b}.jsm-badge--nullable{background:#f3f4f6;color:#6b7280}.jsm-fieldset{border:1.5px solid #e5e7eb;border-radius:.875rem;padding:1.25rem;background:transparent;box-shadow:0 1px 3px #0000000f;transition:box-shadow .2s;box-sizing:border-box}.jsm-fieldset:focus-within{border-color:#c7d2fe;box-shadow:0 0 0 3px #6366f114,0 1px 3px #0000000f}.jsm-legend{display:inline-flex;align-items:center;gap:.375rem;padding:0 .5rem;font-size:.875rem;font-weight:700;color:#1f2937}.jsm-obj-grid{display:grid;grid-template-columns:1fr;gap:1rem}@media (min-width:640px){.jsm-obj-grid{grid-template-columns:repeat(2,1fr)}}.jsm-obj-complex{display:flex;flex-direction:column;gap:1rem;margin-top:1rem}.jsm-dynamic-key-header{display:flex;align-items:center;justify-content:space-between;padding:.25rem .5rem;background:#f9fafb;border-radius:.375rem;border:1px dashed #d1d5db;margin-bottom:.25rem}.jsm-dynamic-key-label{font-size:.75rem;font-weight:600;color:#6b7280;font-family:monospace}.jsm-add-prop{margin-top:1rem;padding:.875rem;background:#f9fafb;border-radius:.625rem;border:1.5px dashed #d1d5db;display:flex;flex-direction:column;gap:.5rem}.jsm-add-prop-label{font-size:.75rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.04em}.jsm-add-prop-row{display:flex;gap:.5rem;align-items:center}.jsm-input--sm{padding:.375rem .625rem;font-size:.8125rem}.jsm-array-block{display:flex;flex-direction:column;gap:.75rem}.jsm-array-header{display:flex;align-items:flex-start;justify-content:space-between;gap:.5rem}.jsm-array-title{font-size:.875rem;font-weight:700;color:#1f2937;display:flex;align-items:center;gap:.375rem}.jsm-array-items{display:flex;flex-direction:column;gap:.75rem}.jsm-array-item{border:1.5px solid #e5e7eb;border-radius:.75rem;padding:.875rem;background:transparent;box-shadow:0 1px 2px #0000000a;transition:border-color .15s,box-shadow .15s}.jsm-array-item:focus-within{border-color:#c7d2fe;box-shadow:0 0 0 3px #6366f114}.jsm-array-item-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem}.jsm-array-index{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:9999px;background:#eef2ff;color:#4f46e5;font-size:.6875rem;font-weight:700}.jsm-empty-array{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:2rem 1rem;border:1.5px dashed #e5e7eb;border-radius:.75rem;color:#9ca3af;font-size:.8125rem;text-align:center}.jsm-contains-hint{display:flex;align-items:center;gap:.375rem;font-size:.75rem;color:#6366f1;background:#eef2ff;border-radius:.375rem;padding:.375rem .625rem}.jsm-combinator{border:1.5px solid #e5e7eb;border-radius:.875rem;overflow:hidden;background:transparent;box-shadow:0 1px 3px #0000000f}.jsm-combinator-header{display:flex;align-items:flex-start;justify-content:space-between;padding:.875rem 1rem;background:#00000008;border-bottom:1.5px solid #e5e7eb}.jsm-combinator-title{font-size:.875rem;font-weight:700;color:#1f2937}.jsm-combinator-body{padding:1rem}.jsm-combinator-hint{font-size:.75rem;color:#6b7280;margin:.25rem 0 0}.jsm-oneof-tabs{display:flex;overflow-x:auto;border-bottom:1.5px solid #e5e7eb}.jsm-tab{padding:.5rem 1rem;font-size:.8125rem;font-weight:500;color:#6b7280;border:none;background:transparent;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1.5px;transition:color .15s,border-color .15s;white-space:nowrap;font-family:inherit}.jsm-tab:hover{color:#4f46e5}.jsm-tab--active{color:#4f46e5;border-bottom-color:#4f46e5;font-weight:600}.jsm-anyof-options{display:flex;flex-wrap:wrap;gap:.5rem;padding:.75rem 1rem;border-bottom:1.5px solid #e5e7eb}.jsm-anyof-option{display:inline-flex;align-items:center;gap:.375rem;padding:.375rem .75rem;border-radius:9999px;border:1.5px solid #e5e7eb;font-size:.8125rem;font-weight:500;color:#6b7280;cursor:pointer;transition:all .15s;background:transparent}.jsm-anyof-option:hover{border-color:#a5b4fc;color:#4f46e5}.jsm-anyof-option--active{border-color:#6366f1;background:#eef2ff;color:#4f46e5}.jsm-allof-section{border-left:3px solid #e0e7ff;padding-left:1rem}.jsm-combinator-badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .5rem;font-size:.6875rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0;margin-left:.75rem}.jsm-badge--oneof{background:#dbeafe;color:#1e40af}.jsm-badge--anyof{background:#ede9fe;color:#5b21b6}.jsm-badge--allof{background:#ccfbf1;color:#065f46}.dark .jsm-field-label{color:#d1d5db}.dark .jsm-field-hint{color:#9ca3af}.dark .jsm-toggle-track{background:#475569}.dark .jsm-toggle-track.jsm-on{background:#4f46e5}.dark .jsm-toggle-text{color:#d1d5db}.dark .jsm-input,.dark .jsm-textarea,.dark .jsm-select{border-color:#475569;color:#f1f5f9}.dark .jsm-input:focus,.dark .jsm-textarea:focus,.dark .jsm-select:focus{border-color:#6366f1;box-shadow:0 0 0 3px #6366f133}.dark .jsm-input--error,.dark .jsm-textarea--error,.dark .jsm-select--error{border-color:#f87171!important;background:#f871711a}.dark .jsm-fieldset{border-color:#334155}.dark .jsm-fieldset:focus-within{border-color:#6366f1}.dark .jsm-legend{color:#f1f5f9}.dark .jsm-combinator{border-color:#334155}.dark .jsm-combinator-header{background:#ffffff0a;border-color:#334155}.dark .jsm-combinator-title{color:#f1f5f9}.dark .jsm-oneof-tabs{border-color:#334155}.dark .jsm-tab{color:#94a3b8}.dark .jsm-tab--active{color:#818cf8;border-bottom-color:#818cf8}.dark .jsm-anyof-options{border-color:#334155}.dark .jsm-anyof-option{border-color:#334155;color:#94a3b8;background:#0f172a}.dark .jsm-anyof-option--active{border-color:#6366f1;background:#6366f126;color:#818cf8}.dark .jsm-allof-section{border-left-color:#3730a3}.dark .jsm-array-item{border-color:#334155}.dark .jsm-array-item:focus-within{border-color:#6366f1}.dark .jsm-array-index{background:#6366f133;color:#818cf8}.dark .jsm-empty-array{border-color:#334155;color:#64748b}.dark .jsm-contains-hint{background:#6366f126;color:#818cf8}.dark .jsm-add-prop{background:#1e293b66;border-color:#475569}.dark .jsm-add-prop-label{color:#64748b}.dark .jsm-dynamic-key-header{background:#1e293b80;border-color:#475569}.dark .jsm-dynamic-key-label{color:#94a3b8}.dark .jsm-error-msg{color:#f87171}.dark .jsm-array-title{color:#f1f5f9}.dark .jsm-badge--deprecated{background:#d9770633;color:#fbbf24}.dark .jsm-badge--readonly{background:#1e293b;color:#94a3b8}.dark .jsm-badge--writeonly{background:#2563eb33;color:#60a5fa}.dark .jsm-badge--not{background:#dc262633;color:#f87171}.dark .jsm-badge--nullable{background:#1e293b;color:#94a3b8}.dark .jsm-select-chevron{color:#64748b}\n"], dependencies: [{ kind: "component", type: JsonSchemaNodeComponent, selector: "jsm-schema-node", inputs: ["schema", "control", "label", "required", "path", "errorsMap", "parent", "controlKey", "allowAdditionalProperties", "showErrors"], outputs: ["controlReplaced"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i4.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i4.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i4.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i4.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i5.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i5.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: i5.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i5.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i5.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i5.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: NumericInputDirective, selector: "input[jsmNumeric]", inputs: ["jsmDecimalSeparator"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1462
+ `, isInline: true, styles: [".jsm-field{display:flex;flex-direction:column;gap:.375rem}.jsm-field-label{display:block;font-size:.8125rem;font-weight:600;color:#374151}.jsm-field-hint{font-size:.75rem;color:#6b7280;margin:0;line-height:1.4}.jsm-required{color:#dc2626;margin-left:.125rem}.jsm-example{font-size:.6875rem;color:#6b7280;margin:0}.jsm-input,.jsm-textarea,.jsm-select{width:100%;border-radius:.5rem;border:1.5px solid #6b7280;background:Canvas;padding:.5rem .75rem;font-size:.875rem;color:CanvasText;outline:none;box-shadow:0 1px 2px #0000000a;transition:border-color .15s,box-shadow .15s;font-family:inherit;box-sizing:border-box}.jsm-input:hover,.jsm-textarea:hover,.jsm-select:hover{border-color:#4b5563}.jsm-input:focus,.jsm-textarea:focus,.jsm-select:focus{border-color:#6366f1;box-shadow:0 0 0 3px #6366f126}.jsm-input--error,.jsm-textarea--error,.jsm-select--error{border-color:#dc2626!important;background:#fff5f5}.jsm-input--error:focus,.jsm-textarea--error:focus,.jsm-select--error:focus{box-shadow:0 0 0 3px #f8717133!important}.jsm-textarea{resize:vertical;min-height:6rem}.jsm-select-wrap{position:relative}.jsm-select{appearance:none;padding-right:2.25rem;cursor:pointer}.jsm-select-chevron{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);width:1rem;height:1rem;color:#6b7280;pointer-events:none}.jsm-toggle-label{display:inline-flex;align-items:center;gap:.625rem;cursor:pointer;-webkit-user-select:none;user-select:none;min-height:1.5rem}.jsm-toggle-track{position:relative;display:inline-flex;align-items:center;width:2.25rem;height:1.25rem;border-radius:9999px;flex-shrink:0;background:#6b7280;transition:background .2s}.jsm-toggle-track.jsm-on{background:#4f46e5}.jsm-toggle-thumb{display:inline-block;width:1rem;height:1rem;border-radius:9999px;background:#fff;box-shadow:0 1px 3px #0003;transform:translate(.125rem);transition:transform .2s}.jsm-toggle-track.jsm-on .jsm-toggle-thumb{transform:translate(1.125rem)}.jsm-toggle-text{font-size:.875rem;font-weight:500;color:#374151}.jsm-btn-primary{display:inline-flex;align-items:center;gap:.25rem;padding:.375rem .75rem;border-radius:.5rem;font-size:.8125rem;font-weight:600;background:#4f46e5;color:#fff;border:none;cursor:pointer;box-shadow:0 1px 2px #4f46e54d;transition:background .15s,transform .1s,box-shadow .15s;white-space:nowrap;font-family:inherit}.jsm-btn-primary:hover{background:#4338ca;transform:translateY(-1px);box-shadow:0 3px 8px #4f46e54d}.jsm-btn-primary:active{transform:translateY(0)}.jsm-btn-primary:disabled{opacity:.45;cursor:not-allowed;transform:none}.jsm-btn-danger{display:inline-flex;align-items:center;gap:.25rem;padding:.25rem .5rem;border-radius:.375rem;font-size:.75rem;font-weight:600;background:transparent;color:#b91c1c;min-height:1.5rem;border:1.5px solid #fca5a5;cursor:pointer;transition:background .15s,border-color .15s;font-family:inherit}.jsm-btn-danger:hover{background:#fee2e2;border-color:#f87171}.jsm-error-list{display:flex;flex-direction:column;gap:.25rem;margin-top:.25rem}.jsm-error-msg{display:flex;align-items:center;gap:.375rem;font-size:.75rem;color:#dc2626;font-weight:500}.jsm-error-icon{width:.875rem;height:.875rem;flex-shrink:0}.jsm-badge-row{display:flex;flex-wrap:wrap;gap:.375rem}.jsm-badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .5rem;font-size:.6875rem;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.jsm-badge--deprecated{background:#fef3c7;color:#92400e}.jsm-badge--readonly{background:#f1f5f9;color:#475569}.jsm-badge--writeonly{background:#dbeafe;color:#1e40af}.jsm-badge--not{background:#fee2e2;color:#991b1b}.jsm-badge--nullable{background:#f3f4f6;color:#4b5563}.jsm-fieldset{border:1.5px solid #e5e7eb;border-radius:.875rem;padding:1.25rem;background:transparent;box-shadow:0 1px 3px #0000000f;transition:box-shadow .2s;box-sizing:border-box}.jsm-fieldset:focus-within{border-color:#c7d2fe;box-shadow:0 0 0 3px #6366f114,0 1px 3px #0000000f}.jsm-legend{display:inline-flex;align-items:center;gap:.375rem;padding:0 .5rem;font-size:.875rem;font-weight:700;color:#1f2937}.jsm-obj-grid{display:grid;grid-template-columns:1fr;gap:1rem}@media (min-width:640px){.jsm-obj-grid{grid-template-columns:repeat(2,1fr)}}.jsm-obj-complex{display:flex;flex-direction:column;gap:1rem;margin-top:1rem}.jsm-dynamic-key-header{display:flex;align-items:center;justify-content:space-between;padding:.25rem .5rem;background:#f9fafb;border-radius:.375rem;border:1px dashed #d1d5db;margin-bottom:.25rem}.jsm-dynamic-key-label{font-size:.75rem;font-weight:600;color:#6b7280;font-family:monospace}.jsm-add-prop{margin-top:1rem;padding:.875rem;background:#f9fafb;border-radius:.625rem;border:1.5px dashed #d1d5db;display:flex;flex-direction:column;gap:.5rem}.jsm-add-prop-label{font-size:.75rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.04em}.jsm-add-prop-row{display:flex;gap:.5rem;align-items:center}.jsm-input--sm{padding:.375rem .625rem;font-size:.8125rem}.jsm-array-block{display:flex;flex-direction:column;gap:.75rem}.jsm-array-header{display:flex;align-items:flex-start;justify-content:space-between;gap:.5rem}.jsm-array-title{font-size:.875rem;font-weight:700;color:#1f2937;display:flex;align-items:center;gap:.375rem}.jsm-array-items{display:flex;flex-direction:column;gap:.75rem}.jsm-array-item{border:1.5px solid #e5e7eb;border-radius:.75rem;padding:.875rem;background:transparent;box-shadow:0 1px 2px #0000000a;transition:border-color .15s,box-shadow .15s}.jsm-array-item:focus-within{border-color:#c7d2fe;box-shadow:0 0 0 3px #6366f114}.jsm-array-item-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem}.jsm-array-index{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:9999px;background:#eef2ff;color:#4f46e5;font-size:.6875rem;font-weight:700}.jsm-empty-array{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:2rem 1rem;border:1.5px dashed #e5e7eb;border-radius:.75rem;color:#6b7280;font-size:.8125rem;text-align:center}.jsm-contains-hint{display:flex;align-items:center;gap:.375rem;font-size:.75rem;color:#4f46e5;background:#eef2ff;border-radius:.375rem;padding:.375rem .625rem}.jsm-combinator{border:1.5px solid #e5e7eb;border-radius:.875rem;overflow:hidden;background:transparent;box-shadow:0 1px 3px #0000000f}.jsm-combinator-header{display:flex;align-items:flex-start;justify-content:space-between;padding:.875rem 1rem;background:#00000008;border-bottom:1.5px solid #e5e7eb}.jsm-combinator-title{font-size:.875rem;font-weight:700;color:#1f2937}.jsm-combinator-body{padding:1rem}.jsm-combinator-hint{font-size:.75rem;color:#6b7280;margin:.25rem 0 0}.jsm-oneof-tabs{display:flex;overflow-x:auto;border-bottom:1.5px solid #e5e7eb}.jsm-tab{padding:.5rem 1rem;font-size:.8125rem;font-weight:500;color:#6b7280;border:none;background:transparent;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1.5px;transition:color .15s,border-color .15s;white-space:nowrap;font-family:inherit}.jsm-tab:hover{color:#4f46e5}.jsm-tab--active{color:#4f46e5;border-bottom-color:#4f46e5;font-weight:600}.jsm-anyof-options{display:flex;flex-wrap:wrap;gap:.5rem;padding:.75rem 1rem;border-bottom:1.5px solid #e5e7eb}.jsm-anyof-option{display:inline-flex;align-items:center;gap:.375rem;padding:.375rem .75rem;border-radius:9999px;border:1.5px solid #6b7280;font-size:.8125rem;font-weight:500;color:#6b7280;cursor:pointer;transition:all .15s;background:transparent}.jsm-anyof-option:hover{border-color:#a5b4fc;color:#4f46e5}.jsm-anyof-option--active{border-color:#6366f1;background:#eef2ff;color:#4f46e5}.jsm-allof-section{border-left:3px solid #e0e7ff;padding-left:1rem}.jsm-combinator-badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .5rem;font-size:.6875rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0;margin-left:.75rem}.jsm-badge--oneof{background:#dbeafe;color:#1e40af}.jsm-badge--anyof{background:#ede9fe;color:#5b21b6}.jsm-badge--allof{background:#ccfbf1;color:#065f46}.jsm-input:focus-visible,.jsm-textarea:focus-visible,.jsm-select:focus-visible,.jsm-btn-primary:focus-visible,.jsm-btn-danger:focus-visible,.jsm-tab:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}.jsm-toggle-label:has(input:focus-visible) .jsm-toggle-track,.jsm-anyof-option:has(input:focus-visible){outline:2px solid #4f46e5;outline-offset:2px}.dark .jsm-field-label{color:#d1d5db}.dark .jsm-required{color:#f87171}.dark .jsm-example{color:#9ca3af}.dark .jsm-input:focus-visible,.dark .jsm-textarea:focus-visible,.dark .jsm-select:focus-visible,.dark .jsm-btn-primary:focus-visible,.dark .jsm-btn-danger:focus-visible,.dark .jsm-tab:focus-visible,.dark .jsm-toggle-label:has(input:focus-visible) .jsm-toggle-track,.dark .jsm-anyof-option:has(input:focus-visible){outline-color:#818cf8}.dark .jsm-btn-danger{color:#f87171;border-color:#f87171}.dark .jsm-btn-danger:hover{background:#f8717126}.dark .jsm-field-hint{color:#9ca3af}.dark .jsm-toggle-track{background:#64748b}.dark .jsm-toggle-track.jsm-on{background:#6366f1}.dark .jsm-toggle-text{color:#d1d5db}.dark .jsm-input,.dark .jsm-textarea,.dark .jsm-select{border-color:#64748b;color:#f1f5f9}.dark .jsm-input:focus,.dark .jsm-textarea:focus,.dark .jsm-select:focus{border-color:#6366f1;box-shadow:0 0 0 3px #6366f133}.dark .jsm-input--error,.dark .jsm-textarea--error,.dark .jsm-select--error{border-color:#f87171!important;background:#f871711a}.dark .jsm-fieldset{border-color:#334155}.dark .jsm-fieldset:focus-within{border-color:#6366f1}.dark .jsm-legend{color:#f1f5f9}.dark .jsm-combinator{border-color:#334155}.dark .jsm-combinator-header{background:#ffffff0a;border-color:#334155}.dark .jsm-combinator-title{color:#f1f5f9}.dark .jsm-oneof-tabs{border-color:#334155}.dark .jsm-tab{color:#94a3b8}.dark .jsm-tab--active{color:#818cf8;border-bottom-color:#818cf8}.dark .jsm-anyof-options{border-color:#334155}.dark .jsm-anyof-option{border-color:#64748b;color:#94a3b8;background:#0f172a}.dark .jsm-anyof-option--active{border-color:#6366f1;background:#6366f126;color:#818cf8}.dark .jsm-allof-section{border-left-color:#3730a3}.dark .jsm-array-item{border-color:#334155}.dark .jsm-array-item:focus-within{border-color:#6366f1}.dark .jsm-array-index{background:#6366f133;color:#818cf8}.dark .jsm-empty-array{border-color:#334155;color:#94a3b8}.dark .jsm-contains-hint{background:#6366f126;color:#818cf8}.dark .jsm-add-prop{background:#1e293b66;border-color:#475569}.dark .jsm-add-prop-label{color:#94a3b8}.dark .jsm-dynamic-key-header{background:#1e293b80;border-color:#475569}.dark .jsm-dynamic-key-label{color:#94a3b8}.dark .jsm-error-msg{color:#f87171}.dark .jsm-array-title{color:#f1f5f9}.dark .jsm-badge--deprecated{background:#d9770633;color:#fbbf24}.dark .jsm-badge--readonly{background:#1e293b;color:#94a3b8}.dark .jsm-badge--writeonly{background:#2563eb33;color:#60a5fa}.dark .jsm-badge--not{background:#dc262633;color:#f87171}.dark .jsm-badge--nullable{background:#1e293b;color:#94a3b8}.dark .jsm-select-chevron{color:#94a3b8}\n"], dependencies: [{ kind: "component", type: JsonSchemaNodeComponent, selector: "jsm-schema-node", inputs: ["schema", "control", "label", "required", "path", "errorsMap", "parent", "controlKey", "allowAdditionalProperties", "showErrors"], outputs: ["controlReplaced"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i4.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i4.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i4.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i4.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i5.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i5.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: i5.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i5.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i5.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i5.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i5.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i5.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i5.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: NumericInputDirective, selector: "input[jsmNumeric]", inputs: ["jsmDecimalSeparator"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1322
1463
  }
1323
1464
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: JsonSchemaNodeComponent, decorators: [{
1324
1465
  type: Component,
@@ -1334,14 +1475,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1334
1475
  </div>
1335
1476
  <span class="jsm-combinator-badge jsm-badge--oneof">oneOf</span>
1336
1477
  </div>
1337
- <div class="jsm-oneof-tabs">
1338
- <button *ngFor="let option of schema.oneOf; let i = index" type="button"
1478
+ <div class="jsm-oneof-tabs" role="tablist" [attr.aria-label]="schema.title || label || null" (keydown)="onOneOfKeydown($event)">
1479
+ <button *ngFor="let option of schema.oneOf; let i = index" type="button" role="tab"
1339
1480
  class="jsm-tab" [class.jsm-tab--active]="selectedOneOf === i"
1340
- (click)="selectedOneOf = i; onOneOfChange()">
1341
- {{ option.title || ('Option ' + (i + 1)) }}
1481
+ [id]="nodeId + '-tab-' + i" [attr.aria-selected]="selectedOneOf === i"
1482
+ [attr.aria-controls]="nodeId + '-panel'" [attr.tabindex]="selectedOneOf === i ? 0 : -1"
1483
+ (click)="selectOneOf(i)">
1484
+ {{ option.title || optionLabel(i) }}
1342
1485
  </button>
1343
1486
  </div>
1344
- <div class="jsm-combinator-body" *ngIf="activeVariant">
1487
+ <div class="jsm-combinator-body" *ngIf="activeVariant" role="tabpanel" [id]="nodeId + '-panel'"
1488
+ [attr.aria-labelledby]="nodeId + '-tab-' + selectedOneOf">
1345
1489
  <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
1346
1490
  [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
1347
1491
  [allowAdditionalProperties]="allowAdditionalProperties" [showErrors]="showErrors"
@@ -1358,11 +1502,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1358
1502
  </div>
1359
1503
  <span class="jsm-combinator-badge jsm-badge--anyof">anyOf</span>
1360
1504
  </div>
1361
- <div class="jsm-anyof-options">
1505
+ <div class="jsm-anyof-options" role="group" [attr.aria-label]="schema.title || label || null">
1362
1506
  <label *ngFor="let option of schema.anyOf; let i = index"
1363
1507
  class="jsm-anyof-option" [class.jsm-anyof-option--active]="selectedAnyOf.has(i)">
1364
1508
  <input type="checkbox" class="sr-only" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
1365
- {{ option.title || ('Option ' + (i + 1)) }}
1509
+ {{ option.title || optionLabel(i) }}
1366
1510
  </label>
1367
1511
  </div>
1368
1512
  <div class="jsm-combinator-body" *ngIf="activeVariant">
@@ -1385,7 +1529,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1385
1529
  <div class="jsm-combinator-body">
1386
1530
  <div *ngFor="let subSchema of mergedAllOf; let i = index" class="jsm-allof-section">
1387
1531
  <jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
1388
- [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
1532
+ [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || sectionLabel(i)" [required]="required"
1389
1533
  [allowAdditionalProperties]="allowAdditionalProperties" [showErrors]="showErrors"
1390
1534
  (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1391
1535
  </div>
@@ -1405,8 +1549,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1405
1549
  <ng-container *ngFor="let key of simpleKeys">
1406
1550
  <div style="display:flex;flex-direction:column;gap:.25rem;">
1407
1551
  <div *ngIf="isDynamicKey(key)" class="jsm-dynamic-key-header">
1408
- <span class="jsm-dynamic-key-label">{{ key }}</span>
1409
- <button type="button" class="jsm-btn-danger" (click)="removeDynamicKey(key)">Remove</button>
1552
+ <span class="jsm-dynamic-key-label" [id]="keyId(key)">{{ key }}</span>
1553
+ <button type="button" class="jsm-btn-danger" [id]="keyId(key) + '-remove'"
1554
+ [attr.aria-labelledby]="keyId(key) + '-remove ' + keyId(key)" (click)="removeDynamicKey(key)">{{ l.remove }}</button>
1410
1555
  </div>
1411
1556
  <jsm-schema-node [schema]="schemaForKey(key)" [control]="childControl(key)" [parent]="controlAsGroup"
1412
1557
  [controlKey]="key" [path]="pathForChild(key)" [errorsMap]="errorsMap" [label]="key"
@@ -1420,8 +1565,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1420
1565
  <ng-container *ngFor="let key of complexKeys">
1421
1566
  <div style="display:flex;flex-direction:column;gap:.25rem;">
1422
1567
  <div *ngIf="isDynamicKey(key)" class="jsm-dynamic-key-header">
1423
- <span class="jsm-dynamic-key-label">{{ key }}</span>
1424
- <button type="button" class="jsm-btn-danger" (click)="removeDynamicKey(key)">Remove</button>
1568
+ <span class="jsm-dynamic-key-label" [id]="keyId(key)">{{ key }}</span>
1569
+ <button type="button" class="jsm-btn-danger" [id]="keyId(key) + '-remove'"
1570
+ [attr.aria-labelledby]="keyId(key) + '-remove ' + keyId(key)" (click)="removeDynamicKey(key)">{{ l.remove }}</button>
1425
1571
  </div>
1426
1572
  <jsm-schema-node [schema]="schemaForKey(key)" [control]="childControl(key)" [parent]="controlAsGroup"
1427
1573
  [controlKey]="key" [path]="pathForChild(key)" [errorsMap]="errorsMap" [label]="key"
@@ -1431,45 +1577,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1431
1577
  </ng-container>
1432
1578
  </div>
1433
1579
 
1434
- <div *ngIf="canAddProperty" class="jsm-add-prop">
1435
- <p class="jsm-add-prop-label">Add property</p>
1580
+ <div *ngIf="canAddProperty" class="jsm-add-prop" role="group" [attr.aria-labelledby]="nodeId + '-addprop'">
1581
+ <p class="jsm-add-prop-label" [id]="nodeId + '-addprop'">{{ l.addProperty }}</p>
1436
1582
  <div class="jsm-add-prop-row">
1437
- <input class="jsm-input jsm-input--sm" style="flex:1;" [(ngModel)]="newPropertyKey" placeholder="propertyName" />
1438
- <button type="button" class="jsm-btn-primary" (click)="addDynamicProperty()">Add</button>
1583
+ <input class="jsm-input jsm-input--sm" style="flex:1;" [(ngModel)]="newPropertyKey" [ngModelOptions]="{standalone: true}"
1584
+ [attr.aria-label]="l.propertyName" [attr.placeholder]="l.propertyName"
1585
+ [attr.aria-invalid]="propertyError ? 'true' : null" [attr.aria-describedby]="propertyError ? nodeId + '-prop-error' : null" />
1586
+ <button type="button" class="jsm-btn-primary" (click)="addDynamicProperty()">{{ l.add }}</button>
1439
1587
  </div>
1440
- <p *ngIf="propertyError" class="jsm-error-msg">
1441
- <svg class="jsm-error-icon" 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>
1588
+ <p *ngIf="propertyError" class="jsm-error-msg" role="alert" [id]="nodeId + '-prop-error'">
1589
+ <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><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
1590
  {{ propertyError }}
1443
1591
  </p>
1444
1592
  </div>
1445
1593
 
1446
- <div *ngIf="errorsForPath.length" class="jsm-error-list" style="margin-top:.75rem;">
1594
+ <div *ngIf="errorsForPath.length" class="jsm-error-list" role="alert" style="margin-top:.75rem;">
1447
1595
  <p *ngFor="let error of errorsForPath" class="jsm-error-msg">
1448
- <svg class="jsm-error-icon" 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>
1449
- {{ error }}
1596
+ <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><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>
1597
+ {{ localize(error) }}
1450
1598
  </p>
1451
1599
  </div>
1452
1600
  </fieldset>
1453
1601
 
1454
1602
  <!-- ═══ array ═══ -->
1455
- <div *ngSwitchCase="'array'" class="jsm-array-block">
1603
+ <div *ngSwitchCase="'array'" class="jsm-array-block" role="group" [attr.aria-labelledby]="nodeId + '-title'">
1456
1604
  <div class="jsm-array-header">
1457
1605
  <div>
1458
- <p class="jsm-array-title">
1459
- {{ schema.title || label }}<span *ngIf="required" style="color:#ef4444;margin-left:.125rem;">*</span>
1606
+ <p class="jsm-array-title" [id]="nodeId + '-title'">
1607
+ {{ schema.title || label }}<span *ngIf="required" class="jsm-required" aria-hidden="true">*</span>
1460
1608
  </p>
1461
1609
  <p *ngIf="schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1462
1610
  </div>
1463
- <button type="button" class="jsm-btn-primary" (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1464
- <svg style="width:.875rem;height:.875rem;flex-shrink:0;" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1465
- Add item
1611
+ <button type="button" class="jsm-btn-primary" [id]="nodeId + '-add'"
1612
+ [attr.aria-labelledby]="nodeId + '-add ' + nodeId + '-title'" (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1613
+ <svg style="width:.875rem;height:.875rem;flex-shrink:0;" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
1614
+ {{ l.addItem }}
1466
1615
  </button>
1467
1616
  </div>
1468
1617
 
1469
1618
  <div *ngIf="effectiveSchema.contains" class="jsm-contains-hint">
1470
- Must contain matching item(s)
1471
- <span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
1472
- <span *ngIf="effectiveSchema.maxContains"> · max {{ effectiveSchema.maxContains }}</span>
1619
+ {{ l.mustContain }}
1620
+ <span *ngIf="effectiveSchema.minContains"> · {{ fmt(l.min, { n: effectiveSchema.minContains }) }}</span>
1621
+ <span *ngIf="effectiveSchema.maxContains"> · {{ fmt(l.max, { n: effectiveSchema.maxContains }) }}</span>
1473
1622
  </div>
1474
1623
 
1475
1624
  <div *ngIf="metaBadges.length" class="jsm-badge-row">
@@ -1477,10 +1626,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1477
1626
  </div>
1478
1627
 
1479
1628
  <div class="jsm-array-items" *ngIf="arrayControls.length > 0; else emptyArray">
1480
- <div *ngFor="let item of arrayControls; let i = index" class="jsm-array-item">
1629
+ <div *ngFor="let item of arrayControls; let i = index" class="jsm-array-item" role="group" [attr.aria-labelledby]="nodeId + '-item-' + i">
1481
1630
  <div class="jsm-array-item-header">
1482
- <span class="jsm-array-index">{{ i + 1 }}</span>
1483
- <button type="button" class="jsm-btn-danger" (click)="removeArrayItem(i)">Remove</button>
1631
+ <span class="jsm-array-index" aria-hidden="true">{{ i + 1 }}</span>
1632
+ <span class="sr-only" [id]="nodeId + '-item-' + i">{{ fmt(l.arrayItem, { n: i + 1 }) }}</span>
1633
+ <button type="button" class="jsm-btn-danger" [id]="nodeId + '-remove-' + i"
1634
+ [attr.aria-labelledby]="nodeId + '-remove-' + i + ' ' + nodeId + '-item-' + i" (click)="removeArrayItem(i)">{{ l.remove }}</button>
1484
1635
  </div>
1485
1636
  <jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
1486
1637
  [controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
@@ -1490,13 +1641,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1490
1641
  </div>
1491
1642
 
1492
1643
  <ng-template #emptyArray>
1493
- <div class="jsm-empty-array">No items yet — click "Add item" to start.</div>
1644
+ <div class="jsm-empty-array">{{ l.noItems }}</div>
1494
1645
  </ng-template>
1495
1646
 
1496
- <div *ngIf="errorsForPath.length" class="jsm-error-list">
1647
+ <div *ngIf="errorsForPath.length" class="jsm-error-list" role="alert">
1497
1648
  <p *ngFor="let error of errorsForPath" class="jsm-error-msg">
1498
- <svg class="jsm-error-icon" 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>
1499
- {{ error }}
1649
+ <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><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>
1650
+ {{ localize(error) }}
1500
1651
  </p>
1501
1652
  </div>
1502
1653
  </div>
@@ -1504,10 +1655,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1504
1655
  <!-- ═══ primitive ═══ -->
1505
1656
  <div *ngSwitchDefault class="jsm-field">
1506
1657
  <ng-container *ngIf="inputKind !== 'checkbox'">
1507
- <label class="jsm-field-label">
1508
- {{ schema.title || label }}<span *ngIf="required" class="jsm-required">*</span>
1658
+ <label class="jsm-field-label" [attr.for]="nodeId">
1659
+ {{ schema.title || label }}<span *ngIf="required" class="jsm-required" aria-hidden="true">*</span>
1509
1660
  </label>
1510
- <p *ngIf="schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1661
+ <p *ngIf="schema.description" class="jsm-field-hint" [id]="nodeId + '-hint'">{{ schema.description }}</p>
1511
1662
  </ng-container>
1512
1663
 
1513
1664
  <div *ngIf="metaBadges.length" class="jsm-badge-row">
@@ -1515,57 +1666,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1515
1666
  </div>
1516
1667
 
1517
1668
  <ng-container>
1518
- <input *ngIf="inputKind === 'number'" jsmNumeric type="text" inputmode="decimal" class="jsm-input"
1669
+ <input *ngIf="inputKind === 'number'" jsmNumeric type="text" inputmode="decimal" class="jsm-input" [id]="nodeId"
1519
1670
  [class.jsm-input--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1520
1671
  [jsmDecimalSeparator]="decimalSeparator" [attr.data-step]="numericStep"
1521
- [attr.placeholder]="schema.title || label"
1672
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1673
+ [attr.aria-describedby]="describedBy"
1522
1674
  [formControl]="controlAsFormControl"
1523
1675
  (blur)="onBlur()" />
1524
1676
 
1525
- <input *ngIf="inputKind === 'text'" class="jsm-input"
1677
+ <input *ngIf="inputKind === 'text'" class="jsm-input" [id]="nodeId"
1526
1678
  [class.jsm-input--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1527
- [attr.type]="inputType" [attr.placeholder]="schema.title || label"
1679
+ [attr.type]="inputType"
1680
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1681
+ [attr.aria-describedby]="describedBy"
1528
1682
  [formControl]="controlAsFormControl"
1529
1683
  (blur)="onBlur()" />
1530
1684
 
1531
- <textarea *ngIf="inputKind === 'textarea'" rows="4" class="jsm-textarea"
1685
+ <textarea *ngIf="inputKind === 'textarea'" rows="4" class="jsm-textarea" [id]="nodeId"
1532
1686
  [class.jsm-textarea--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1533
- [attr.placeholder]="schema.title || label"
1687
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1688
+ [attr.aria-describedby]="describedBy"
1534
1689
  [formControl]="controlAsFormControl"
1535
1690
  (blur)="onBlur()"></textarea>
1536
1691
 
1537
1692
  <div *ngIf="inputKind === 'select'" class="jsm-select-wrap">
1538
- <select class="jsm-select"
1693
+ <select class="jsm-select" [id]="nodeId"
1539
1694
  [class.jsm-select--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1695
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1696
+ [attr.aria-describedby]="describedBy"
1540
1697
  [formControl]="controlAsFormControl"
1541
1698
  (blur)="onBlur()">
1542
1699
  <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1543
1700
  </select>
1544
- <svg class="jsm-select-chevron" viewBox="0 0 16 16" fill="none">
1701
+ <svg class="jsm-select-chevron" viewBox="0 0 16 16" fill="none" aria-hidden="true">
1545
1702
  <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1546
1703
  </svg>
1547
1704
  </div>
1548
1705
 
1549
1706
  <label *ngIf="inputKind === 'checkbox'" class="jsm-toggle-label">
1550
1707
  <span class="jsm-toggle-track" [class.jsm-on]="controlAsFormControl.value">
1551
- <input type="checkbox" class="sr-only" [formControl]="controlAsFormControl" />
1708
+ <input type="checkbox" class="sr-only" [id]="nodeId" [formControl]="controlAsFormControl"
1709
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1710
+ [attr.aria-describedby]="describedBy" (blur)="onBlur()" />
1552
1711
  <span class="jsm-toggle-thumb"></span>
1553
1712
  </span>
1554
1713
  <span class="jsm-toggle-text">
1555
- {{ schema.title || label }}<span *ngIf="required" class="jsm-required">*</span>
1714
+ {{ schema.title || label }}<span *ngIf="required" class="jsm-required" aria-hidden="true">*</span>
1556
1715
  </span>
1557
1716
  </label>
1558
1717
  </ng-container>
1559
1718
 
1560
- <p *ngIf="inputKind === 'checkbox' && schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1561
- <p *ngIf="effectiveSchema.examples?.length" class="jsm-example">e.g. {{ effectiveSchema.examples?.[0] }}</p>
1562
- <p *ngIf="effectiveSchema.contentMediaType" class="jsm-example">
1719
+ <p *ngIf="inputKind === 'checkbox' && schema.description" class="jsm-field-hint" [id]="nodeId + '-hint'">{{ schema.description }}</p>
1720
+ <p *ngIf="effectiveSchema.examples?.length" class="jsm-example" [id]="nodeId + '-example'">{{ l.example }} {{ effectiveSchema.examples?.[0] }}</p>
1721
+ <p *ngIf="effectiveSchema.contentMediaType" class="jsm-example" [id]="nodeId + '-media'">
1563
1722
  {{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
1564
1723
  </p>
1565
1724
 
1566
- <div *ngIf="allErrors.length && (controlAsFormControl.dirty || isTouched || showErrors)" class="jsm-error-list">
1725
+ <div *ngIf="visibleError" class="jsm-error-list" role="alert" [id]="nodeId + '-error'">
1567
1726
  <p *ngFor="let error of allErrors" class="jsm-error-msg">
1568
- <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none">
1727
+ <svg class="jsm-error-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
1569
1728
  <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
1570
1729
  <path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1571
1730
  </svg>
@@ -1575,7 +1734,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1575
1734
  </div>
1576
1735
 
1577
1736
  </ng-container>
1578
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".jsm-field{display:flex;flex-direction:column;gap:.375rem}.jsm-field-label{display:block;font-size:.8125rem;font-weight:600;color:#374151}.jsm-field-hint{font-size:.75rem;color:#6b7280;margin:0;line-height:1.4}.jsm-required{color:#ef4444;margin-left:.125rem}.jsm-example{font-size:.6875rem;color:#9ca3af}.jsm-input,.jsm-textarea,.jsm-select{width:100%;border-radius:.5rem;border:1.5px solid #d1d5db;background:Canvas;padding:.5rem .75rem;font-size:.875rem;color:CanvasText;outline:none;box-shadow:0 1px 2px #0000000a;transition:border-color .15s,box-shadow .15s;font-family:inherit;box-sizing:border-box}.jsm-input:hover,.jsm-textarea:hover,.jsm-select:hover{border-color:#9ca3af}.jsm-input:focus,.jsm-textarea:focus,.jsm-select:focus{border-color:#6366f1;box-shadow:0 0 0 3px #6366f126}.jsm-input--error,.jsm-textarea--error,.jsm-select--error{border-color:#f87171!important;background:#fff5f5}.jsm-input--error:focus,.jsm-textarea--error:focus,.jsm-select--error:focus{box-shadow:0 0 0 3px #f8717133!important}.jsm-textarea{resize:vertical;min-height:6rem}.jsm-select-wrap{position:relative}.jsm-select{appearance:none;padding-right:2.25rem;cursor:pointer}.jsm-select-chevron{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);width:1rem;height:1rem;color:#9ca3af;pointer-events:none}.jsm-toggle-label{display:inline-flex;align-items:center;gap:.625rem;cursor:pointer;-webkit-user-select:none;user-select:none}.jsm-toggle-track{position:relative;display:inline-flex;align-items:center;width:2.25rem;height:1.25rem;border-radius:9999px;flex-shrink:0;background:#d1d5db;transition:background .2s}.jsm-toggle-track.jsm-on{background:#4f46e5}.jsm-toggle-thumb{display:inline-block;width:1rem;height:1rem;border-radius:9999px;background:#fff;box-shadow:0 1px 3px #0003;transform:translate(.125rem);transition:transform .2s}.jsm-toggle-track.jsm-on .jsm-toggle-thumb{transform:translate(1.125rem)}.jsm-toggle-text{font-size:.875rem;font-weight:500;color:#374151}.jsm-btn-primary{display:inline-flex;align-items:center;gap:.25rem;padding:.375rem .75rem;border-radius:.5rem;font-size:.8125rem;font-weight:600;background:#4f46e5;color:#fff;border:none;cursor:pointer;box-shadow:0 1px 2px #4f46e54d;transition:background .15s,transform .1s,box-shadow .15s;white-space:nowrap;font-family:inherit}.jsm-btn-primary:hover{background:#4338ca;transform:translateY(-1px);box-shadow:0 3px 8px #4f46e54d}.jsm-btn-primary:active{transform:translateY(0)}.jsm-btn-primary:disabled{opacity:.45;cursor:not-allowed;transform:none}.jsm-btn-danger{display:inline-flex;align-items:center;gap:.25rem;padding:.25rem .5rem;border-radius:.375rem;font-size:.75rem;font-weight:600;background:transparent;color:#dc2626;border:1.5px solid #fca5a5;cursor:pointer;transition:background .15s,border-color .15s;font-family:inherit}.jsm-btn-danger:hover{background:#fee2e2;border-color:#f87171}.jsm-error-list{display:flex;flex-direction:column;gap:.25rem;margin-top:.25rem}.jsm-error-msg{display:flex;align-items:center;gap:.375rem;font-size:.75rem;color:#dc2626;font-weight:500}.jsm-error-icon{width:.875rem;height:.875rem;flex-shrink:0}.jsm-badge-row{display:flex;flex-wrap:wrap;gap:.375rem}.jsm-badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .5rem;font-size:.6875rem;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.jsm-badge--deprecated{background:#fef3c7;color:#92400e}.jsm-badge--readonly{background:#f1f5f9;color:#475569}.jsm-badge--writeonly{background:#dbeafe;color:#1e40af}.jsm-badge--not{background:#fee2e2;color:#991b1b}.jsm-badge--nullable{background:#f3f4f6;color:#6b7280}.jsm-fieldset{border:1.5px solid #e5e7eb;border-radius:.875rem;padding:1.25rem;background:transparent;box-shadow:0 1px 3px #0000000f;transition:box-shadow .2s;box-sizing:border-box}.jsm-fieldset:focus-within{border-color:#c7d2fe;box-shadow:0 0 0 3px #6366f114,0 1px 3px #0000000f}.jsm-legend{display:inline-flex;align-items:center;gap:.375rem;padding:0 .5rem;font-size:.875rem;font-weight:700;color:#1f2937}.jsm-obj-grid{display:grid;grid-template-columns:1fr;gap:1rem}@media (min-width:640px){.jsm-obj-grid{grid-template-columns:repeat(2,1fr)}}.jsm-obj-complex{display:flex;flex-direction:column;gap:1rem;margin-top:1rem}.jsm-dynamic-key-header{display:flex;align-items:center;justify-content:space-between;padding:.25rem .5rem;background:#f9fafb;border-radius:.375rem;border:1px dashed #d1d5db;margin-bottom:.25rem}.jsm-dynamic-key-label{font-size:.75rem;font-weight:600;color:#6b7280;font-family:monospace}.jsm-add-prop{margin-top:1rem;padding:.875rem;background:#f9fafb;border-radius:.625rem;border:1.5px dashed #d1d5db;display:flex;flex-direction:column;gap:.5rem}.jsm-add-prop-label{font-size:.75rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.04em}.jsm-add-prop-row{display:flex;gap:.5rem;align-items:center}.jsm-input--sm{padding:.375rem .625rem;font-size:.8125rem}.jsm-array-block{display:flex;flex-direction:column;gap:.75rem}.jsm-array-header{display:flex;align-items:flex-start;justify-content:space-between;gap:.5rem}.jsm-array-title{font-size:.875rem;font-weight:700;color:#1f2937;display:flex;align-items:center;gap:.375rem}.jsm-array-items{display:flex;flex-direction:column;gap:.75rem}.jsm-array-item{border:1.5px solid #e5e7eb;border-radius:.75rem;padding:.875rem;background:transparent;box-shadow:0 1px 2px #0000000a;transition:border-color .15s,box-shadow .15s}.jsm-array-item:focus-within{border-color:#c7d2fe;box-shadow:0 0 0 3px #6366f114}.jsm-array-item-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem}.jsm-array-index{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:9999px;background:#eef2ff;color:#4f46e5;font-size:.6875rem;font-weight:700}.jsm-empty-array{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:2rem 1rem;border:1.5px dashed #e5e7eb;border-radius:.75rem;color:#9ca3af;font-size:.8125rem;text-align:center}.jsm-contains-hint{display:flex;align-items:center;gap:.375rem;font-size:.75rem;color:#6366f1;background:#eef2ff;border-radius:.375rem;padding:.375rem .625rem}.jsm-combinator{border:1.5px solid #e5e7eb;border-radius:.875rem;overflow:hidden;background:transparent;box-shadow:0 1px 3px #0000000f}.jsm-combinator-header{display:flex;align-items:flex-start;justify-content:space-between;padding:.875rem 1rem;background:#00000008;border-bottom:1.5px solid #e5e7eb}.jsm-combinator-title{font-size:.875rem;font-weight:700;color:#1f2937}.jsm-combinator-body{padding:1rem}.jsm-combinator-hint{font-size:.75rem;color:#6b7280;margin:.25rem 0 0}.jsm-oneof-tabs{display:flex;overflow-x:auto;border-bottom:1.5px solid #e5e7eb}.jsm-tab{padding:.5rem 1rem;font-size:.8125rem;font-weight:500;color:#6b7280;border:none;background:transparent;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1.5px;transition:color .15s,border-color .15s;white-space:nowrap;font-family:inherit}.jsm-tab:hover{color:#4f46e5}.jsm-tab--active{color:#4f46e5;border-bottom-color:#4f46e5;font-weight:600}.jsm-anyof-options{display:flex;flex-wrap:wrap;gap:.5rem;padding:.75rem 1rem;border-bottom:1.5px solid #e5e7eb}.jsm-anyof-option{display:inline-flex;align-items:center;gap:.375rem;padding:.375rem .75rem;border-radius:9999px;border:1.5px solid #e5e7eb;font-size:.8125rem;font-weight:500;color:#6b7280;cursor:pointer;transition:all .15s;background:transparent}.jsm-anyof-option:hover{border-color:#a5b4fc;color:#4f46e5}.jsm-anyof-option--active{border-color:#6366f1;background:#eef2ff;color:#4f46e5}.jsm-allof-section{border-left:3px solid #e0e7ff;padding-left:1rem}.jsm-combinator-badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .5rem;font-size:.6875rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0;margin-left:.75rem}.jsm-badge--oneof{background:#dbeafe;color:#1e40af}.jsm-badge--anyof{background:#ede9fe;color:#5b21b6}.jsm-badge--allof{background:#ccfbf1;color:#065f46}.dark .jsm-field-label{color:#d1d5db}.dark .jsm-field-hint{color:#9ca3af}.dark .jsm-toggle-track{background:#475569}.dark .jsm-toggle-track.jsm-on{background:#4f46e5}.dark .jsm-toggle-text{color:#d1d5db}.dark .jsm-input,.dark .jsm-textarea,.dark .jsm-select{border-color:#475569;color:#f1f5f9}.dark .jsm-input:focus,.dark .jsm-textarea:focus,.dark .jsm-select:focus{border-color:#6366f1;box-shadow:0 0 0 3px #6366f133}.dark .jsm-input--error,.dark .jsm-textarea--error,.dark .jsm-select--error{border-color:#f87171!important;background:#f871711a}.dark .jsm-fieldset{border-color:#334155}.dark .jsm-fieldset:focus-within{border-color:#6366f1}.dark .jsm-legend{color:#f1f5f9}.dark .jsm-combinator{border-color:#334155}.dark .jsm-combinator-header{background:#ffffff0a;border-color:#334155}.dark .jsm-combinator-title{color:#f1f5f9}.dark .jsm-oneof-tabs{border-color:#334155}.dark .jsm-tab{color:#94a3b8}.dark .jsm-tab--active{color:#818cf8;border-bottom-color:#818cf8}.dark .jsm-anyof-options{border-color:#334155}.dark .jsm-anyof-option{border-color:#334155;color:#94a3b8;background:#0f172a}.dark .jsm-anyof-option--active{border-color:#6366f1;background:#6366f126;color:#818cf8}.dark .jsm-allof-section{border-left-color:#3730a3}.dark .jsm-array-item{border-color:#334155}.dark .jsm-array-item:focus-within{border-color:#6366f1}.dark .jsm-array-index{background:#6366f133;color:#818cf8}.dark .jsm-empty-array{border-color:#334155;color:#64748b}.dark .jsm-contains-hint{background:#6366f126;color:#818cf8}.dark .jsm-add-prop{background:#1e293b66;border-color:#475569}.dark .jsm-add-prop-label{color:#64748b}.dark .jsm-dynamic-key-header{background:#1e293b80;border-color:#475569}.dark .jsm-dynamic-key-label{color:#94a3b8}.dark .jsm-error-msg{color:#f87171}.dark .jsm-array-title{color:#f1f5f9}.dark .jsm-badge--deprecated{background:#d9770633;color:#fbbf24}.dark .jsm-badge--readonly{background:#1e293b;color:#94a3b8}.dark .jsm-badge--writeonly{background:#2563eb33;color:#60a5fa}.dark .jsm-badge--not{background:#dc262633;color:#f87171}.dark .jsm-badge--nullable{background:#1e293b;color:#94a3b8}.dark .jsm-select-chevron{color:#64748b}\n"] }]
1737
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".jsm-field{display:flex;flex-direction:column;gap:.375rem}.jsm-field-label{display:block;font-size:.8125rem;font-weight:600;color:#374151}.jsm-field-hint{font-size:.75rem;color:#6b7280;margin:0;line-height:1.4}.jsm-required{color:#dc2626;margin-left:.125rem}.jsm-example{font-size:.6875rem;color:#6b7280;margin:0}.jsm-input,.jsm-textarea,.jsm-select{width:100%;border-radius:.5rem;border:1.5px solid #6b7280;background:Canvas;padding:.5rem .75rem;font-size:.875rem;color:CanvasText;outline:none;box-shadow:0 1px 2px #0000000a;transition:border-color .15s,box-shadow .15s;font-family:inherit;box-sizing:border-box}.jsm-input:hover,.jsm-textarea:hover,.jsm-select:hover{border-color:#4b5563}.jsm-input:focus,.jsm-textarea:focus,.jsm-select:focus{border-color:#6366f1;box-shadow:0 0 0 3px #6366f126}.jsm-input--error,.jsm-textarea--error,.jsm-select--error{border-color:#dc2626!important;background:#fff5f5}.jsm-input--error:focus,.jsm-textarea--error:focus,.jsm-select--error:focus{box-shadow:0 0 0 3px #f8717133!important}.jsm-textarea{resize:vertical;min-height:6rem}.jsm-select-wrap{position:relative}.jsm-select{appearance:none;padding-right:2.25rem;cursor:pointer}.jsm-select-chevron{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);width:1rem;height:1rem;color:#6b7280;pointer-events:none}.jsm-toggle-label{display:inline-flex;align-items:center;gap:.625rem;cursor:pointer;-webkit-user-select:none;user-select:none;min-height:1.5rem}.jsm-toggle-track{position:relative;display:inline-flex;align-items:center;width:2.25rem;height:1.25rem;border-radius:9999px;flex-shrink:0;background:#6b7280;transition:background .2s}.jsm-toggle-track.jsm-on{background:#4f46e5}.jsm-toggle-thumb{display:inline-block;width:1rem;height:1rem;border-radius:9999px;background:#fff;box-shadow:0 1px 3px #0003;transform:translate(.125rem);transition:transform .2s}.jsm-toggle-track.jsm-on .jsm-toggle-thumb{transform:translate(1.125rem)}.jsm-toggle-text{font-size:.875rem;font-weight:500;color:#374151}.jsm-btn-primary{display:inline-flex;align-items:center;gap:.25rem;padding:.375rem .75rem;border-radius:.5rem;font-size:.8125rem;font-weight:600;background:#4f46e5;color:#fff;border:none;cursor:pointer;box-shadow:0 1px 2px #4f46e54d;transition:background .15s,transform .1s,box-shadow .15s;white-space:nowrap;font-family:inherit}.jsm-btn-primary:hover{background:#4338ca;transform:translateY(-1px);box-shadow:0 3px 8px #4f46e54d}.jsm-btn-primary:active{transform:translateY(0)}.jsm-btn-primary:disabled{opacity:.45;cursor:not-allowed;transform:none}.jsm-btn-danger{display:inline-flex;align-items:center;gap:.25rem;padding:.25rem .5rem;border-radius:.375rem;font-size:.75rem;font-weight:600;background:transparent;color:#b91c1c;min-height:1.5rem;border:1.5px solid #fca5a5;cursor:pointer;transition:background .15s,border-color .15s;font-family:inherit}.jsm-btn-danger:hover{background:#fee2e2;border-color:#f87171}.jsm-error-list{display:flex;flex-direction:column;gap:.25rem;margin-top:.25rem}.jsm-error-msg{display:flex;align-items:center;gap:.375rem;font-size:.75rem;color:#dc2626;font-weight:500}.jsm-error-icon{width:.875rem;height:.875rem;flex-shrink:0}.jsm-badge-row{display:flex;flex-wrap:wrap;gap:.375rem}.jsm-badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .5rem;font-size:.6875rem;font-weight:600;letter-spacing:.02em;text-transform:uppercase}.jsm-badge--deprecated{background:#fef3c7;color:#92400e}.jsm-badge--readonly{background:#f1f5f9;color:#475569}.jsm-badge--writeonly{background:#dbeafe;color:#1e40af}.jsm-badge--not{background:#fee2e2;color:#991b1b}.jsm-badge--nullable{background:#f3f4f6;color:#4b5563}.jsm-fieldset{border:1.5px solid #e5e7eb;border-radius:.875rem;padding:1.25rem;background:transparent;box-shadow:0 1px 3px #0000000f;transition:box-shadow .2s;box-sizing:border-box}.jsm-fieldset:focus-within{border-color:#c7d2fe;box-shadow:0 0 0 3px #6366f114,0 1px 3px #0000000f}.jsm-legend{display:inline-flex;align-items:center;gap:.375rem;padding:0 .5rem;font-size:.875rem;font-weight:700;color:#1f2937}.jsm-obj-grid{display:grid;grid-template-columns:1fr;gap:1rem}@media (min-width:640px){.jsm-obj-grid{grid-template-columns:repeat(2,1fr)}}.jsm-obj-complex{display:flex;flex-direction:column;gap:1rem;margin-top:1rem}.jsm-dynamic-key-header{display:flex;align-items:center;justify-content:space-between;padding:.25rem .5rem;background:#f9fafb;border-radius:.375rem;border:1px dashed #d1d5db;margin-bottom:.25rem}.jsm-dynamic-key-label{font-size:.75rem;font-weight:600;color:#6b7280;font-family:monospace}.jsm-add-prop{margin-top:1rem;padding:.875rem;background:#f9fafb;border-radius:.625rem;border:1.5px dashed #d1d5db;display:flex;flex-direction:column;gap:.5rem}.jsm-add-prop-label{font-size:.75rem;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.04em}.jsm-add-prop-row{display:flex;gap:.5rem;align-items:center}.jsm-input--sm{padding:.375rem .625rem;font-size:.8125rem}.jsm-array-block{display:flex;flex-direction:column;gap:.75rem}.jsm-array-header{display:flex;align-items:flex-start;justify-content:space-between;gap:.5rem}.jsm-array-title{font-size:.875rem;font-weight:700;color:#1f2937;display:flex;align-items:center;gap:.375rem}.jsm-array-items{display:flex;flex-direction:column;gap:.75rem}.jsm-array-item{border:1.5px solid #e5e7eb;border-radius:.75rem;padding:.875rem;background:transparent;box-shadow:0 1px 2px #0000000a;transition:border-color .15s,box-shadow .15s}.jsm-array-item:focus-within{border-color:#c7d2fe;box-shadow:0 0 0 3px #6366f114}.jsm-array-item-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem}.jsm-array-index{display:inline-flex;align-items:center;justify-content:center;width:1.5rem;height:1.5rem;border-radius:9999px;background:#eef2ff;color:#4f46e5;font-size:.6875rem;font-weight:700}.jsm-empty-array{display:flex;flex-direction:column;align-items:center;gap:.5rem;padding:2rem 1rem;border:1.5px dashed #e5e7eb;border-radius:.75rem;color:#6b7280;font-size:.8125rem;text-align:center}.jsm-contains-hint{display:flex;align-items:center;gap:.375rem;font-size:.75rem;color:#4f46e5;background:#eef2ff;border-radius:.375rem;padding:.375rem .625rem}.jsm-combinator{border:1.5px solid #e5e7eb;border-radius:.875rem;overflow:hidden;background:transparent;box-shadow:0 1px 3px #0000000f}.jsm-combinator-header{display:flex;align-items:flex-start;justify-content:space-between;padding:.875rem 1rem;background:#00000008;border-bottom:1.5px solid #e5e7eb}.jsm-combinator-title{font-size:.875rem;font-weight:700;color:#1f2937}.jsm-combinator-body{padding:1rem}.jsm-combinator-hint{font-size:.75rem;color:#6b7280;margin:.25rem 0 0}.jsm-oneof-tabs{display:flex;overflow-x:auto;border-bottom:1.5px solid #e5e7eb}.jsm-tab{padding:.5rem 1rem;font-size:.8125rem;font-weight:500;color:#6b7280;border:none;background:transparent;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1.5px;transition:color .15s,border-color .15s;white-space:nowrap;font-family:inherit}.jsm-tab:hover{color:#4f46e5}.jsm-tab--active{color:#4f46e5;border-bottom-color:#4f46e5;font-weight:600}.jsm-anyof-options{display:flex;flex-wrap:wrap;gap:.5rem;padding:.75rem 1rem;border-bottom:1.5px solid #e5e7eb}.jsm-anyof-option{display:inline-flex;align-items:center;gap:.375rem;padding:.375rem .75rem;border-radius:9999px;border:1.5px solid #6b7280;font-size:.8125rem;font-weight:500;color:#6b7280;cursor:pointer;transition:all .15s;background:transparent}.jsm-anyof-option:hover{border-color:#a5b4fc;color:#4f46e5}.jsm-anyof-option--active{border-color:#6366f1;background:#eef2ff;color:#4f46e5}.jsm-allof-section{border-left:3px solid #e0e7ff;padding-left:1rem}.jsm-combinator-badge{display:inline-flex;align-items:center;border-radius:9999px;padding:.125rem .5rem;font-size:.6875rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0;margin-left:.75rem}.jsm-badge--oneof{background:#dbeafe;color:#1e40af}.jsm-badge--anyof{background:#ede9fe;color:#5b21b6}.jsm-badge--allof{background:#ccfbf1;color:#065f46}.jsm-input:focus-visible,.jsm-textarea:focus-visible,.jsm-select:focus-visible,.jsm-btn-primary:focus-visible,.jsm-btn-danger:focus-visible,.jsm-tab:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}.jsm-toggle-label:has(input:focus-visible) .jsm-toggle-track,.jsm-anyof-option:has(input:focus-visible){outline:2px solid #4f46e5;outline-offset:2px}.dark .jsm-field-label{color:#d1d5db}.dark .jsm-required{color:#f87171}.dark .jsm-example{color:#9ca3af}.dark .jsm-input:focus-visible,.dark .jsm-textarea:focus-visible,.dark .jsm-select:focus-visible,.dark .jsm-btn-primary:focus-visible,.dark .jsm-btn-danger:focus-visible,.dark .jsm-tab:focus-visible,.dark .jsm-toggle-label:has(input:focus-visible) .jsm-toggle-track,.dark .jsm-anyof-option:has(input:focus-visible){outline-color:#818cf8}.dark .jsm-btn-danger{color:#f87171;border-color:#f87171}.dark .jsm-btn-danger:hover{background:#f8717126}.dark .jsm-field-hint{color:#9ca3af}.dark .jsm-toggle-track{background:#64748b}.dark .jsm-toggle-track.jsm-on{background:#6366f1}.dark .jsm-toggle-text{color:#d1d5db}.dark .jsm-input,.dark .jsm-textarea,.dark .jsm-select{border-color:#64748b;color:#f1f5f9}.dark .jsm-input:focus,.dark .jsm-textarea:focus,.dark .jsm-select:focus{border-color:#6366f1;box-shadow:0 0 0 3px #6366f133}.dark .jsm-input--error,.dark .jsm-textarea--error,.dark .jsm-select--error{border-color:#f87171!important;background:#f871711a}.dark .jsm-fieldset{border-color:#334155}.dark .jsm-fieldset:focus-within{border-color:#6366f1}.dark .jsm-legend{color:#f1f5f9}.dark .jsm-combinator{border-color:#334155}.dark .jsm-combinator-header{background:#ffffff0a;border-color:#334155}.dark .jsm-combinator-title{color:#f1f5f9}.dark .jsm-oneof-tabs{border-color:#334155}.dark .jsm-tab{color:#94a3b8}.dark .jsm-tab--active{color:#818cf8;border-bottom-color:#818cf8}.dark .jsm-anyof-options{border-color:#334155}.dark .jsm-anyof-option{border-color:#64748b;color:#94a3b8;background:#0f172a}.dark .jsm-anyof-option--active{border-color:#6366f1;background:#6366f126;color:#818cf8}.dark .jsm-allof-section{border-left-color:#3730a3}.dark .jsm-array-item{border-color:#334155}.dark .jsm-array-item:focus-within{border-color:#6366f1}.dark .jsm-array-index{background:#6366f133;color:#818cf8}.dark .jsm-empty-array{border-color:#334155;color:#94a3b8}.dark .jsm-contains-hint{background:#6366f126;color:#818cf8}.dark .jsm-add-prop{background:#1e293b66;border-color:#475569}.dark .jsm-add-prop-label{color:#94a3b8}.dark .jsm-dynamic-key-header{background:#1e293b80;border-color:#475569}.dark .jsm-dynamic-key-label{color:#94a3b8}.dark .jsm-error-msg{color:#f87171}.dark .jsm-array-title{color:#f1f5f9}.dark .jsm-badge--deprecated{background:#d9770633;color:#fbbf24}.dark .jsm-badge--readonly{background:#1e293b;color:#94a3b8}.dark .jsm-badge--writeonly{background:#2563eb33;color:#60a5fa}.dark .jsm-badge--not{background:#dc262633;color:#f87171}.dark .jsm-badge--nullable{background:#1e293b;color:#94a3b8}.dark .jsm-select-chevron{color:#94a3b8}\n"] }]
1579
1738
  }], ctorParameters: () => [{ type: JsonSchemaFormService }, { type: JsonSchemaValidationService }, { type: i0.ChangeDetectorRef }, { type: JsonSchemaFormOptions, decorators: [{
1580
1739
  type: Optional
1581
1740
  }] }], propDecorators: { schema: [{
@@ -1788,6 +1947,8 @@ class JsonSchemaFormComponent {
1788
1947
  * either way); the emitted value is always a JSON number.
1789
1948
  */
1790
1949
  decimalSeparator = '.';
1950
+ /** Texts shown by the form (translate them here); omitted keys keep the English default. */
1951
+ labels = {};
1791
1952
  formReady = new EventEmitter();
1792
1953
  valueChange = new EventEmitter();
1793
1954
  schemaReady = new EventEmitter();
@@ -1815,6 +1976,9 @@ class JsonSchemaFormComponent {
1815
1976
  if (changes['decimalSeparator']) {
1816
1977
  this.options.decimalSeparator = this.decimalSeparator;
1817
1978
  }
1979
+ if (changes['labels']) {
1980
+ this.options.labels = { ...DEFAULT_LABELS, ...this.labels };
1981
+ }
1818
1982
  if (changes['schema']) {
1819
1983
  // Schema changed — full rebuild required
1820
1984
  void this.buildForm();
@@ -1913,20 +2077,20 @@ class JsonSchemaFormComponent {
1913
2077
  });
1914
2078
  }
1915
2079
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: JsonSchemaFormComponent, deps: [{ token: JsonSchemaFormService }, { token: JsonSchemaResolverService }, { token: JsonSchemaValidationService }, { token: i0.ChangeDetectorRef }, { token: JsonSchemaFormOptions }, { token: JsonSchemaStylesService }], target: i0.ɵɵFactoryTarget.Component });
1916
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: JsonSchemaFormComponent, isStandalone: true, selector: "jsm-json-schema-form", inputs: { schema: "schema", value: "value", data: "data", allowAdditionalProperties: "allowAdditionalProperties", decimalSeparator: "decimalSeparator" }, outputs: { formReady: "formReady", valueChange: "valueChange", schemaReady: "schemaReady", schemaErrors: "schemaErrors" }, providers: [JsonSchemaFormOptions], usesOnChanges: true, ngImport: i0, template: `
2080
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: JsonSchemaFormComponent, isStandalone: true, selector: "jsm-json-schema-form", inputs: { schema: "schema", value: "value", data: "data", allowAdditionalProperties: "allowAdditionalProperties", decimalSeparator: "decimalSeparator", labels: "labels" }, outputs: { formReady: "formReady", valueChange: "valueChange", schemaReady: "schemaReady", schemaErrors: "schemaErrors" }, providers: [JsonSchemaFormOptions], usesOnChanges: true, ngImport: i0, template: `
1917
2081
  <div class="jsm-root">
1918
2082
  <form class="w-full space-y-5">
1919
2083
  <div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
1920
- <h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || 'Form' }}</h2>
2084
+ <h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || options.labels.formTitle }}</h2>
1921
2085
  <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
1922
2086
  </div>
1923
2087
 
1924
- <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
1925
- <svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none">
2088
+ <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" role="status">
2089
+ <svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none" aria-hidden="true">
1926
2090
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
1927
2091
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
1928
2092
  </svg>
1929
- Resolving schema&hellip;
2093
+ {{ options.labels.resolving }}
1930
2094
  </div>
1931
2095
 
1932
2096
  <!-- schema structural errors panel -->
@@ -1934,16 +2098,16 @@ class JsonSchemaFormComponent {
1934
2098
  class="rounded-xl border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4 space-y-3"
1935
2099
  role="alert">
1936
2100
  <div class="flex items-center gap-2">
1937
- <svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none">
2101
+ <svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none" aria-hidden="true">
1938
2102
  <path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
1939
2103
  <path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1940
2104
  </svg>
1941
2105
  <span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
1942
- Schema errors ({{ schemaErrorList.length }})
2106
+ {{ options.labels.schemaErrorsTitle }} ({{ schemaErrorList.length }})
1943
2107
  </span>
1944
2108
  </div>
1945
2109
  <p class="text-xs text-amber-700 dark:text-amber-400">
1946
- The schema has structural issues. Fix them to get a valid form.
2110
+ {{ options.labels.schemaErrorsHint }}
1947
2111
  </p>
1948
2112
  <div class="space-y-2">
1949
2113
  <div *ngFor="let err of schemaErrorList"
@@ -1977,16 +2141,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1977
2141
  <div class="jsm-root">
1978
2142
  <form class="w-full space-y-5">
1979
2143
  <div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
1980
- <h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || 'Form' }}</h2>
2144
+ <h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || options.labels.formTitle }}</h2>
1981
2145
  <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
1982
2146
  </div>
1983
2147
 
1984
- <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
1985
- <svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none">
2148
+ <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" role="status">
2149
+ <svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none" aria-hidden="true">
1986
2150
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
1987
2151
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
1988
2152
  </svg>
1989
- Resolving schema&hellip;
2153
+ {{ options.labels.resolving }}
1990
2154
  </div>
1991
2155
 
1992
2156
  <!-- schema structural errors panel -->
@@ -1994,16 +2158,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1994
2158
  class="rounded-xl border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4 space-y-3"
1995
2159
  role="alert">
1996
2160
  <div class="flex items-center gap-2">
1997
- <svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none">
2161
+ <svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none" aria-hidden="true">
1998
2162
  <path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
1999
2163
  <path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
2000
2164
  </svg>
2001
2165
  <span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
2002
- Schema errors ({{ schemaErrorList.length }})
2166
+ {{ options.labels.schemaErrorsTitle }} ({{ schemaErrorList.length }})
2003
2167
  </span>
2004
2168
  </div>
2005
2169
  <p class="text-xs text-amber-700 dark:text-amber-400">
2006
- The schema has structural issues. Fix them to get a valid form.
2170
+ {{ options.labels.schemaErrorsHint }}
2007
2171
  </p>
2008
2172
  <div class="space-y-2">
2009
2173
  <div *ngFor="let err of schemaErrorList"
@@ -2041,6 +2205,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
2041
2205
  type: Input
2042
2206
  }], decimalSeparator: [{
2043
2207
  type: Input
2208
+ }], labels: [{
2209
+ type: Input
2044
2210
  }], formReady: [{
2045
2211
  type: Output
2046
2212
  }], valueChange: [{
@@ -2068,5 +2234,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
2068
2234
  * Generated bundle index. Do not edit.
2069
2235
  */
2070
2236
 
2071
- export { JsonSchemaFormComponent, JsonSchemaFormModule, JsonSchemaFormOptions, JsonSchemaFormService, JsonSchemaNodeComponent, JsonSchemaResolverService, JsonSchemaStylesService, JsonSchemaValidationService, NumericInputDirective, formatNumeric, localizeNumbers, normalizeValue, parseNumericText, sanitizeNumericText };
2237
+ export { DEFAULT_LABELS, JsonSchemaFormComponent, JsonSchemaFormModule, JsonSchemaFormOptions, JsonSchemaFormService, JsonSchemaNodeComponent, JsonSchemaResolverService, JsonSchemaStylesService, JsonSchemaValidationService, NumericInputDirective, formatLabel, formatNumeric, localizeNumbers, normalizeValue, parseNumericText, sanitizeNumericText };
2072
2238
  //# sourceMappingURL=elle96-ng-json-schema-manager.mjs.map