@elle96/ng-json-schema-manager 0.2.0 → 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
  }
@@ -661,6 +704,10 @@ class NumericInputDirective {
661
704
  }
662
705
  writeValue(value) {
663
706
  this._model = value ?? null;
707
+ // Keep the text being typed when it already means this number ("45," for 45): rewriting it would drop the separator.
708
+ if (typeof value === 'number' && parseNumericText(this._el.nativeElement.value) === value) {
709
+ return;
710
+ }
664
711
  this._show(formatNumeric(value, this.jsmDecimalSeparator));
665
712
  }
666
713
  registerOnChange(fn) {
@@ -717,6 +764,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
717
764
  type: Input
718
765
  }] } });
719
766
 
767
+ let nextNodeId = 0;
720
768
  class JsonSchemaNodeComponent {
721
769
  schemaService;
722
770
  validation;
@@ -740,6 +788,8 @@ class JsonSchemaNodeComponent {
740
788
  propertyError = '';
741
789
  /** Tracks whether this primitive control has been touched (for OnPush CD). */
742
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}`;
743
793
  valueSub;
744
794
  constructor(schemaService, validation, cdr, options) {
745
795
  this.schemaService = schemaService;
@@ -800,6 +850,74 @@ class JsonSchemaNodeComponent {
800
850
  get decimalSeparator() {
801
851
  return this.options?.decimalSeparator ?? '.';
802
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
+ }
803
921
  /** `multipleOf` of this node's own schema, exposed as step metadata. */
804
922
  get numericStep() {
805
923
  return this.effectiveSchema.multipleOf ? String(this.effectiveSchema.multipleOf) : 'any';
@@ -851,34 +969,35 @@ class JsonSchemaNodeComponent {
851
969
  return ajvErrors;
852
970
  const angularErrors = [];
853
971
  const errs = ctrl.errors;
972
+ const l = this.l;
854
973
  if (errs['required'])
855
- angularErrors.push('This field is required');
974
+ angularErrors.push(l.errorRequired);
856
975
  if (errs['minlength'])
857
- angularErrors.push(`Minimum length is ${errs['minlength'].requiredLength}`);
976
+ angularErrors.push(this.fmt(l.errorMinLength, { n: errs['minlength'].requiredLength }));
858
977
  if (errs['maxlength'])
859
- angularErrors.push(`Maximum length is ${errs['maxlength'].requiredLength}`);
978
+ angularErrors.push(this.fmt(l.errorMaxLength, { n: errs['maxlength'].requiredLength }));
860
979
  if (errs['min'])
861
- angularErrors.push(`Minimum value is ${errs['min'].min}`);
980
+ angularErrors.push(this.fmt(l.errorMin, { n: errs['min'].min }));
862
981
  if (errs['max'])
863
- angularErrors.push(`Maximum value is ${errs['max'].max}`);
982
+ angularErrors.push(this.fmt(l.errorMax, { n: errs['max'].max }));
864
983
  if (errs['email'])
865
- angularErrors.push('Invalid email address');
984
+ angularErrors.push(l.errorEmail);
866
985
  if (errs['pattern'])
867
- angularErrors.push('Value does not match the required pattern');
986
+ angularErrors.push(l.errorPattern);
868
987
  if (errs['integer'])
869
- angularErrors.push('Value must be an integer');
988
+ angularErrors.push(l.errorInteger);
870
989
  if (errs['exclusiveMinimum'])
871
- angularErrors.push('Value must be greater than the minimum');
990
+ angularErrors.push(l.errorExclusiveMin);
872
991
  if (errs['exclusiveMaximum'])
873
- angularErrors.push('Value must be less than the maximum');
992
+ angularErrors.push(l.errorExclusiveMax);
874
993
  if (errs['multipleOf'])
875
- angularErrors.push('Value must be a multiple of the required number');
994
+ angularErrors.push(l.errorMultipleOf);
876
995
  if (errs['minItems'])
877
- angularErrors.push(`Minimum ${errs['minItems'].requiredLength ?? ''} items required`);
996
+ angularErrors.push(this.fmt(l.errorMinItems, { n: errs['minItems'].requiredLength ?? '' }));
878
997
  if (errs['maxItems'])
879
- angularErrors.push(`Maximum ${errs['maxItems'].requiredLength ?? ''} items allowed`);
998
+ angularErrors.push(this.fmt(l.errorMaxItems, { n: errs['maxItems'].requiredLength ?? '' }));
880
999
  if (errs['uniqueItems'])
881
- angularErrors.push('Items must be unique');
1000
+ angularErrors.push(l.errorUniqueItems);
882
1001
  // Merge: prefer Ajv messages, fall back to Angular messages for errors not covered by Ajv
883
1002
  const messages = ajvErrors.length > 0 ? ajvErrors : angularErrors;
884
1003
  return messages.map((message) => localizeNumbers(message, this.decimalSeparator));
@@ -908,8 +1027,16 @@ class JsonSchemaNodeComponent {
908
1027
  return this.mergeSchemas([this.schema, ...this.schema.allOf]);
909
1028
  return this.effectiveSchema;
910
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
+ */
911
1034
  get mergedAllOf() {
912
- 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];
913
1040
  }
914
1041
  isRequired(key) { return this.effectiveSchema.required?.includes(key) ?? false; }
915
1042
  schemaForKey(key) {
@@ -923,22 +1050,22 @@ class JsonSchemaNodeComponent {
923
1050
  this.propertyError = '';
924
1051
  const key = this.newPropertyKey.trim();
925
1052
  if (!key) {
926
- this.propertyError = 'Property name is required.';
1053
+ this.propertyError = this.l.propertyNameRequired;
927
1054
  return;
928
1055
  }
929
1056
  if (this.effectiveSchema.propertyNames) {
930
1057
  if (this.validation.validate(this.effectiveSchema.propertyNames, key).length) {
931
- this.propertyError = 'Property name does not match schema constraints.';
1058
+ this.propertyError = this.l.propertyNameInvalid;
932
1059
  return;
933
1060
  }
934
1061
  }
935
1062
  if (this.controlAsGroup.contains(key)) {
936
- this.propertyError = 'Property already exists.';
1063
+ this.propertyError = this.l.propertyExists;
937
1064
  return;
938
1065
  }
939
1066
  const s = this.schemaService.resolveDynamicSchema(this.effectiveSchema, key);
940
1067
  if (!s) {
941
- this.propertyError = 'This property is not allowed by the schema.';
1068
+ this.propertyError = this.l.propertyNotAllowed;
942
1069
  return;
943
1070
  }
944
1071
  this.controlAsGroup.addControl(key, this.schemaService.buildControl(s, undefined, false));
@@ -1073,14 +1200,17 @@ class JsonSchemaNodeComponent {
1073
1200
  </div>
1074
1201
  <span class="jsm-combinator-badge jsm-badge--oneof">oneOf</span>
1075
1202
  </div>
1076
- <div class="jsm-oneof-tabs">
1077
- <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"
1078
1205
  class="jsm-tab" [class.jsm-tab--active]="selectedOneOf === i"
1079
- (click)="selectedOneOf = i; onOneOfChange()">
1080
- {{ 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) }}
1081
1210
  </button>
1082
1211
  </div>
1083
- <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">
1084
1214
  <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
1085
1215
  [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
1086
1216
  [allowAdditionalProperties]="allowAdditionalProperties" [showErrors]="showErrors"
@@ -1097,11 +1227,11 @@ class JsonSchemaNodeComponent {
1097
1227
  </div>
1098
1228
  <span class="jsm-combinator-badge jsm-badge--anyof">anyOf</span>
1099
1229
  </div>
1100
- <div class="jsm-anyof-options">
1230
+ <div class="jsm-anyof-options" role="group" [attr.aria-label]="schema.title || label || null">
1101
1231
  <label *ngFor="let option of schema.anyOf; let i = index"
1102
1232
  class="jsm-anyof-option" [class.jsm-anyof-option--active]="selectedAnyOf.has(i)">
1103
1233
  <input type="checkbox" class="sr-only" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
1104
- {{ option.title || ('Option ' + (i + 1)) }}
1234
+ {{ option.title || optionLabel(i) }}
1105
1235
  </label>
1106
1236
  </div>
1107
1237
  <div class="jsm-combinator-body" *ngIf="activeVariant">
@@ -1124,7 +1254,7 @@ class JsonSchemaNodeComponent {
1124
1254
  <div class="jsm-combinator-body">
1125
1255
  <div *ngFor="let subSchema of mergedAllOf; let i = index" class="jsm-allof-section">
1126
1256
  <jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
1127
- [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
1257
+ [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || sectionLabel(i)" [required]="required"
1128
1258
  [allowAdditionalProperties]="allowAdditionalProperties" [showErrors]="showErrors"
1129
1259
  (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1130
1260
  </div>
@@ -1144,8 +1274,9 @@ class JsonSchemaNodeComponent {
1144
1274
  <ng-container *ngFor="let key of simpleKeys">
1145
1275
  <div style="display:flex;flex-direction:column;gap:.25rem;">
1146
1276
  <div *ngIf="isDynamicKey(key)" class="jsm-dynamic-key-header">
1147
- <span class="jsm-dynamic-key-label">{{ key }}</span>
1148
- <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>
1149
1280
  </div>
1150
1281
  <jsm-schema-node [schema]="schemaForKey(key)" [control]="childControl(key)" [parent]="controlAsGroup"
1151
1282
  [controlKey]="key" [path]="pathForChild(key)" [errorsMap]="errorsMap" [label]="key"
@@ -1159,8 +1290,9 @@ class JsonSchemaNodeComponent {
1159
1290
  <ng-container *ngFor="let key of complexKeys">
1160
1291
  <div style="display:flex;flex-direction:column;gap:.25rem;">
1161
1292
  <div *ngIf="isDynamicKey(key)" class="jsm-dynamic-key-header">
1162
- <span class="jsm-dynamic-key-label">{{ key }}</span>
1163
- <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>
1164
1296
  </div>
1165
1297
  <jsm-schema-node [schema]="schemaForKey(key)" [control]="childControl(key)" [parent]="controlAsGroup"
1166
1298
  [controlKey]="key" [path]="pathForChild(key)" [errorsMap]="errorsMap" [label]="key"
@@ -1170,45 +1302,48 @@ class JsonSchemaNodeComponent {
1170
1302
  </ng-container>
1171
1303
  </div>
1172
1304
 
1173
- <div *ngIf="canAddProperty" class="jsm-add-prop">
1174
- <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>
1175
1307
  <div class="jsm-add-prop-row">
1176
- <input class="jsm-input jsm-input--sm" style="flex:1;" [(ngModel)]="newPropertyKey" placeholder="propertyName" />
1177
- <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>
1178
1312
  </div>
1179
- <p *ngIf="propertyError" class="jsm-error-msg">
1180
- <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>
1181
1315
  {{ propertyError }}
1182
1316
  </p>
1183
1317
  </div>
1184
1318
 
1185
- <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;">
1186
1320
  <p *ngFor="let error of errorsForPath" class="jsm-error-msg">
1187
- <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>
1188
- {{ 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) }}
1189
1323
  </p>
1190
1324
  </div>
1191
1325
  </fieldset>
1192
1326
 
1193
1327
  <!-- ═══ array ═══ -->
1194
- <div *ngSwitchCase="'array'" class="jsm-array-block">
1328
+ <div *ngSwitchCase="'array'" class="jsm-array-block" role="group" [attr.aria-labelledby]="nodeId + '-title'">
1195
1329
  <div class="jsm-array-header">
1196
1330
  <div>
1197
- <p class="jsm-array-title">
1198
- {{ 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>
1199
1333
  </p>
1200
1334
  <p *ngIf="schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1201
1335
  </div>
1202
- <button type="button" class="jsm-btn-primary" (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1203
- <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>
1204
- 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 }}
1205
1340
  </button>
1206
1341
  </div>
1207
1342
 
1208
1343
  <div *ngIf="effectiveSchema.contains" class="jsm-contains-hint">
1209
- Must contain matching item(s)
1210
- <span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
1211
- <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>
1212
1347
  </div>
1213
1348
 
1214
1349
  <div *ngIf="metaBadges.length" class="jsm-badge-row">
@@ -1216,10 +1351,12 @@ class JsonSchemaNodeComponent {
1216
1351
  </div>
1217
1352
 
1218
1353
  <div class="jsm-array-items" *ngIf="arrayControls.length > 0; else emptyArray">
1219
- <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">
1220
1355
  <div class="jsm-array-item-header">
1221
- <span class="jsm-array-index">{{ i + 1 }}</span>
1222
- <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>
1223
1360
  </div>
1224
1361
  <jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
1225
1362
  [controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
@@ -1229,13 +1366,13 @@ class JsonSchemaNodeComponent {
1229
1366
  </div>
1230
1367
 
1231
1368
  <ng-template #emptyArray>
1232
- <div class="jsm-empty-array">No items yet — click "Add item" to start.</div>
1369
+ <div class="jsm-empty-array">{{ l.noItems }}</div>
1233
1370
  </ng-template>
1234
1371
 
1235
- <div *ngIf="errorsForPath.length" class="jsm-error-list">
1372
+ <div *ngIf="errorsForPath.length" class="jsm-error-list" role="alert">
1236
1373
  <p *ngFor="let error of errorsForPath" class="jsm-error-msg">
1237
- <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>
1238
- {{ 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) }}
1239
1376
  </p>
1240
1377
  </div>
1241
1378
  </div>
@@ -1243,10 +1380,10 @@ class JsonSchemaNodeComponent {
1243
1380
  <!-- ═══ primitive ═══ -->
1244
1381
  <div *ngSwitchDefault class="jsm-field">
1245
1382
  <ng-container *ngIf="inputKind !== 'checkbox'">
1246
- <label class="jsm-field-label">
1247
- {{ 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>
1248
1385
  </label>
1249
- <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>
1250
1387
  </ng-container>
1251
1388
 
1252
1389
  <div *ngIf="metaBadges.length" class="jsm-badge-row">
@@ -1254,57 +1391,65 @@ class JsonSchemaNodeComponent {
1254
1391
  </div>
1255
1392
 
1256
1393
  <ng-container>
1257
- <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"
1258
1395
  [class.jsm-input--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1259
1396
  [jsmDecimalSeparator]="decimalSeparator" [attr.data-step]="numericStep"
1260
- [attr.placeholder]="schema.title || label"
1397
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1398
+ [attr.aria-describedby]="describedBy"
1261
1399
  [formControl]="controlAsFormControl"
1262
1400
  (blur)="onBlur()" />
1263
1401
 
1264
- <input *ngIf="inputKind === 'text'" class="jsm-input"
1402
+ <input *ngIf="inputKind === 'text'" class="jsm-input" [id]="nodeId"
1265
1403
  [class.jsm-input--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1266
- [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"
1267
1407
  [formControl]="controlAsFormControl"
1268
1408
  (blur)="onBlur()" />
1269
1409
 
1270
- <textarea *ngIf="inputKind === 'textarea'" rows="4" class="jsm-textarea"
1410
+ <textarea *ngIf="inputKind === 'textarea'" rows="4" class="jsm-textarea" [id]="nodeId"
1271
1411
  [class.jsm-textarea--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1272
- [attr.placeholder]="schema.title || label"
1412
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1413
+ [attr.aria-describedby]="describedBy"
1273
1414
  [formControl]="controlAsFormControl"
1274
1415
  (blur)="onBlur()"></textarea>
1275
1416
 
1276
1417
  <div *ngIf="inputKind === 'select'" class="jsm-select-wrap">
1277
- <select class="jsm-select"
1418
+ <select class="jsm-select" [id]="nodeId"
1278
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"
1279
1422
  [formControl]="controlAsFormControl"
1280
1423
  (blur)="onBlur()">
1281
1424
  <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1282
1425
  </select>
1283
- <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">
1284
1427
  <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1285
1428
  </svg>
1286
1429
  </div>
1287
1430
 
1288
1431
  <label *ngIf="inputKind === 'checkbox'" class="jsm-toggle-label">
1289
1432
  <span class="jsm-toggle-track" [class.jsm-on]="controlAsFormControl.value">
1290
- <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()" />
1291
1436
  <span class="jsm-toggle-thumb"></span>
1292
1437
  </span>
1293
1438
  <span class="jsm-toggle-text">
1294
- {{ schema.title || label }}<span *ngIf="required" class="jsm-required">*</span>
1439
+ {{ schema.title || label }}<span *ngIf="required" class="jsm-required" aria-hidden="true">*</span>
1295
1440
  </span>
1296
1441
  </label>
1297
1442
  </ng-container>
1298
1443
 
1299
- <p *ngIf="inputKind === 'checkbox' && schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1300
- <p *ngIf="effectiveSchema.examples?.length" class="jsm-example">e.g. {{ effectiveSchema.examples?.[0] }}</p>
1301
- <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'">
1302
1447
  {{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
1303
1448
  </p>
1304
1449
 
1305
- <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'">
1306
1451
  <p *ngFor="let error of allErrors" class="jsm-error-msg">
1307
- <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">
1308
1453
  <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
1309
1454
  <path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1310
1455
  </svg>
@@ -1314,7 +1459,7 @@ class JsonSchemaNodeComponent {
1314
1459
  </div>
1315
1460
 
1316
1461
  </ng-container>
1317
- `, 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 });
1318
1463
  }
