@pure-ds/core 0.3.18 → 0.4.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.
@@ -11,21 +11,21 @@ function getStep(value) {
11
11
  // Default options for pds-jsonform
12
12
  const DEFAULT_OPTIONS = {
13
13
  widgets: {
14
- booleans: "toggle", // 'toggle' | 'checkbox'
15
- numbers: "input", // 'input' | 'range'
16
- selects: "standard", // 'standard' | 'dropdown'
14
+ booleans: "toggle", // 'toggle' | 'checkbox'
15
+ numbers: "input", // 'input' | 'range'
16
+ selects: "standard", // 'standard' | 'dropdown'
17
17
  },
18
18
  layouts: {
19
- fieldsets: "default", // 'default' | 'flex' | 'grid' | 'accordion' | 'tabs' | 'card'
20
- arrays: "default", // 'default' | 'open' | 'compact'
19
+ fieldsets: "default", // 'default' | 'flex' | 'grid' | 'accordion' | 'tabs' | 'card'
20
+ arrays: "default", // 'default' | 'open' | 'compact'
21
21
  },
22
22
  enhancements: {
23
- icons: true, // Enable icon-enhanced inputs
24
- datalists: true, // Enable datalist autocomplete
25
- rangeOutput: true, // Use .range-output for ranges
23
+ icons: true, // Enable icon-enhanced inputs
24
+ datalists: true, // Enable datalist autocomplete
25
+ rangeOutput: true, // Use .range-output for ranges
26
26
  },
27
27
  validation: {
28
- showErrors: true, // Show validation errors inline
28
+ showErrors: true, // Show validation errors inline
29
29
  validateOnChange: false, // Validate on every change vs on submit
30
30
  },
31
31
  };
@@ -88,6 +88,7 @@ export class SchemaForm extends LitElement {
88
88
  #data = {};
89
89
  #idBase = `sf-${Math.random().toString(36).slice(2)}`;
90
90
  #mergedOptions = null;
91
+ #slottedActions = [];
91
92
 
92
93
  constructor() {
93
94
  super();
@@ -102,12 +103,12 @@ export class SchemaForm extends LitElement {
102
103
  this.hideReset = false;
103
104
  this.hideLegend = false;
104
105
  this.#installDefaultRenderers();
105
-
106
+
106
107
  // Handle submit button clicks in slotted actions
107
- this.addEventListener('click', (e) => {
108
+ this.addEventListener("click", (e) => {
108
109
  const button = e.target.closest('button[type="submit"]');
109
110
  if (button && this.contains(button)) {
110
- const form = this.querySelector('form');
111
+ const form = this.querySelector("form");
111
112
  if (form) {
112
113
  e.preventDefault();
113
114
  form.requestSubmit();
@@ -123,18 +124,21 @@ export class SchemaForm extends LitElement {
123
124
  useValidator(fn) {
124
125
  this.#validator = fn;
125
126
  }
126
-
127
+
127
128
  // Get values in flat JSON Pointer format
128
129
  getValuesFlat() {
129
130
  return this.#flattenToPointers(this.#data);
130
131
  }
131
-
132
+
132
133
  #flattenToPointers(obj, prefix = "") {
133
134
  const flattened = {};
134
135
  for (const [key, value] of Object.entries(obj)) {
135
136
  const jsonPointerPath = prefix ? `${prefix}/${key}` : `/${key}`;
136
137
  if (value && typeof value === "object" && !Array.isArray(value)) {
137
- Object.assign(flattened, this.#flattenToPointers(value, jsonPointerPath));
138
+ Object.assign(
139
+ flattened,
140
+ this.#flattenToPointers(value, jsonPointerPath)
141
+ );
138
142
  } else {
139
143
  flattened[jsonPointerPath] = value;
140
144
  }
@@ -152,13 +156,21 @@ export class SchemaForm extends LitElement {
152
156
  return this.#onSubmit(new Event("submit", { cancelable: true }));
153
157
  }
154
158
 
159
+ connectedCallback() {
160
+ super.connectedCallback();
161
+ // Capture slotted actions before first render (Light DOM doesn't support native slots)
162
+ this.#slottedActions = Array.from(
163
+ this.querySelectorAll('[slot="actions"]')
164
+ );
165
+ }
166
+
155
167
  // ===== Lit lifecycle =====
156
168
  willUpdate(changed) {
157
169
  // Merge options when options or jsonSchema changes
158
170
  if (changed.has("options") || changed.has("jsonSchema")) {
159
171
  this.#mergeOptions();
160
172
  }
161
-
173
+
162
174
  if (changed.has("jsonSchema")) this.#compile();
163
175
  if (changed.has("uiSchema")) this.requestUpdate();
164
176
  if (changed.has("values")) {
@@ -169,7 +181,7 @@ export class SchemaForm extends LitElement {
169
181
  } else {
170
182
  const newData = {};
171
183
  for (const [key, value] of Object.entries(v)) {
172
- if (key.startsWith('/')) {
184
+ if (key.startsWith("/")) {
173
185
  this.#setByPath(newData, key, value);
174
186
  } else {
175
187
  newData[key] = value;
@@ -183,34 +195,37 @@ export class SchemaForm extends LitElement {
183
195
  #mergeOptions() {
184
196
  // Start with default options
185
197
  let merged = { ...DEFAULT_OPTIONS };
186
-
198
+
187
199
  // Try to get preset options from window.PDS if available
188
200
  if (typeof window !== "undefined" && window.PDS?.config?.form?.options) {
189
- merged = window.PDS.common.deepMerge(merged, window.PDS.config.form.options);
201
+ merged = window.PDS.common.deepMerge(
202
+ merged,
203
+ window.PDS.config.form.options
204
+ );
190
205
  }
191
-
206
+
192
207
  // Merge instance options
193
208
  if (this.options) {
194
209
  merged = window.PDS.common.deepMerge(merged, this.options);
195
210
  }
196
-
211
+
197
212
  this.#mergedOptions = merged;
198
213
  }
199
214
 
200
215
  #getOption(path, defaultValue) {
201
216
  if (!this.#mergedOptions) this.#mergeOptions();
202
-
217
+
203
218
  // Support path-based options like '/address/zip': { ... }
204
- if (path.startsWith('/')) {
219
+ if (path.startsWith("/")) {
205
220
  const pathOptions = this.#mergedOptions[path];
206
221
  if (pathOptions !== undefined) return pathOptions;
207
222
  }
208
-
223
+
209
224
  // Support nested option paths like 'widgets.booleans'
210
- const parts = path.split('.');
225
+ const parts = path.split(".");
211
226
  let current = this.#mergedOptions;
212
227
  for (const part of parts) {
213
- if (current && typeof current === 'object' && part in current) {
228
+ if (current && typeof current === "object" && part in current) {
214
229
  current = current[part];
215
230
  } else {
216
231
  return defaultValue;
@@ -260,7 +275,7 @@ export class SchemaForm extends LitElement {
260
275
  if (ui?.["ui:dialog"]) {
261
276
  return { kind: "dialog", path, title, schema, ui };
262
277
  }
263
-
278
+
264
279
  const order = this.#propertyOrder(schema, ui);
265
280
  const children = order.map((key) => {
266
281
  const childPath = path + "/" + this.#escapeJsonPointer(key);
@@ -326,7 +341,7 @@ export class SchemaForm extends LitElement {
326
341
  if (schema.const !== undefined) return "const";
327
342
  if (schema.type === "string") {
328
343
  // Check for binary/upload content via JSON Schema contentMediaType
329
- if (schema.contentMediaType || schema.contentEncoding === 'base64') {
344
+ if (schema.contentMediaType || schema.contentEncoding === "base64") {
330
345
  return "upload";
331
346
  }
332
347
  switch (schema.format) {
@@ -354,24 +369,23 @@ export class SchemaForm extends LitElement {
354
369
  case "date-time":
355
370
  return "input-datetime";
356
371
  default:
357
- if (
358
- (schema.maxLength ?? 0) > 160 ||
359
- ui?.["ui:widget"] === "textarea"
360
- )
372
+ if ((schema.maxLength ?? 0) > 160 || ui?.["ui:widget"] === "textarea")
361
373
  return "textarea";
362
374
  return "input-text";
363
375
  }
364
376
  }
365
377
  if (schema.type === "number" || schema.type === "integer") {
366
378
  // Check if range widget should be used
367
- const useRange = this.#getOption('widgets.numbers', 'input') === 'range' ||
368
- ui?.["ui:widget"] === "range" ||
369
- ui?.["ui:widget"] === "input-range";
379
+ const useRange =
380
+ this.#getOption("widgets.numbers", "input") === "range" ||
381
+ ui?.["ui:widget"] === "range" ||
382
+ ui?.["ui:widget"] === "input-range";
370
383
  return useRange ? "input-range" : "input-number";
371
384
  }
372
385
  if (schema.type === "boolean") {
373
386
  // Check if toggle should be used
374
- const useToggle = this.#getOption('widgets.booleans', 'toggle') === 'toggle';
387
+ const useToggle =
388
+ this.#getOption("widgets.booleans", "toggle") === "toggle";
375
389
  return useToggle ? "toggle" : "checkbox";
376
390
  }
377
391
  return "input-text";
@@ -425,24 +439,21 @@ export class SchemaForm extends LitElement {
425
439
  ${tree ? this.#renderNode(tree) : html`<slot></slot>`}
426
440
  ${!this.hideActions
427
441
  ? html`
428
- <div
429
- class="form-actions"
430
- style="margin-top: var(--spacing-6, 1.5rem); display: flex; gap: var(--spacing-3, 0.75rem);"
431
- >
432
- ${!this.hideSubmit ? html`
433
- <button type="submit" class="btn btn-primary">
434
- ${this.submitLabel}
435
- </button>` : nothing}
436
-
442
+ <div class="form-actions">
443
+ ${!this.hideSubmit
444
+ ? html` <button type="submit" class="btn btn-primary">
445
+ ${this.submitLabel}
446
+ </button>`
447
+ : nothing}
437
448
  ${!this.hideReset
438
449
  ? html`<button type="reset" class="btn">
439
450
  ${this.resetLabel}
440
451
  </button>`
441
452
  : nothing}
442
- <slot name="actions"></slot>
453
+ ${this.#slottedActions}
443
454
  </div>
444
455
  `
445
- : html`<slot name="actions"></slot>`}
456
+ : html`<div class="form-actions">${this.#slottedActions}</div>`}
446
457
  </form>
447
458
  `;
448
459
  }
@@ -467,39 +478,45 @@ export class SchemaForm extends LitElement {
467
478
  #renderFieldset(node, context = {}) {
468
479
  const legend = node.title ?? "Section";
469
480
  const ui = node.ui || this.#uiFor(node.path);
470
-
481
+
471
482
  // Check for path-specific options
472
483
  const pathOptions = this.#getOption(node.path, {});
473
-
484
+
474
485
  // Determine layout mode
475
- const layout = ui?.["ui:layout"] || pathOptions.layout ||
476
- this.#getOption('layouts.fieldsets', 'default');
477
-
486
+ const layout =
487
+ ui?.["ui:layout"] ||
488
+ pathOptions.layout ||
489
+ this.#getOption("layouts.fieldsets", "default");
490
+
478
491
  // Check for tabs layout
479
492
  if (layout === "tabs" || ui?.["ui:tabs"]) {
480
493
  return this.#renderFieldsetTabs(node, legend, ui);
481
494
  }
482
-
495
+
483
496
  // Check for accordion layout
484
497
  if (layout === "accordion" || ui?.["ui:accordion"]) {
485
498
  return this.#renderFieldsetAccordion(node, legend, ui);
486
499
  }
487
-
500
+
488
501
  // Check for surface wrapping
489
502
  const surface = ui?.["ui:surface"] || pathOptions.surface;
490
-
503
+
491
504
  // Build layout classes and inline styles
492
505
  const layoutClasses = [];
493
506
  let layoutStyle = "";
494
507
  const layoutOptions = ui?.["ui:layoutOptions"] || {};
495
-
508
+
496
509
  if (layout === "flex") {
497
510
  layoutClasses.push("flex");
498
511
  if (layoutOptions.wrap) layoutClasses.push("flex-wrap");
499
512
  if (layoutOptions.direction === "column") layoutClasses.push("flex-col");
500
513
  if (layoutOptions.gap) {
501
514
  // Check if gap is a CSS class name (e.g., 'md', 'lg') or a CSS value
502
- if (layoutOptions.gap.startsWith('var(') || layoutOptions.gap.includes('px') || layoutOptions.gap.includes('rem')) {
515
+ if (
516
+ layoutOptions.gap.startsWith("var(") ||
517
+ layoutOptions.gap.includes("px") ||
518
+ layoutOptions.gap.includes("rem")
519
+ ) {
503
520
  layoutStyle += `gap: ${layoutOptions.gap};`;
504
521
  } else {
505
522
  layoutClasses.push(`gap-${layoutOptions.gap}`);
@@ -516,45 +533,60 @@ export class SchemaForm extends LitElement {
516
533
  }
517
534
  if (layoutOptions.gap) {
518
535
  // Check if gap is a CSS class name (e.g., 'md', 'lg') or a CSS value
519
- if (layoutOptions.gap.startsWith('var(') || layoutOptions.gap.includes('px') || layoutOptions.gap.includes('rem')) {
536
+ if (
537
+ layoutOptions.gap.startsWith("var(") ||
538
+ layoutOptions.gap.includes("px") ||
539
+ layoutOptions.gap.includes("rem")
540
+ ) {
520
541
  layoutStyle += `gap: ${layoutOptions.gap};`;
521
542
  } else {
522
543
  layoutClasses.push(`gap-${layoutOptions.gap}`);
523
544
  }
524
545
  }
525
546
  }
526
-
527
- const fieldsetClass = layoutClasses.length > 0 ? layoutClasses.join(" ") : undefined;
528
-
547
+
548
+ const fieldsetClass =
549
+ layoutClasses.length > 0 ? layoutClasses.join(" ") : undefined;
550
+
529
551
  // Render basic fieldset
530
552
  const fieldsetContent = html`
531
- <fieldset data-path=${node.path} class=${ifDefined(fieldsetClass)} style=${ifDefined(layoutStyle || undefined)}>
532
- ${!this.hideLegend && !context.hideLegend ? html`<legend>${legend}</legend>` : nothing}
553
+ <fieldset
554
+ data-path=${node.path}
555
+ class=${ifDefined(fieldsetClass)}
556
+ style=${ifDefined(layoutStyle || undefined)}
557
+ >
558
+ ${!this.hideLegend && !context.hideLegend
559
+ ? html`<legend>${legend}</legend>`
560
+ : nothing}
533
561
  ${node.children.map((child) => this.#renderNode(child, context))}
534
562
  </fieldset>
535
563
  `;
536
-
564
+
537
565
  // Wrap in surface if specified
538
566
  if (surface) {
539
- const surfaceClass = surface === "card" || surface === "elevated" || surface === "dialog"
540
- ? `surface ${surface}`
541
- : "surface";
567
+ const surfaceClass =
568
+ surface === "card" || surface === "elevated" || surface === "dialog"
569
+ ? `surface ${surface}`
570
+ : "surface";
542
571
  return html`<div class=${surfaceClass}>${fieldsetContent}</div>`;
543
572
  }
544
-
573
+
545
574
  return fieldsetContent;
546
575
  }
547
576
 
548
577
  #renderFieldsetTabs(node, legend, ui) {
549
578
  const children = node.children || [];
550
579
  if (children.length === 0) return nothing;
551
-
580
+
552
581
  // Create tab panels from child fields
553
582
  return html`
554
583
  <pds-tabstrip label=${legend} data-path=${node.path}>
555
584
  ${children.map((child, idx) => {
556
585
  const childTitle = child.title ?? `Tab ${idx + 1}`;
557
- const childId = `${node.path}-tab-${idx}`.replace(/[^a-zA-Z0-9_-]/g, '-');
586
+ const childId = `${node.path}-tab-${idx}`.replace(
587
+ /[^a-zA-Z0-9_-]/g,
588
+ "-"
589
+ );
558
590
  return html`
559
591
  <pds-tabpanel id=${childId} label=${childTitle}>
560
592
  ${this.#renderNode(child)}
@@ -570,13 +602,17 @@ export class SchemaForm extends LitElement {
570
602
  if (children.length === 0) return nothing;
571
603
  const layoutOptions = ui?.["ui:layoutOptions"] || {};
572
604
  const openFirst = layoutOptions.openFirst ?? true;
573
-
605
+
574
606
  return html`
575
607
  <section class="accordion" data-path=${node.path}>
576
608
  ${children.map((child, idx) => {
577
609
  const childTitle = child.title ?? `Section ${idx + 1}`;
578
- const childId = `${node.path}-acc-${idx}`.replace(/[^a-zA-Z0-9_-]/g, '-');
579
- const isOpen = ui?.["ui:defaultOpen"]?.includes(idx) ?? (openFirst && idx === 0);
610
+ const childId = `${node.path}-acc-${idx}`.replace(
611
+ /[^a-zA-Z0-9_-]/g,
612
+ "-"
613
+ );
614
+ const isOpen =
615
+ ui?.["ui:defaultOpen"]?.includes(idx) ?? (openFirst && idx === 0);
580
616
  return html`
581
617
  <details ?open=${isOpen}>
582
618
  <summary id=${childId}>${childTitle}</summary>
@@ -595,22 +631,27 @@ export class SchemaForm extends LitElement {
595
631
  const title = node.title ?? "Edit";
596
632
  const ui = node.ui || this.#uiFor(path);
597
633
  const dialogOpts = ui?.["ui:dialogOptions"] || {};
598
- const buttonLabel = dialogOpts.buttonLabel || ui?.["ui:dialogButton"] || `Edit ${title}`;
634
+ const buttonLabel =
635
+ dialogOpts.buttonLabel || ui?.["ui:dialogButton"] || `Edit ${title}`;
599
636
  const dialogTitle = dialogOpts.dialogTitle || title;
600
-
637
+
601
638
  const openDialog = async () => {
602
639
  // Read current value from this.#data on each open (not captured at render time)
603
640
  const currentValue = this.#getByPath(this.#data, path) || {};
604
-
605
- console.log('Opening dialog for path:', path);
606
- console.log('Current this.#data:', this.#data);
607
- console.log('Current value at path:', currentValue);
608
-
609
- this.#emit("pw:dialog-open", { path, schema: node.schema, value: currentValue });
610
-
641
+
642
+ console.log("Opening dialog for path:", path);
643
+ console.log("Current this.#data:", this.#data);
644
+ console.log("Current value at path:", currentValue);
645
+
646
+ this.#emit("pw:dialog-open", {
647
+ path,
648
+ schema: node.schema,
649
+ value: currentValue,
650
+ });
651
+
611
652
  // Create a nested form schema for the dialog
612
653
  const dialogSchema = { ...node.schema, title: dialogTitle };
613
-
654
+
614
655
  try {
615
656
  // Use PDS.ask to show dialog with form - it returns FormData when useForm: true
616
657
  const formData = await window.PDS.ask(
@@ -629,27 +670,40 @@ export class SchemaForm extends LitElement {
629
670
  size: dialogOpts.size || "lg",
630
671
  buttons: {
631
672
  ok: { name: dialogOpts.submitLabel || "Save", primary: true },
632
- cancel: { name: dialogOpts.cancelLabel || "Cancel", cancel: true }
633
- }
673
+ cancel: {
674
+ name: dialogOpts.cancelLabel || "Cancel",
675
+ cancel: true,
676
+ },
677
+ },
634
678
  }
635
679
  );
636
-
680
+
637
681
  // formData is a FormData object if user clicked OK, null/false if cancelled
638
682
  if (formData && formData instanceof FormData) {
639
683
  // Convert FormData to nested object structure
640
684
  // Note: The nested form generates paths from its own root (e.g., /name, /email)
641
685
  // so we don't need to strip a basePath prefix
642
- const updatedValue = this.#formDataToObject(formData, "", dialogSchema);
643
-
644
- console.log('Updating path:', path, 'with value:', updatedValue);
645
- console.log('Before update - this.#data:', structuredClone(this.#data));
646
-
686
+ const updatedValue = this.#formDataToObject(
687
+ formData,
688
+ "",
689
+ dialogSchema
690
+ );
691
+
692
+ console.log("Updating path:", path, "with value:", updatedValue);
693
+ console.log(
694
+ "Before update - this.#data:",
695
+ structuredClone(this.#data)
696
+ );
697
+
647
698
  // Update the data at the dialog's path
648
699
  this.#setByPath(this.#data, path, updatedValue);
649
-
650
- console.log('After update - this.#data:', structuredClone(this.#data));
651
- console.log('Verify read back:', this.#getByPath(this.#data, path));
652
-
700
+
701
+ console.log(
702
+ "After update - this.#data:",
703
+ structuredClone(this.#data)
704
+ );
705
+ console.log("Verify read back:", this.#getByPath(this.#data, path));
706
+
653
707
  this.requestUpdate();
654
708
  this.#emit("pw:dialog-submit", { path, value: updatedValue });
655
709
  }
@@ -657,37 +711,43 @@ export class SchemaForm extends LitElement {
657
711
  console.error("Dialog error:", err);
658
712
  }
659
713
  };
660
-
714
+
661
715
  const buttonIcon = dialogOpts.icon;
662
-
716
+
663
717
  return html`
664
718
  <div class="dialog-field" data-path=${path}>
665
719
  <button type="button" class="btn" @click=${openDialog}>
666
- ${buttonIcon ? html`<pds-icon icon=${buttonIcon}></pds-icon>` : nothing}
720
+ ${buttonIcon
721
+ ? html`<pds-icon icon=${buttonIcon}></pds-icon>`
722
+ : nothing}
667
723
  ${buttonLabel}
668
724
  </button>
669
- <input type="hidden" name=${path} .value=${JSON.stringify(this.#getByPath(this.#data, path) || {})} />
725
+ <input
726
+ type="hidden"
727
+ name=${path}
728
+ .value=${JSON.stringify(this.#getByPath(this.#data, path) || {})}
729
+ />
670
730
  </div>
671
731
  `;
672
732
  }
673
-
733
+
674
734
  // Convert FormData to nested object, handling JSON pointer paths
675
735
  #formDataToObject(formData, basePath = "", schema = null) {
676
736
  const result = {};
677
737
  const arrays = new Map(); // Track array values from checkbox groups
678
-
738
+
679
739
  for (const [key, value] of formData.entries()) {
680
740
  // Remove basePath prefix if present to get relative path
681
741
  let relativePath = key;
682
742
  if (basePath && key.startsWith(basePath)) {
683
743
  relativePath = key.substring(basePath.length);
684
744
  }
685
-
745
+
686
746
  // Skip empty paths
687
747
  if (!relativePath || relativePath === "/") continue;
688
-
748
+
689
749
  // Handle array notation for checkbox groups (path[])
690
- if (relativePath.endsWith('[]')) {
750
+ if (relativePath.endsWith("[]")) {
691
751
  const arrayPath = relativePath.slice(0, -2);
692
752
  if (!arrays.has(arrayPath)) {
693
753
  arrays.set(arrayPath, []);
@@ -695,52 +755,61 @@ export class SchemaForm extends LitElement {
695
755
  arrays.get(arrayPath).push(value);
696
756
  continue;
697
757
  }
698
-
758
+
699
759
  // Convert value based on schema type if available
700
760
  let convertedValue = value;
701
- const fieldSchema = schema ? this.#schemaAtPath(schema, relativePath) : this.#schemaAt(relativePath);
702
-
761
+ const fieldSchema = schema
762
+ ? this.#schemaAtPath(schema, relativePath)
763
+ : this.#schemaAt(relativePath);
764
+
703
765
  if (fieldSchema) {
704
- if (fieldSchema.type === 'number') {
766
+ if (fieldSchema.type === "number") {
705
767
  convertedValue = parseFloat(value);
706
- } else if (fieldSchema.type === 'integer') {
768
+ } else if (fieldSchema.type === "integer") {
707
769
  convertedValue = parseInt(value, 10);
708
- } else if (fieldSchema.type === 'boolean') {
770
+ } else if (fieldSchema.type === "boolean") {
709
771
  // Checkbox inputs: if present in FormData, they're checked (true)
710
772
  // If not present, they're unchecked (false) - handled below
711
- convertedValue = value === 'on' || value === 'true' || value === true;
773
+ convertedValue = value === "on" || value === "true" || value === true;
712
774
  }
713
775
  }
714
-
776
+
715
777
  // Set value using JSON pointer path
716
778
  this.#setByPath(result, relativePath, convertedValue);
717
779
  }
718
-
780
+
719
781
  // Add array values from checkbox groups
720
782
  for (const [arrayPath, values] of arrays) {
721
783
  this.#setByPath(result, arrayPath, values);
722
784
  }
723
-
785
+
724
786
  // Handle unchecked checkboxes - they won't be in FormData
725
787
  // We need to set them to false based on schema
726
788
  this.#ensureCheckboxDefaults(result, basePath, schema);
727
-
789
+
728
790
  return result;
729
791
  }
730
-
792
+
731
793
  // Ensure boolean fields that weren't in FormData are set to false
732
794
  #ensureCheckboxDefaults(obj, basePath = "", schemaRoot = null) {
733
- const schema = schemaRoot ? this.#schemaAtPath(schemaRoot, basePath) : this.#schemaAt(basePath);
795
+ const schema = schemaRoot
796
+ ? this.#schemaAtPath(schemaRoot, basePath)
797
+ : this.#schemaAt(basePath);
734
798
  if (!schema) return;
735
-
736
- if (schema.type === 'object' && schema.properties) {
799
+
800
+ if (schema.type === "object" && schema.properties) {
737
801
  for (const [key, propSchema] of Object.entries(schema.properties)) {
738
802
  const propPath = basePath + "/" + this.#escapeJsonPointer(key);
739
- const relativePath = propPath.startsWith("/") ? propPath.substring(1) : propPath;
740
-
741
- if (propSchema.type === 'boolean' && this.#getByPath(obj, propPath) === undefined) {
803
+ const relativePath = propPath.startsWith("/")
804
+ ? propPath.substring(1)
805
+ : propPath;
806
+
807
+ if (
808
+ propSchema.type === "boolean" &&
809
+ this.#getByPath(obj, propPath) === undefined
810
+ ) {
742
811
  this.#setByPath(obj, propPath, false);
743
- } else if (propSchema.type === 'object') {
812
+ } else if (propSchema.type === "object") {
744
813
  this.#ensureCheckboxDefaults(obj, propPath, schemaRoot);
745
814
  }
746
815
  }
@@ -786,94 +855,122 @@ export class SchemaForm extends LitElement {
786
855
  const arr = this.#ensureArrayAtPath(path);
787
856
  const ui = node.ui || this.#uiFor(path);
788
857
  const itemSchema = node.item?.schema;
789
-
858
+
790
859
  // Check if this is a simple string array that should use the open group enhancement
791
- const isSimpleStringArray = itemSchema?.type === "string" &&
792
- !itemSchema.format &&
793
- !itemSchema.enum &&
794
- (!itemSchema.maxLength || itemSchema.maxLength <= 100);
795
-
860
+ const isSimpleStringArray =
861
+ itemSchema?.type === "string" &&
862
+ !itemSchema.format &&
863
+ !itemSchema.enum &&
864
+ (!itemSchema.maxLength || itemSchema.maxLength <= 100);
865
+
796
866
  // Check layout preference: use 'open' for simple string arrays by default, or if explicitly set
797
867
  let arrayLayout = ui?.["ui:arrayLayout"];
798
868
  if (!arrayLayout) {
799
869
  // If not explicitly set, use global option with smart default based on array type
800
- const globalDefault = this.#getOption('layouts.arrays', 'default');
801
- arrayLayout = globalDefault === 'default' && isSimpleStringArray ? 'open' : globalDefault;
870
+ const globalDefault = this.#getOption("layouts.arrays", "default");
871
+ arrayLayout =
872
+ globalDefault === "default" && isSimpleStringArray
873
+ ? "open"
874
+ : globalDefault;
802
875
  }
803
-
804
- const useOpenGroup = arrayLayout === 'open' && isSimpleStringArray;
805
-
876
+
877
+ const useOpenGroup = arrayLayout === "open" && isSimpleStringArray;
878
+
806
879
  // Check if this is a single-selection array (maxItems: 1) for radio group
807
880
  const isSingleSelection = node.schema?.maxItems === 1;
808
881
  const inputType = isSingleSelection ? "radio" : "checkbox";
809
-
882
+
810
883
  if (useOpenGroup) {
811
884
  // Render fieldset with data-open to let the enhancement handle UI
812
885
  // We sync state after the enhancement runs via MutationObserver
813
-
886
+
814
887
  const syncFromDOM = (fieldset) => {
815
888
  // Read current state from DOM (after enhancement has modified it)
816
- const inputs = Array.from(fieldset.querySelectorAll('input[type="radio"], input[type="checkbox"]'));
889
+ const inputs = Array.from(
890
+ fieldset.querySelectorAll(
891
+ 'input[type="radio"], input[type="checkbox"]'
892
+ )
893
+ );
817
894
  const values = inputs
818
- .map(input => input.value)
819
- .filter(v => v && v.trim());
820
-
895
+ .map((input) => input.value)
896
+ .filter((v) => v && v.trim());
897
+
821
898
  if (isSingleSelection) {
822
899
  // For radio groups, find the checked one
823
- const checkedInput = inputs.find(input => input.checked);
824
- this.#setByPath(this.#data, path, checkedInput && checkedInput.value ? [checkedInput.value] : []);
900
+ const checkedInput = inputs.find((input) => input.checked);
901
+ this.#setByPath(
902
+ this.#data,
903
+ path,
904
+ checkedInput && checkedInput.value ? [checkedInput.value] : []
905
+ );
825
906
  } else {
826
907
  // For checkbox groups, all items in DOM are in the array
827
908
  this.#setByPath(this.#data, path, values);
828
909
  }
829
- this.#emit("pw:array-change", { path, values: this.#getByPath(this.#data, path) });
910
+ this.#emit("pw:array-change", {
911
+ path,
912
+ values: this.#getByPath(this.#data, path),
913
+ });
830
914
  };
831
-
915
+
832
916
  const handleChange = (e) => {
833
917
  const fieldset = e.currentTarget;
834
918
  if (isSingleSelection) {
835
919
  // For radio groups, update to selected value immediately
836
- const checkedInput = fieldset.querySelector('input[type="radio"]:checked');
837
- this.#setByPath(this.#data, path, checkedInput && checkedInput.value ? [checkedInput.value] : []);
838
- this.#emit("pw:array-change", { path, values: this.#getByPath(this.#data, path) });
920
+ const checkedInput = fieldset.querySelector(
921
+ 'input[type="radio"]:checked'
922
+ );
923
+ this.#setByPath(
924
+ this.#data,
925
+ path,
926
+ checkedInput && checkedInput.value ? [checkedInput.value] : []
927
+ );
928
+ this.#emit("pw:array-change", {
929
+ path,
930
+ values: this.#getByPath(this.#data, path),
931
+ });
839
932
  }
840
933
  };
841
-
934
+
842
935
  const afterRender = (fieldset) => {
843
936
  // Observe DOM changes made by the data-open enhancement
844
937
  const observer = new MutationObserver(() => {
845
938
  syncFromDOM(fieldset);
846
939
  });
847
- observer.observe(fieldset, {
848
- childList: true,
849
- subtree: true
940
+ observer.observe(fieldset, {
941
+ childList: true,
942
+ subtree: true,
850
943
  });
851
-
944
+
852
945
  // Store observer for cleanup
853
946
  if (!fieldset._arrayObserver) {
854
947
  fieldset._arrayObserver = observer;
855
948
  }
856
949
  };
857
-
950
+
858
951
  const selectedValue = isSingleSelection && arr.length > 0 ? arr[0] : null;
859
-
952
+
860
953
  return html`
861
- <fieldset
954
+ <fieldset
862
955
  role="group"
863
956
  data-open
864
957
  data-path=${path}
865
958
  data-name=${path}
866
959
  @change=${handleChange}
867
- ${ref((el) => { if (el) afterRender(el); })}
960
+ ${ref((el) => {
961
+ if (el) afterRender(el);
962
+ })}
868
963
  >
869
964
  <legend>${node.title ?? "List"}</legend>
870
965
  ${arr.map((value, i) => {
871
966
  const id = `${path}-${i}`;
872
- const isChecked = isSingleSelection ? value === selectedValue : false;
967
+ const isChecked = isSingleSelection
968
+ ? value === selectedValue
969
+ : false;
873
970
  return html`
874
971
  <label for=${id}>
875
972
  <span data-label>${value}</span>
876
- <input
973
+ <input
877
974
  id=${id}
878
975
  type=${inputType}
879
976
  name=${path}
@@ -1014,13 +1111,18 @@ export class SchemaForm extends LitElement {
1014
1111
  // Wrap with icon if ui:icon is specified and enhancements.icons is enabled
1015
1112
  const iconName = ui?.["ui:icon"];
1016
1113
  const iconPos = ui?.["ui:iconPosition"] || "start";
1017
- if (iconName && this.#getOption('enhancements.icons', true)) {
1018
- const iconClasses = iconPos === "end" ? "input-icon input-icon-end" : "input-icon";
1114
+ if (iconName && this.#getOption("enhancements.icons", true)) {
1115
+ const iconClasses =
1116
+ iconPos === "end" ? "input-icon input-icon-end" : "input-icon";
1019
1117
  controlTpl = html`
1020
1118
  <div class=${iconClasses}>
1021
- ${iconPos === "start" ? html`<pds-icon icon=${iconName}></pds-icon>` : nothing}
1119
+ ${iconPos === "start"
1120
+ ? html`<pds-icon icon=${iconName}></pds-icon>`
1121
+ : nothing}
1022
1122
  ${controlTpl}
1023
- ${iconPos === "end" ? html`<pds-icon icon=${iconName}></pds-icon>` : nothing}
1123
+ ${iconPos === "end"
1124
+ ? html`<pds-icon icon=${iconName}></pds-icon>`
1125
+ : nothing}
1024
1126
  </div>
1025
1127
  `;
1026
1128
  }
@@ -1040,7 +1142,11 @@ export class SchemaForm extends LitElement {
1040
1142
  : undefined;
1041
1143
  const fieldsetClass = ui?.["ui:class"];
1042
1144
  return html`
1043
- <fieldset data-path=${path} role=${ifDefined(role)} class=${ifDefined(fieldsetClass)}>
1145
+ <fieldset
1146
+ data-path=${path}
1147
+ role=${ifDefined(role)}
1148
+ class=${ifDefined(fieldsetClass)}
1149
+ >
1044
1150
  <legend>${label}</legend>
1045
1151
  ${controlTpl} ${help ? html`<div data-help>${help}</div>` : nothing}
1046
1152
  </fieldset>
@@ -1054,25 +1160,22 @@ export class SchemaForm extends LitElement {
1054
1160
 
1055
1161
  // Add data-toggle for toggle switches
1056
1162
  const isToggle = node.widgetKey === "toggle";
1057
-
1163
+
1058
1164
  // Add range-output class for range inputs if enabled
1059
1165
  const isRange = node.widgetKey === "input-range";
1060
- const useRangeOutput = isRange && this.#getOption('enhancements.rangeOutput', true);
1166
+ const useRangeOutput =
1167
+ isRange && this.#getOption("enhancements.rangeOutput", true);
1061
1168
  const labelClass = useRangeOutput ? "range-output" : undefined;
1062
1169
 
1063
1170
  const renderControlAndLabel = (isToggle) => {
1064
- if(isToggle)
1065
- return html`${controlTpl} <span data-label>${label}</span>`;
1171
+ if (isToggle) return html`${controlTpl} <span data-label>${label}</span>`;
1066
1172
 
1067
1173
  return html`<span data-label>${label}</span> ${controlTpl}`;
1174
+ };
1068
1175
 
1069
- }
1070
-
1071
1176
  return html`
1072
- <label for=${id} ?data-toggle=${isToggle} class=${ifDefined(labelClass)}>
1073
-
1177
+ <label for=${id} ?data-toggle=${isToggle} class=${ifDefined(labelClass)}>
1074
1178
  ${renderControlAndLabel(isToggle)}
1075
-
1076
1179
  ${help ? html`<div data-help>${help}</div>` : nothing}
1077
1180
  </label>
1078
1181
  `;
@@ -1149,47 +1252,50 @@ export class SchemaForm extends LitElement {
1149
1252
  `
1150
1253
  );
1151
1254
 
1152
- this.defineRenderer("input-number", ({ id, path, value, attrs, set, schema }) => {
1153
- const step = attrs.step || getStep(value);
1154
- return html`
1155
- <input
1156
- id=${id}
1157
- name=${path}
1158
- type="number"
1159
- placeholder=${ifDefined(attrs.placeholder)}
1160
- .value=${value ?? ""}
1161
- min=${ifDefined(attrs.min)}
1162
- max=${ifDefined(attrs.max)}
1163
- step=${ifDefined(step)}
1164
- ?readonly=${!!attrs.readOnly}
1165
- ?required=${!!attrs.required}
1166
- @input=${(e) => {
1167
- const v = e.target.value;
1168
- const numValue =
1169
- schema.type === "integer" ? parseInt(v, 10) : parseFloat(v);
1170
- const step =
1171
- attrs.step ||
1172
- (numValue % 1 !== 0
1173
- ? `0.${"1".padStart(
1174
- numValue.toString().split(".")[1]?.length || 0,
1175
- "0"
1176
- )}`
1177
- : "1");
1178
- e.target.step = step; // Dynamically set step based on value precision
1179
- if (
1180
- (attrs.min != null && numValue < attrs.min) ||
1181
- (attrs.max != null && numValue > attrs.max) ||
1182
- (attrs.step != null && numValue % parseFloat(attrs.step) !== 0)
1183
- ) {
1184
- e.target.setCustomValidity("Invalid value");
1185
- } else {
1186
- e.target.setCustomValidity("");
1187
- set(numValue);
1188
- }
1189
- }}
1190
- />
1191
- `;
1192
- });
1255
+ this.defineRenderer(
1256
+ "input-number",
1257
+ ({ id, path, value, attrs, set, schema }) => {
1258
+ const step = attrs.step || getStep(value);
1259
+ return html`
1260
+ <input
1261
+ id=${id}
1262
+ name=${path}
1263
+ type="number"
1264
+ placeholder=${ifDefined(attrs.placeholder)}
1265
+ .value=${value ?? ""}
1266
+ min=${ifDefined(attrs.min)}
1267
+ max=${ifDefined(attrs.max)}
1268
+ step=${ifDefined(step)}
1269
+ ?readonly=${!!attrs.readOnly}
1270
+ ?required=${!!attrs.required}
1271
+ @input=${(e) => {
1272
+ const v = e.target.value;
1273
+ const numValue =
1274
+ schema.type === "integer" ? parseInt(v, 10) : parseFloat(v);
1275
+ const step =
1276
+ attrs.step ||
1277
+ (numValue % 1 !== 0
1278
+ ? `0.${"1".padStart(
1279
+ numValue.toString().split(".")[1]?.length || 0,
1280
+ "0"
1281
+ )}`
1282
+ : "1");
1283
+ e.target.step = step; // Dynamically set step based on value precision
1284
+ if (
1285
+ (attrs.min != null && numValue < attrs.min) ||
1286
+ (attrs.max != null && numValue > attrs.max) ||
1287
+ (attrs.step != null && numValue % parseFloat(attrs.step) !== 0)
1288
+ ) {
1289
+ e.target.setCustomValidity("Invalid value");
1290
+ } else {
1291
+ e.target.setCustomValidity("");
1292
+ set(numValue);
1293
+ }
1294
+ }}
1295
+ />
1296
+ `;
1297
+ }
1298
+ );
1193
1299
 
1194
1300
  // Range input renderer for ui:widget = 'input-range'
1195
1301
  this.defineRenderer(
@@ -1237,8 +1343,9 @@ export class SchemaForm extends LitElement {
1237
1343
  "input-password",
1238
1344
  ({ id, path, value, attrs, set, ui }) => {
1239
1345
  // Determine autocomplete value based on UI hints or use "current-password" as default
1240
- const autocomplete = ui?.["ui:autocomplete"] || attrs.autocomplete || "current-password";
1241
-
1346
+ const autocomplete =
1347
+ ui?.["ui:autocomplete"] || attrs.autocomplete || "current-password";
1348
+
1242
1349
  return html`
1243
1350
  <input
1244
1351
  id=${id}
@@ -1371,8 +1478,9 @@ export class SchemaForm extends LitElement {
1371
1478
  this.defineRenderer(
1372
1479
  "select",
1373
1480
  ({ id, path, value, attrs, set, schema, ui, host }) => {
1374
- const useDropdown = host.#getOption('widgets.selects', 'standard') === 'dropdown' ||
1375
- ui?.["ui:dropdown"] === true;
1481
+ const useDropdown =
1482
+ host.#getOption("widgets.selects", "standard") === "dropdown" ||
1483
+ ui?.["ui:dropdown"] === true;
1376
1484
  const enumValues = schema.enum || [];
1377
1485
  const enumLabels = schema.enumNames || enumValues;
1378
1486
  return html`
@@ -1386,7 +1494,10 @@ export class SchemaForm extends LitElement {
1386
1494
  >
1387
1495
  <option value="" ?selected=${value == null}>—</option>
1388
1496
  ${enumValues.map(
1389
- (v, i) => html`<option value=${String(v)}>${String(enumLabels[i])}</option>`
1497
+ (v, i) =>
1498
+ html`<option value=${String(v)}>
1499
+ ${String(enumLabels[i])}
1500
+ </option>`
1390
1501
  )}
1391
1502
  </select>
1392
1503
  `;
@@ -1395,34 +1506,31 @@ export class SchemaForm extends LitElement {
1395
1506
 
1396
1507
  // Radio group: returns ONLY the labeled inputs
1397
1508
  // Matches PDS pattern: input hidden, label styled as button
1398
- this.defineRenderer(
1399
- "radio",
1400
- ({ id, path, value, attrs, set, schema }) => {
1401
- const enumValues = schema.enum || [];
1402
- const enumLabels = schema.enumNames || enumValues;
1403
- return html`
1404
- ${enumValues.map((v, i) => {
1405
- const rid = `${id}-${i}`;
1406
- return html`
1407
- <label for=${rid}>
1408
- <input
1409
- id=${rid}
1410
- type="radio"
1411
- name=${path}
1412
- .value=${String(v)}
1413
- .checked=${String(value) === String(v)}
1414
- ?required=${!!attrs.required}
1415
- @change=${(e) => {
1416
- if (e.target.checked) set(enumValues[i]);
1417
- }}
1418
- />
1419
- ${String(enumLabels[i])}
1420
- </label>
1421
- `;
1422
- })}
1423
- `;
1424
- }
1425
- );
1509
+ this.defineRenderer("radio", ({ id, path, value, attrs, set, schema }) => {
1510
+ const enumValues = schema.enum || [];
1511
+ const enumLabels = schema.enumNames || enumValues;
1512
+ return html`
1513
+ ${enumValues.map((v, i) => {
1514
+ const rid = `${id}-${i}`;
1515
+ return html`
1516
+ <label for=${rid}>
1517
+ <input
1518
+ id=${rid}
1519
+ type="radio"
1520
+ name=${path}
1521
+ .value=${String(v)}
1522
+ .checked=${String(value) === String(v)}
1523
+ ?required=${!!attrs.required}
1524
+ @change=${(e) => {
1525
+ if (e.target.checked) set(enumValues[i]);
1526
+ }}
1527
+ />
1528
+ ${String(enumLabels[i])}
1529
+ </label>
1530
+ `;
1531
+ })}
1532
+ `;
1533
+ });
1426
1534
 
1427
1535
  // Checkbox group: for multi-select from enum (array type with enum items)
1428
1536
  // Shows actual checkboxes (not button-style like radios)
@@ -1431,7 +1539,8 @@ export class SchemaForm extends LitElement {
1431
1539
  ({ id, path, value, attrs, set, schema }) => {
1432
1540
  const selected = Array.isArray(value) ? value : [];
1433
1541
  const options = schema.items?.enum || schema.enum || [];
1434
- const optionLabels = schema.items?.enumNames || schema.enumNames || options;
1542
+ const optionLabels =
1543
+ schema.items?.enumNames || schema.enumNames || options;
1435
1544
 
1436
1545
  return html`
1437
1546
  ${options.map((v, i) => {
@@ -1476,45 +1585,41 @@ export class SchemaForm extends LitElement {
1476
1585
  );
1477
1586
 
1478
1587
  // pds-upload: File upload component
1479
- this.defineRenderer(
1480
- "upload",
1481
- ({ id, value, attrs, set, ui, path }) => {
1482
- const uploadOpts = ui?.["ui:options"] || {};
1483
- return html`
1484
- <pds-upload
1485
- id=${id}
1486
- accept=${ifDefined(uploadOpts.accept)}
1487
- ?multiple=${uploadOpts.multiple ?? false}
1488
- max-files=${ifDefined(uploadOpts.maxFiles)}
1489
- max-size=${ifDefined(uploadOpts.maxSize)}
1490
- label=${ifDefined(uploadOpts.label)}
1491
- ?required=${!!attrs.required}
1492
- @pw:change=${(e) => set(e.detail.files)}
1493
- ></pds-upload>
1494
- `;
1495
- }
1496
- );
1588
+ this.defineRenderer("upload", ({ id, value, attrs, set, ui, path }) => {
1589
+ const uploadOpts = ui?.["ui:options"] || {};
1590
+ return html`
1591
+ <pds-upload
1592
+ id=${id}
1593
+ accept=${ifDefined(uploadOpts.accept)}
1594
+ ?multiple=${uploadOpts.multiple ?? false}
1595
+ max-files=${ifDefined(uploadOpts.maxFiles)}
1596
+ max-size=${ifDefined(uploadOpts.maxSize)}
1597
+ label=${ifDefined(uploadOpts.label)}
1598
+ ?required=${!!attrs.required}
1599
+ @pw:change=${(e) => set(e.detail.files)}
1600
+ ></pds-upload>
1601
+ `;
1602
+ });
1497
1603
 
1498
1604
  // pds-richtext: Rich text editor
1499
- this.defineRenderer(
1500
- "richtext",
1501
- ({ id, value, attrs, set, ui, path }) => {
1502
- const richtextOpts = ui?.["ui:options"] || {};
1503
- return html`
1504
- <pds-richtext
1505
- id=${id}
1506
- name=${path}
1507
- placeholder=${ifDefined(richtextOpts.placeholder || attrs.placeholder)}
1508
- .value=${value ?? ""}
1509
- toolbar=${ifDefined(richtextOpts.toolbar)}
1510
- ?required=${!!attrs.required}
1511
- ?submit-on-enter=${richtextOpts.submitOnEnter ?? false}
1512
- spellcheck=${richtextOpts.spellcheck ?? true ? "true" : "false"}
1513
- @input=${(e) => set(e.target.value)}
1514
- ></pds-richtext>
1515
- `;
1516
- }
1517
- );
1605
+ this.defineRenderer("richtext", ({ id, value, attrs, set, ui, path }) => {
1606
+ const richtextOpts = ui?.["ui:options"] || {};
1607
+ return html`
1608
+ <pds-richtext
1609
+ id=${id}
1610
+ name=${path}
1611
+ placeholder=${ifDefined(
1612
+ richtextOpts.placeholder || attrs.placeholder
1613
+ )}
1614
+ .value=${value ?? ""}
1615
+ toolbar=${ifDefined(richtextOpts.toolbar)}
1616
+ ?required=${!!attrs.required}
1617
+ ?submit-on-enter=${richtextOpts.submitOnEnter ?? false}
1618
+ spellcheck=${richtextOpts.spellcheck ?? true ? "true" : "false"}
1619
+ @input=${(e) => set(e.target.value)}
1620
+ ></pds-richtext>
1621
+ `;
1622
+ });
1518
1623
  }
1519
1624
 
1520
1625
  // ===== Form submit =====
@@ -1549,34 +1654,34 @@ export class SchemaForm extends LitElement {
1549
1654
  // ===== Utilities =====
1550
1655
  #uiFor(path) {
1551
1656
  if (!this.uiSchema) return undefined;
1552
-
1657
+
1553
1658
  // Try exact match first (flat structure)
1554
1659
  if (this.uiSchema[path]) return this.uiSchema[path];
1555
-
1660
+
1556
1661
  // Try with leading slash
1557
1662
  const withSlash = this.#asRel(path);
1558
1663
  if (this.uiSchema[withSlash]) return this.uiSchema[withSlash];
1559
-
1664
+
1560
1665
  // Try without leading slash for convenience
1561
1666
  const withoutSlash = path.startsWith("/") ? path.substring(1) : path;
1562
1667
  if (this.uiSchema[withoutSlash]) return this.uiSchema[withoutSlash];
1563
-
1668
+
1564
1669
  // Try nested navigation (e.g., userProfile/settings/preferences/theme)
1565
1670
  // Skip array indices (numeric parts and wildcard *) when navigating UI schema
1566
- const parts = path.replace(/^\//, '').split('/');
1671
+ const parts = path.replace(/^\//, "").split("/");
1567
1672
  let current = this.uiSchema;
1568
1673
  for (const part of parts) {
1569
1674
  // Skip numeric array indices and wildcard in UI schema navigation
1570
- if (/^\d+$/.test(part) || part === '*') continue;
1571
-
1572
- if (current && typeof current === 'object' && part in current) {
1675
+ if (/^\d+$/.test(part) || part === "*") continue;
1676
+
1677
+ if (current && typeof current === "object" && part in current) {
1573
1678
  current = current[part];
1574
1679
  } else {
1575
1680
  return undefined;
1576
1681
  }
1577
1682
  }
1578
1683
  // Only return if we found a UI config object (not a nested parent)
1579
- return current && typeof current === 'object' ? current : undefined;
1684
+ return current && typeof current === "object" ? current : undefined;
1580
1685
  }
1581
1686
  #asRel(path) {
1582
1687
  return path.startsWith("/") ? path : "/" + path;
@@ -1598,10 +1703,14 @@ export class SchemaForm extends LitElement {
1598
1703
 
1599
1704
  #nativeConstraints(path, schema) {
1600
1705
  // Use placeholder if explicitly set, otherwise use first example as placeholder
1601
- const attrs = {
1602
- placeholder: schema.placeholder || (schema.examples && schema.examples.length > 0 ? schema.examples[0] : undefined)
1706
+ const attrs = {
1707
+ placeholder:
1708
+ schema.placeholder ||
1709
+ (schema.examples && schema.examples.length > 0
1710
+ ? schema.examples[0]
1711
+ : undefined),
1603
1712
  };
1604
-
1713
+
1605
1714
  if (schema.type === "string") {
1606
1715
  if (schema.minLength != null) attrs.minLength = schema.minLength;
1607
1716
  if (schema.maxLength != null) attrs.maxLength = schema.maxLength;
@@ -1643,7 +1752,7 @@ export class SchemaForm extends LitElement {
1643
1752
  #schemaAt(path) {
1644
1753
  return this.#schemaAtPath(this.jsonSchema, path);
1645
1754
  }
1646
-
1755
+
1647
1756
  #schemaAtPath(schemaRoot, path) {
1648
1757
  let cur = schemaRoot;
1649
1758
  for (const seg of path.split("/").filter(Boolean)) {