1319
1464
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: JsonSchemaNodeComponent, decorators: [{
1320
1465
  type: Component,
@@ -1330,14 +1475,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1330
1475
  </div>
1331
1476
  <span class="jsm-combinator-badge jsm-badge--oneof">oneOf</span>
1332
1477
  </div>
1333
- <div class="jsm-oneof-tabs">
1334
- <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"
1335
1480
  class="jsm-tab" [class.jsm-tab--active]="selectedOneOf === i"
1336
- (click)="selectedOneOf = i; onOneOfChange()">
1337
- {{ 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) }}
1338
1485
  </button>
1339
1486
  </div>
1340
- <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">
1341
1489
  <jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
1342
1490
  [path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
1343
1491
  [allowAdditionalProperties]="allowAdditionalProperties" [showErrors]="showErrors"
@@ -1354,11 +1502,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1354
1502
  </div>
1355
1503
  <span class="jsm-combinator-badge jsm-badge--anyof">anyOf</span>
1356
1504
  </div>
1357
- <div class="jsm-anyof-options">
1505
+ <div class="jsm-anyof-options" role="group" [attr.aria-label]="schema.title || label || null">
1358
1506
  <label *ngFor="let option of schema.anyOf; let i = index"
1359
1507
  class="jsm-anyof-option" [class.jsm-anyof-option--active]="selectedAnyOf.has(i)">
1360
1508
  <input type="checkbox" class="sr-only" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
1361
- {{ option.title || ('Option ' + (i + 1)) }}
1509
+ {{ option.title || optionLabel(i) }}
1362
1510
  </label>
1363
1511
  </div>
1364
1512
  <div class="jsm-combinator-body" *ngIf="activeVariant">
@@ -1381,7 +1529,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1381
1529
  <div class="jsm-combinator-body">
1382
1530
  <div *ngFor="let subSchema of mergedAllOf; let i = index" class="jsm-allof-section">
1383
1531
  <jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
1384
- [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
1532
+ [path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || sectionLabel(i)" [required]="required"
1385
1533
  [allowAdditionalProperties]="allowAdditionalProperties" [showErrors]="showErrors"
1386
1534
  (controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
1387
1535
  </div>
@@ -1401,8 +1549,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1401
1549
  <ng-container *ngFor="let key of simpleKeys">
1402
1550
  <div style="display:flex;flex-direction:column;gap:.25rem;">
1403
1551
  <div *ngIf="isDynamicKey(key)" class="jsm-dynamic-key-header">
1404
- <span class="jsm-dynamic-key-label">{{ key }}</span>
1405
- <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>
1406
1555
  </div>
1407
1556
  <jsm-schema-node [schema]="schemaForKey(key)" [control]="childControl(key)" [parent]="controlAsGroup"
1408
1557
  [controlKey]="key" [path]="pathForChild(key)" [errorsMap]="errorsMap" [label]="key"
@@ -1416,8 +1565,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1416
1565
  <ng-container *ngFor="let key of complexKeys">
1417
1566
  <div style="display:flex;flex-direction:column;gap:.25rem;">
1418
1567
  <div *ngIf="isDynamicKey(key)" class="jsm-dynamic-key-header">
1419
- <span class="jsm-dynamic-key-label">{{ key }}</span>
1420
- <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>
1421
1571
  </div>
1422
1572
  <jsm-schema-node [schema]="schemaForKey(key)" [control]="childControl(key)" [parent]="controlAsGroup"
1423
1573
  [controlKey]="key" [path]="pathForChild(key)" [errorsMap]="errorsMap" [label]="key"
@@ -1427,45 +1577,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1427
1577
  </ng-container>
1428
1578
  </div>
1429
1579
 
1430
- <div *ngIf="canAddProperty" class="jsm-add-prop">
1431
- <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>
1432
1582
  <div class="jsm-add-prop-row">
1433
- <input class="jsm-input jsm-input--sm" style="flex:1;" [(ngModel)]="newPropertyKey" placeholder="propertyName" />
1434
- <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>
1435
1587
  </div>
1436
- <p *ngIf="propertyError" class="jsm-error-msg">
1437
- <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>
1438
1590
  {{ propertyError }}
1439
1591
  </p>
1440
1592
  </div>
1441
1593
 
1442
- <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;">
1443
1595
  <p *ngFor="let error of errorsForPath" class="jsm-error-msg">
1444
- <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>
1445
- {{ 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) }}
1446
1598
  </p>
1447
1599
  </div>
1448
1600
  </fieldset>
1449
1601
 
1450
1602
  <!-- ═══ array ═══ -->
1451
- <div *ngSwitchCase="'array'" class="jsm-array-block">
1603
+ <div *ngSwitchCase="'array'" class="jsm-array-block" role="group" [attr.aria-labelledby]="nodeId + '-title'">
1452
1604
  <div class="jsm-array-header">
1453
1605
  <div>
1454
- <p class="jsm-array-title">
1455
- {{ 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>
1456
1608
  </p>
1457
1609
  <p *ngIf="schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1458
1610
  </div>
1459
- <button type="button" class="jsm-btn-primary" (click)="addArrayItem()" [disabled]="!canAddArrayItem">
1460
- <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>
1461
- 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 }}
1462
1615
  </button>
1463
1616
  </div>
1464
1617
 
1465
1618
  <div *ngIf="effectiveSchema.contains" class="jsm-contains-hint">
1466
- Must contain matching item(s)
1467
- <span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
1468
- <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>
1469
1622
  </div>
1470
1623
 
1471
1624
  <div *ngIf="metaBadges.length" class="jsm-badge-row">
@@ -1473,10 +1626,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1473
1626
  </div>
1474
1627
 
1475
1628
  <div class="jsm-array-items" *ngIf="arrayControls.length > 0; else emptyArray">
1476
- <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">
1477
1630
  <div class="jsm-array-item-header">
1478
- <span class="jsm-array-index">{{ i + 1 }}</span>
1479
- <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>
1480
1635
  </div>
1481
1636
  <jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
1482
1637
  [controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
@@ -1486,13 +1641,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1486
1641
  </div>
1487
1642
 
1488
1643
  <ng-template #emptyArray>
1489
- <div class="jsm-empty-array">No items yet — click "Add item" to start.</div>
1644
+ <div class="jsm-empty-array">{{ l.noItems }}</div>
1490
1645
  </ng-template>
1491
1646
 
1492
- <div *ngIf="errorsForPath.length" class="jsm-error-list">
1647
+ <div *ngIf="errorsForPath.length" class="jsm-error-list" role="alert">
1493
1648
  <p *ngFor="let error of errorsForPath" class="jsm-error-msg">
1494
- <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>
1495
- {{ 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) }}
1496
1651
  </p>
1497
1652
  </div>
1498
1653
  </div>
@@ -1500,10 +1655,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1500
1655
  <!-- ═══ primitive ═══ -->
1501
1656
  <div *ngSwitchDefault class="jsm-field">
1502
1657
  <ng-container *ngIf="inputKind !== 'checkbox'">
1503
- <label class="jsm-field-label">
1504
- {{ 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>
1505
1660
  </label>
1506
- <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>
1507
1662
  </ng-container>
1508
1663
 
1509
1664
  <div *ngIf="metaBadges.length" class="jsm-badge-row">
@@ -1511,57 +1666,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1511
1666
  </div>
1512
1667
 
1513
1668
  <ng-container>
1514
- <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"
1515
1670
  [class.jsm-input--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1516
1671
  [jsmDecimalSeparator]="decimalSeparator" [attr.data-step]="numericStep"
1517
- [attr.placeholder]="schema.title || label"
1672
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1673
+ [attr.aria-describedby]="describedBy"
1518
1674
  [formControl]="controlAsFormControl"
1519
1675
  (blur)="onBlur()" />
1520
1676
 
1521
- <input *ngIf="inputKind === 'text'" class="jsm-input"
1677
+ <input *ngIf="inputKind === 'text'" class="jsm-input" [id]="nodeId"
1522
1678
  [class.jsm-input--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1523
- [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"
1524
1682
  [formControl]="controlAsFormControl"
1525
1683
  (blur)="onBlur()" />
1526
1684
 
1527
- <textarea *ngIf="inputKind === 'textarea'" rows="4" class="jsm-textarea"
1685
+ <textarea *ngIf="inputKind === 'textarea'" rows="4" class="jsm-textarea" [id]="nodeId"
1528
1686
  [class.jsm-textarea--error]="controlAsFormControl.invalid && (controlAsFormControl.dirty || isTouched || showErrors)"
1529
- [attr.placeholder]="schema.title || label"
1687
+ [attr.aria-required]="required ? 'true' : null" [attr.aria-invalid]="visibleError ? 'true' : null"
1688
+ [attr.aria-describedby]="describedBy"
1530
1689
  [formControl]="controlAsFormControl"
1531
1690
  (blur)="onBlur()"></textarea>
1532
1691
 
1533
1692
  <div *ngIf="inputKind === 'select'" class="jsm-select-wrap">
1534
- <select class="jsm-select"
1693
+ <select class="jsm-select" [id]="nodeId"
1535
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"
1536
1697
  [formControl]="controlAsFormControl"
1537
1698
  (blur)="onBlur()">
1538
1699
  <option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
1539
1700
  </select>
1540
- <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">
1541
1702
  <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
1542
1703
  </svg>
1543
1704
  </div>
1544
1705
 
1545
1706
  <label *ngIf="inputKind === 'checkbox'" class="jsm-toggle-label">
1546
1707
  <span class="jsm-toggle-track" [class.jsm-on]="controlAsFormControl.value">
1547
- <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()" />
1548
1711
  <span class="jsm-toggle-thumb"></span>
1549
1712
  </span>
1550
1713
  <span class="jsm-toggle-text">
1551
- {{ schema.title || label }}<span *ngIf="required" class="jsm-required">*</span>
1714
+ {{ schema.title || label }}<span *ngIf="required" class="jsm-required" aria-hidden="true">*</span>
1552
1715
  </span>
1553
1716
  </label>
1554
1717
  </ng-container>
1555
1718
 
1556
- <p *ngIf="inputKind === 'checkbox' && schema.description" class="jsm-field-hint">{{ schema.description }}</p>
1557
- <p *ngIf="effectiveSchema.examples?.length" class="jsm-example">e.g. {{ effectiveSchema.examples?.[0] }}</p>
1558
- <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'">
1559
1722
  {{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
1560
1723
  </p>
1561
1724
 
1562
- <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'">
1563
1726
  <p *ngFor="let error of allErrors" class="jsm-error-msg">
1564
- <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">
1565
1728
  <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
1566
1729
  <path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1567
1730
  </svg>
@@ -1571,7 +1734,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1571
1734
  </div>
1572
1735
 
1573
1736
  </ng-container>
1574
- `, 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"] }]
1575
1738
  }], ctorParameters: () => [{ type: JsonSchemaFormService }, { type: JsonSchemaValidationService }, { type: i0.ChangeDetectorRef }, { type: JsonSchemaFormOptions, decorators: [{
1576
1739
  type: Optional
1577
1740
  }] }], propDecorators: { schema: [{
@@ -1784,6 +1947,8 @@ class JsonSchemaFormComponent {
1784
1947
  * either way); the emitted value is always a JSON number.
1785
1948
  */
1786
1949
  decimalSeparator = '.';
1950
+ /** Texts shown by the form (translate them here); omitted keys keep the English default. */
1951
+ labels = {};
1787
1952
  formReady = new EventEmitter();
1788
1953
  valueChange = new EventEmitter();
1789
1954
  schemaReady = new EventEmitter();
@@ -1797,6 +1962,8 @@ class JsonSchemaFormComponent {
1797
1962
  /** When true, all field errors are shown regardless of touched/dirty state. */
1798
1963
  showErrors = false;
1799
1964
  valueSub;
1965
+ /** Last value emitted through valueChange (same reference the parent gets back through `data`/`value`). */
1966
+ lastEmitted;
1800
1967
  constructor(schemaService, resolver, validation, cdr, options, stylesService) {
1801
1968
  this.schemaService = schemaService;
1802
1969
  this.resolver = resolver;
@@ -1809,6 +1976,9 @@ class JsonSchemaFormComponent {
1809
1976
  if (changes['decimalSeparator']) {
1810
1977
  this.options.decimalSeparator = this.decimalSeparator;
1811
1978
  }
1979
+ if (changes['labels']) {
1980
+ this.options.labels = { ...DEFAULT_LABELS, ...this.labels };
1981
+ }
1812
1982
  if (changes['schema']) {
1813
1983
  // Schema changed — full rebuild required
1814
1984
  void this.buildForm();
@@ -1816,7 +1986,11 @@ class JsonSchemaFormComponent {
1816
1986
  else if ((changes['value'] || changes['data']) && this.form) {
1817
1987
  // Only data changed and form already exists — patch values without rebuilding
1818
1988
  const newData = this.value !== undefined ? this.value : this.data;
1819
- this.form.patchValue(newData, { emitEvent: false });
1989
+ // The parent usually feeds back what valueChange just emitted: patching it would overwrite the text being
1990
+ // typed (e.g. "45," would become "45"), so a round trip of the emitted value is ignored.
1991
+ if (newData !== this.lastEmitted) {
1992
+ this.form.patchValue(newData, { emitEvent: false });
1993
+ }
1820
1994
  }
1821
1995
  else if (changes['value'] || changes['data']) {
1822
1996
  // Form not built yet — build it
@@ -1898,24 +2072,25 @@ class JsonSchemaFormComponent {
1898
2072
  this.updateErrors();
1899
2073
  this.valueSub = this.form.valueChanges.subscribe(() => {
1900
2074
  this.updateErrors();
1901
- this.valueChange.emit(this.resolvedSchema ? normalizeValue(this.resolvedSchema, this.rootControl.value) : this.rootControl.value);
2075
+ this.lastEmitted = this.resolvedSchema ? normalizeValue(this.resolvedSchema, this.rootControl.value) : this.rootControl.value;
2076
+ this.valueChange.emit(this.lastEmitted);
1902
2077
  });
1903
2078
  }
1904
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 });
1905
- 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: `
1906
2081
  <div class="jsm-root">
1907
2082
  <form class="w-full space-y-5">
1908
2083
  <div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
1909
- <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>
1910
2085
  <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
1911
2086
  </div>
1912
2087
 
1913
- <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
1914
- <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">
1915
2090
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
1916
2091
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
1917
2092
  </svg>
1918
- Resolving schema&hellip;
2093
+ {{ options.labels.resolving }}
1919
2094
  </div>
1920
2095
 
1921
2096
  <!-- schema structural errors panel -->
@@ -1923,16 +2098,16 @@ class JsonSchemaFormComponent {
1923
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"
1924
2099
  role="alert">
1925
2100
  <div class="flex items-center gap-2">
1926
- <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">
1927
2102
  <path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
1928
2103
  <path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1929
2104
  </svg>
1930
2105
  <span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
1931
- Schema errors ({{ schemaErrorList.length }})
2106
+ {{ options.labels.schemaErrorsTitle }} ({{ schemaErrorList.length }})
1932
2107
  </span>
1933
2108
  </div>
1934
2109
  <p class="text-xs text-amber-700 dark:text-amber-400">
1935
- The schema has structural issues. Fix them to get a valid form.
2110
+ {{ options.labels.schemaErrorsHint }}
1936
2111
  </p>
1937
2112
  <div class="space-y-2">
1938
2113
  <div *ngFor="let err of schemaErrorList"
@@ -1966,16 +2141,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1966
2141
  <div class="jsm-root">
1967
2142
  <form class="w-full space-y-5">
1968
2143
  <div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
1969
- <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>
1970
2145
  <p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
1971
2146
  </div>
1972
2147
 
1973
- <div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
1974
- <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">
1975
2150
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
1976
2151
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
1977
2152
  </svg>
1978
- Resolving schema&hellip;
2153
+ {{ options.labels.resolving }}
1979
2154
  </div>
1980
2155
 
1981
2156
  <!-- schema structural errors panel -->
@@ -1983,16 +2158,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
1983
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"
1984
2159
  role="alert">
1985
2160
  <div class="flex items-center gap-2">
1986
- <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">
1987
2162
  <path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
1988
2163
  <path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
1989
2164
  </svg>
1990
2165
  <span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
1991
- Schema errors ({{ schemaErrorList.length }})
2166
+ {{ options.labels.schemaErrorsTitle }} ({{ schemaErrorList.length }})
1992
2167
  </span>
1993
2168
  </div>
1994
2169
  <p class="text-xs text-amber-700 dark:text-amber-400">
1995
- The schema has structural issues. Fix them to get a valid form.
2170
+ {{ options.labels.schemaErrorsHint }}
1996
2171
  </p>
1997
2172
  <div class="space-y-2">
1998
2173
  <div *ngFor="let err of schemaErrorList"
@@ -2030,6 +2205,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
2030
2205
  type: Input
2031
2206
  }], decimalSeparator: [{
2032
2207
  type: Input
2208
+ }], labels: [{
2209
+ type: Input
2033
2210
  }], formReady: [{
2034
2211
  type: Output
2035
2212
  }], valueChange: [{
@@ -2057,5 +2234,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
2057
2234
  * Generated bundle index. Do not edit.
2058
2235
  */
2059
2236
 
2060
- 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 };
2061
2238
  //# sourceMappingURL=elle96-ng-json-schema-manager.mjs.map