@structured-field/widget-editor 1.2.2 → 1.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.
@@ -1,4 +1,4 @@
1
- var script$e = {
1
+ var script$f = {
2
2
  name: 'StringEditor',
3
3
  props: {
4
4
  schema: { type: Object, required: true },
@@ -6765,7 +6765,7 @@ class VueElement extends BaseClass {
6765
6765
  _update() {
6766
6766
  const vnode = this._createVNode();
6767
6767
  if (this._app) vnode.appContext = this._app._context;
6768
- render$e(vnode, this._root);
6768
+ render$f(vnode, this._root);
6769
6769
  }
6770
6770
  _createVNode() {
6771
6771
  const baseProps = {};
@@ -7032,7 +7032,7 @@ let renderer;
7032
7032
  function ensureRenderer() {
7033
7033
  return renderer || (renderer = createRenderer(rendererOptions));
7034
7034
  }
7035
- const render$e = ((...args) => {
7035
+ const render$f = ((...args) => {
7036
7036
  ensureRenderer().render(...args);
7037
7037
  });
7038
7038
  const createApp = ((...args) => {
@@ -7073,18 +7073,18 @@ function normalizeContainer(container) {
7073
7073
  return container;
7074
7074
  }
7075
7075
 
7076
- const _hoisted_1$c = {
7076
+ const _hoisted_1$d = {
7077
7077
  key: 0,
7078
7078
  class: "sf-null-badge"
7079
7079
  };
7080
- const _hoisted_2$a = ["value", "placeholder"];
7081
- const _hoisted_3$a = ["value", "placeholder"];
7080
+ const _hoisted_2$b = ["value", "placeholder"];
7081
+ const _hoisted_3$b = ["value", "placeholder"];
7082
7082
  const _hoisted_4$7 = {
7083
7083
  key: 0,
7084
7084
  class: "errorlist"
7085
7085
  };
7086
7086
 
7087
- function render$d(_ctx, _cache, $props, $setup, $data, $options) {
7087
+ function render$e(_ctx, _cache, $props, $setup, $data, $options) {
7088
7088
  return (openBlock(), createElementBlock("div", {
7089
7089
  class: normalizeClass(["sf-field", { errors: $options.fieldErrors.length }])
7090
7090
  }, [
@@ -7093,7 +7093,7 @@ function render$d(_ctx, _cache, $props, $setup, $data, $options) {
7093
7093
  }, [
7094
7094
  createTextVNode(toDisplayString($options.title) + " ", 1 /* TEXT */),
7095
7095
  ($options.isNullable && $options.isNullValue)
7096
- ? (openBlock(), createElementBlock("span", _hoisted_1$c, "null"))
7096
+ ? (openBlock(), createElementBlock("span", _hoisted_1$d, "null"))
7097
7097
  : createCommentVNode("v-if", true)
7098
7098
  ], 2 /* CLASS */),
7099
7099
  createBaseVNode("div", {
@@ -7107,7 +7107,7 @@ function render$d(_ctx, _cache, $props, $setup, $data, $options) {
7107
7107
  value: $options.isNullValue ? '' : $props.modelValue,
7108
7108
  placeholder: $options.isNullValue ? 'null' : ($props.schema.placeholder || ''),
7109
7109
  onInput: _cache[0] || (_cache[0] = $event => (_ctx.$emit('update:modelValue', $event.target.value)))
7110
- }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_2$a))
7110
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_2$b))
7111
7111
  : (openBlock(), createElementBlock("input", {
7112
7112
  key: 1,
7113
7113
  type: "text",
@@ -7115,7 +7115,7 @@ function render$d(_ctx, _cache, $props, $setup, $data, $options) {
7115
7115
  value: $options.isNullValue ? '' : ($props.modelValue != null ? String($props.modelValue) : ''),
7116
7116
  placeholder: $options.isNullValue ? 'null' : ($props.schema.placeholder || ''),
7117
7117
  onInput: _cache[1] || (_cache[1] = $event => (_ctx.$emit('update:modelValue', $event.target.value)))
7118
- }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_3$a)),
7118
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_3$b)),
7119
7119
  ($options.isNullable && !$options.isNullValue)
7120
7120
  ? (openBlock(), createElementBlock("button", {
7121
7121
  key: 2,
@@ -7136,10 +7136,10 @@ function render$d(_ctx, _cache, $props, $setup, $data, $options) {
7136
7136
  ], 2 /* CLASS */))
7137
7137
  }
7138
7138
 
7139
- script$e.render = render$d;
7140
- script$e.__file = "src/editors/StringEditor.vue";
7139
+ script$f.render = render$e;
7140
+ script$f.__file = "src/editors/StringEditor.vue";
7141
7141
 
7142
- var script$d = {
7142
+ var script$e = {
7143
7143
  name: 'NumberEditor',
7144
7144
  props: {
7145
7145
  schema: { type: Object, required: true },
@@ -7186,17 +7186,17 @@ var script$d = {
7186
7186
  },
7187
7187
  };
7188
7188
 
7189
- const _hoisted_1$b = {
7189
+ const _hoisted_1$c = {
7190
7190
  key: 0,
7191
7191
  class: "sf-null-badge"
7192
7192
  };
7193
- const _hoisted_2$9 = ["step", "min", "max", "value", "placeholder"];
7194
- const _hoisted_3$9 = {
7193
+ const _hoisted_2$a = ["step", "min", "max", "value", "placeholder"];
7194
+ const _hoisted_3$a = {
7195
7195
  key: 0,
7196
7196
  class: "errorlist"
7197
7197
  };
7198
7198
 
7199
- function render$c(_ctx, _cache, $props, $setup, $data, $options) {
7199
+ function render$d(_ctx, _cache, $props, $setup, $data, $options) {
7200
7200
  return (openBlock(), createElementBlock("div", {
7201
7201
  class: normalizeClass(["sf-field", { errors: $options.fieldErrors.length }])
7202
7202
  }, [
@@ -7205,7 +7205,7 @@ function render$c(_ctx, _cache, $props, $setup, $data, $options) {
7205
7205
  }, [
7206
7206
  createTextVNode(toDisplayString($options.title) + " ", 1 /* TEXT */),
7207
7207
  ($options.isNullable && $options.isNullValue)
7208
- ? (openBlock(), createElementBlock("span", _hoisted_1$b, "null"))
7208
+ ? (openBlock(), createElementBlock("span", _hoisted_1$c, "null"))
7209
7209
  : createCommentVNode("v-if", true)
7210
7210
  ], 2 /* CLASS */),
7211
7211
  createBaseVNode("div", {
@@ -7220,7 +7220,7 @@ function render$c(_ctx, _cache, $props, $setup, $data, $options) {
7220
7220
  value: $options.isNullValue ? '' : $props.modelValue,
7221
7221
  placeholder: $options.isNullValue ? 'null' : undefined,
7222
7222
  onInput: _cache[0] || (_cache[0] = (...args) => ($options.onInput && $options.onInput(...args)))
7223
- }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_2$9),
7223
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_2$a),
7224
7224
  ($options.isNullable && !$options.isNullValue)
7225
7225
  ? (openBlock(), createElementBlock("button", {
7226
7226
  key: 0,
@@ -7232,7 +7232,7 @@ function render$c(_ctx, _cache, $props, $setup, $data, $options) {
7232
7232
  : createCommentVNode("v-if", true)
7233
7233
  ], 2 /* CLASS */),
7234
7234
  ($options.fieldErrors.length)
7235
- ? (openBlock(), createElementBlock("ul", _hoisted_3$9, [
7235
+ ? (openBlock(), createElementBlock("ul", _hoisted_3$a, [
7236
7236
  (openBlock(true), createElementBlock(Fragment, null, renderList($options.fieldErrors, (err, i) => {
7237
7237
  return (openBlock(), createElementBlock("li", { key: i }, toDisplayString(err), 1 /* TEXT */))
7238
7238
  }), 128 /* KEYED_FRAGMENT */))
@@ -7241,10 +7241,10 @@ function render$c(_ctx, _cache, $props, $setup, $data, $options) {
7241
7241
  ], 2 /* CLASS */))
7242
7242
  }
7243
7243
 
7244
- script$d.render = render$c;
7245
- script$d.__file = "src/editors/NumberEditor.vue";
7244
+ script$e.render = render$d;
7245
+ script$e.__file = "src/editors/NumberEditor.vue";
7246
7246
 
7247
- var script$c = {
7247
+ var script$d = {
7248
7248
  name: 'BooleanEditor',
7249
7249
  props: {
7250
7250
  schema: { type: Object, required: true },
@@ -7276,9 +7276,9 @@ var script$c = {
7276
7276
  },
7277
7277
  };
7278
7278
 
7279
- const _hoisted_1$a = { class: "sf-boolean-row" };
7280
- const _hoisted_2$8 = { class: "sf-checkbox-label" };
7281
- const _hoisted_3$8 = ["checked"];
7279
+ const _hoisted_1$b = { class: "sf-boolean-row" };
7280
+ const _hoisted_2$9 = { class: "sf-checkbox-label" };
7281
+ const _hoisted_3$9 = ["checked"];
7282
7282
  const _hoisted_4$6 = {
7283
7283
  key: 0,
7284
7284
  class: "sf-null-badge"
@@ -7288,18 +7288,18 @@ const _hoisted_5$6 = {
7288
7288
  class: "errorlist"
7289
7289
  };
7290
7290
 
7291
- function render$b(_ctx, _cache, $props, $setup, $data, $options) {
7291
+ function render$c(_ctx, _cache, $props, $setup, $data, $options) {
7292
7292
  return (openBlock(), createElementBlock("div", {
7293
7293
  class: normalizeClass(["sf-field sf-field-boolean", { errors: $options.fieldErrors.length }])
7294
7294
  }, [
7295
- createBaseVNode("div", _hoisted_1$a, [
7296
- createBaseVNode("label", _hoisted_2$8, [
7295
+ createBaseVNode("div", _hoisted_1$b, [
7296
+ createBaseVNode("label", _hoisted_2$9, [
7297
7297
  createBaseVNode("input", {
7298
7298
  type: "checkbox",
7299
7299
  class: "sf-checkbox",
7300
7300
  checked: !!$props.modelValue,
7301
7301
  onChange: _cache[0] || (_cache[0] = $event => (_ctx.$emit('update:modelValue', $event.target.checked)))
7302
- }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_3$8),
7302
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_3$9),
7303
7303
  createTextVNode(" " + toDisplayString($options.title) + " ", 1 /* TEXT */),
7304
7304
  ($options.isNullable && $options.isNullValue)
7305
7305
  ? (openBlock(), createElementBlock("span", _hoisted_4$6, "null"))
@@ -7325,8 +7325,192 @@ function render$b(_ctx, _cache, $props, $setup, $data, $options) {
7325
7325
  ], 2 /* CLASS */))
7326
7326
  }
7327
7327
 
7328
+ script$d.render = render$c;
7329
+ script$d.__file = "src/editors/BooleanEditor.vue";
7330
+
7331
+ var script$c = {
7332
+ name: 'DateEditor',
7333
+ props: {
7334
+ schema: { type: Object, required: true },
7335
+ modelValue: { default: '' },
7336
+ path: { type: Array, default: () => [] },
7337
+ form: { type: Object, default: null },
7338
+ },
7339
+ emits: ['update:modelValue'],
7340
+ computed: {
7341
+ isDateTime() {
7342
+ return this.schema.format === 'date-time';
7343
+ },
7344
+ inputType() {
7345
+ return this.isDateTime ? 'datetime-local' : 'date';
7346
+ },
7347
+ displayValue() {
7348
+ if (this.isNullValue) return '';
7349
+ const v = String(this.modelValue);
7350
+ if (this.isDateTime) {
7351
+ // Accept ISO 8601 strings like "2026-04-09T10:30:00[.sss][Z|+00:00]"
7352
+ // datetime-local expects "YYYY-MM-DDTHH:mm" (or with seconds).
7353
+ const match = v.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})(?::(\d{2}(?:\.\d+)?))?/);
7354
+ if (match) {
7355
+ return match[3] ? `${match[1]}T${match[2]}:${match[3]}` : `${match[1]}T${match[2]}`;
7356
+ }
7357
+ return v;
7358
+ }
7359
+ // date: expect "YYYY-MM-DD"
7360
+ return v.slice(0, 10);
7361
+ },
7362
+ title() {
7363
+ return this.schema.title || this.humanize(this.path[this.path.length - 1]) || '';
7364
+ },
7365
+ isRequired() {
7366
+ if (this.path.length < 2 || !this.form) return false;
7367
+ const parentPath = this.path.slice(0, -1);
7368
+ const fieldName = this.path[this.path.length - 1];
7369
+ const parentSchema = this.form.getSchemaAtPath(parentPath);
7370
+ return parentSchema && Array.isArray(parentSchema.required) && parentSchema.required.includes(fieldName);
7371
+ },
7372
+ isNullable() {
7373
+ return !!this.schema._nullable;
7374
+ },
7375
+ isNullValue() {
7376
+ return this.modelValue === null || this.modelValue === undefined;
7377
+ },
7378
+ fieldErrors() {
7379
+ if (!this.form || !this.form.getErrorsForPath) return [];
7380
+ return this.form.getErrorsForPath(this.path);
7381
+ },
7382
+ },
7383
+ methods: {
7384
+ onInput(e) {
7385
+ const val = e.target.value;
7386
+ if (val === '') {
7387
+ this.$emit('update:modelValue', this.isNullable ? null : '');
7388
+ return;
7389
+ }
7390
+ // Emit ISO-compatible string. Pydantic accepts both "YYYY-MM-DD"
7391
+ // and "YYYY-MM-DDTHH:mm[:ss]" for date / datetime fields.
7392
+ this.$emit('update:modelValue', val);
7393
+ },
7394
+ humanize(str) {
7395
+ if (!str) return '';
7396
+ return str.replace(/_/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, s => s.toUpperCase());
7397
+ },
7398
+ },
7399
+ };
7400
+
7401
+ const _hoisted_1$a = {
7402
+ key: 0,
7403
+ class: "sf-null-badge"
7404
+ };
7405
+ const _hoisted_2$8 = ["type", "value", "placeholder", "min", "max"];
7406
+ const _hoisted_3$8 = {
7407
+ key: 0,
7408
+ class: "errorlist"
7409
+ };
7410
+
7411
+ function render$b(_ctx, _cache, $props, $setup, $data, $options) {
7412
+ return (openBlock(), createElementBlock("div", {
7413
+ class: normalizeClass(["sf-field", { errors: $options.fieldErrors.length }])
7414
+ }, [
7415
+ createBaseVNode("span", {
7416
+ class: normalizeClass(["sf-label", { required: $options.isRequired }])
7417
+ }, [
7418
+ createTextVNode(toDisplayString($options.title) + " ", 1 /* TEXT */),
7419
+ ($options.isNullable && $options.isNullValue)
7420
+ ? (openBlock(), createElementBlock("span", _hoisted_1$a, "null"))
7421
+ : createCommentVNode("v-if", true)
7422
+ ], 2 /* CLASS */),
7423
+ createBaseVNode("div", {
7424
+ class: normalizeClass($options.isNullable ? 'sf-input-row' : null)
7425
+ }, [
7426
+ createBaseVNode("input", {
7427
+ type: $options.inputType,
7428
+ class: "sf-input",
7429
+ value: $options.displayValue,
7430
+ placeholder: $options.isNullValue ? 'null' : ($props.schema.placeholder || ''),
7431
+ min: $props.schema.minimum || $props.schema.formatMinimum || null,
7432
+ max: $props.schema.maximum || $props.schema.formatMaximum || null,
7433
+ onInput: _cache[0] || (_cache[0] = (...args) => ($options.onInput && $options.onInput(...args)))
7434
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_2$8),
7435
+ ($options.isNullable && !$options.isNullValue)
7436
+ ? (openBlock(), createElementBlock("button", {
7437
+ key: 0,
7438
+ type: "button",
7439
+ class: "sf-null-clear-btn",
7440
+ title: "Set to null",
7441
+ onClick: _cache[1] || (_cache[1] = $event => (_ctx.$emit('update:modelValue', null)))
7442
+ }, "✕"))
7443
+ : createCommentVNode("v-if", true)
7444
+ ], 2 /* CLASS */),
7445
+ ($options.fieldErrors.length)
7446
+ ? (openBlock(), createElementBlock("ul", _hoisted_3$8, [
7447
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.fieldErrors, (err, i) => {
7448
+ return (openBlock(), createElementBlock("li", { key: i }, toDisplayString(err), 1 /* TEXT */))
7449
+ }), 128 /* KEYED_FRAGMENT */))
7450
+ ]))
7451
+ : createCommentVNode("v-if", true)
7452
+ ], 2 /* CLASS */))
7453
+ }
7454
+
7328
7455
  script$c.render = render$b;
7329
- script$c.__file = "src/editors/BooleanEditor.vue";
7456
+ script$c.__file = "src/editors/DateEditor.vue";
7457
+
7458
+ function debounce(fn, delay) {
7459
+ let timer;
7460
+ return function (...args) {
7461
+ clearTimeout(timer);
7462
+ timer = setTimeout(() => fn.apply(this, args), delay);
7463
+ };
7464
+ }
7465
+
7466
+ function deepClone(obj) {
7467
+ if (obj === null || typeof obj !== 'object') return obj;
7468
+ if (Array.isArray(obj)) return obj.map(deepClone);
7469
+ const clone = {};
7470
+ for (const key of Object.keys(obj)) {
7471
+ clone[key] = deepClone(obj[key]);
7472
+ }
7473
+ return clone;
7474
+ }
7475
+
7476
+ function isChoiceOneOf(list) {
7477
+ // A "choice list" oneOf: every member is a {const, title?} value option
7478
+ // (the shape emitted for enumerated choices, e.g. metaobjects' select
7479
+ // kind), as opposed to a oneOf of alternative sub-schemas.
7480
+ return (
7481
+ Array.isArray(list) &&
7482
+ list.length > 0 &&
7483
+ list.every(
7484
+ (m) =>
7485
+ m &&
7486
+ typeof m === 'object' &&
7487
+ 'const' in m &&
7488
+ !('properties' in m) &&
7489
+ m.type !== 'null'
7490
+ )
7491
+ );
7492
+ }
7493
+
7494
+ function getDefaultForSchema(schema) {
7495
+ if ('default' in schema) return deepClone(schema.default);
7496
+ if (schema.type === 'object') {
7497
+ const obj = {};
7498
+ for (const [key, prop] of Object.entries(schema.properties || {})) {
7499
+ if ('default' in prop) obj[key] = deepClone(prop.default);
7500
+ else if (prop.type === 'string') obj[key] = '';
7501
+ else if (prop.type === 'integer' || prop.type === 'number') obj[key] = 0;
7502
+ else if (prop.type === 'boolean') obj[key] = false;
7503
+ else if (prop.type === 'array') obj[key] = [];
7504
+ }
7505
+ return obj;
7506
+ }
7507
+ if (schema.type === 'array') return [];
7508
+ if (schema.type === 'string') return '';
7509
+ if (schema.type === 'integer' || schema.type === 'number') return 0;
7510
+ if (schema.type === 'boolean') return false;
7511
+ if (schema.type === 'relation') return schema.multiple ? [] : null;
7512
+ return null;
7513
+ }
7330
7514
 
7331
7515
  var script$b = {
7332
7516
  name: 'SelectEditor',
@@ -7338,6 +7522,20 @@ var script$b = {
7338
7522
  },
7339
7523
  emits: ['update:modelValue'],
7340
7524
  computed: {
7525
+ options() {
7526
+ // Two source shapes: a choice-list oneOf ({const, title} pairs, e.g.
7527
+ // metaobjects' select kind — labels preserved) or a plain enum.
7528
+ if (isChoiceOneOf(this.schema.oneOf)) {
7529
+ return this.schema.oneOf.map((o) => ({
7530
+ value: o.const,
7531
+ label: o.title != null ? o.title : String(o.const),
7532
+ }));
7533
+ }
7534
+ return (this.schema.enum || []).map((v) => ({ value: v, label: String(v) }));
7535
+ },
7536
+ selectedIndex() {
7537
+ return this.options.findIndex((o) => o.value === this.modelValue);
7538
+ },
7341
7539
  title() {
7342
7540
  return this.schema.title || this.humanize(this.path[this.path.length - 1]) || '';
7343
7541
  },
@@ -7360,6 +7558,13 @@ var script$b = {
7360
7558
  },
7361
7559
  },
7362
7560
  methods: {
7561
+ onChange(indexStr) {
7562
+ const opt = this.options[Number(indexStr)];
7563
+ // Emit the ORIGINAL option value (options are addressed by index in
7564
+ // the DOM), so integer/boolean enums keep their native type instead
7565
+ // of being stringified by the <select> element.
7566
+ if (opt !== undefined) this.$emit('update:modelValue', opt.value);
7567
+ },
7363
7568
  humanize(str) {
7364
7569
  if (!str) return '';
7365
7570
  return str.replace(/_/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, s => s.toUpperCase());
@@ -7401,17 +7606,17 @@ function render$a(_ctx, _cache, $props, $setup, $data, $options) {
7401
7606
  }, [
7402
7607
  createBaseVNode("select", {
7403
7608
  class: "sf-input sf-select",
7404
- value: $options.isNullValue ? '' : ($props.modelValue != null ? String($props.modelValue) : ''),
7405
- onChange: _cache[0] || (_cache[0] = $event => (_ctx.$emit('update:modelValue', $event.target.value)))
7609
+ value: $options.selectedIndex === -1 ? '' : String($options.selectedIndex),
7610
+ onChange: _cache[0] || (_cache[0] = $event => ($options.onChange($event.target.value)))
7406
7611
  }, [
7407
- ($options.isNullable && $options.isNullValue)
7408
- ? (openBlock(), createElementBlock("option", _hoisted_3$7, "null"))
7612
+ ($options.selectedIndex === -1)
7613
+ ? (openBlock(), createElementBlock("option", _hoisted_3$7, toDisplayString($options.isNullValue ? 'null' : ''), 1 /* TEXT */))
7409
7614
  : createCommentVNode("v-if", true),
7410
- (openBlock(true), createElementBlock(Fragment, null, renderList(($props.schema.enum || []), (opt) => {
7615
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.options, (opt, i) => {
7411
7616
  return (openBlock(), createElementBlock("option", {
7412
- key: opt,
7413
- value: String(opt)
7414
- }, toDisplayString(opt), 9 /* TEXT, PROPS */, _hoisted_4$5))
7617
+ key: i,
7618
+ value: String(i)
7619
+ }, toDisplayString(opt.label), 9 /* TEXT, PROPS */, _hoisted_4$5))
7415
7620
  }), 128 /* KEYED_FRAGMENT */))
7416
7621
  ], 40 /* PROPS, NEED_HYDRATION */, _hoisted_2$7),
7417
7622
  ($options.isNullable && !$options.isNullValue)
@@ -7818,6 +8023,121 @@ function hasConditionals(schema) {
7818
8023
  return false;
7819
8024
  }
7820
8025
 
8026
+ // Field-size classification for the multicolumn flow layout.
8027
+ // Sizes are flex-basis tokens (see scss/components/layout.scss), not column
8028
+ // counts: columns emerge from how many cells fit the container's width.
8029
+ // Invariant: visual order === DOM order === tab order. Never reorder cells.
8030
+
8031
+ const SIZES = ['xs', 'sm', 'md', 'lg', 'full'];
8032
+ const BREAKS = ['before', 'after', 'both'];
8033
+
8034
+ // Schema-author hint: `layout: 'sm'` or `layout: { size: 'sm', break: 'before' }`.
8035
+ // Invalid hints are silently ignored so a typo degrades to the heuristic.
8036
+ function normalizeLayoutHint(layout) {
8037
+ if (typeof layout === 'string') {
8038
+ return { size: SIZES.includes(layout) ? layout : null, break: null };
8039
+ }
8040
+ if (layout && typeof layout === 'object' && !Array.isArray(layout)) {
8041
+ return {
8042
+ size: SIZES.includes(layout.size) ? layout.size : null,
8043
+ break: BREAKS.includes(layout.break) ? layout.break : null,
8044
+ };
8045
+ }
8046
+ return { size: null, break: null };
8047
+ }
8048
+
8049
+ // Nullable scalars carry the inline null-clear button (~30px of chrome), and
8050
+ // datetime-local needs its full width — one tier of slack keeps them usable.
8051
+ const NULLABLE_BUMP = { xs: 'sm', sm: 'md' };
8052
+
8053
+ function bumpNullable(size, schema) {
8054
+ return schema._nullable ? (NULLABLE_BUMP[size] || size) : size;
8055
+ }
8056
+
8057
+ function choiceSize(labels, schema) {
8058
+ const compact = labels.length <= 8 && labels.every((l) => String(l ?? '').length <= 12);
8059
+ return bumpNullable(compact ? 'sm' : 'md', schema);
8060
+ }
8061
+
8062
+ // Shared with SchemaEditor: past this depth everything renders StringEditor.
8063
+ const MAX_DEPTH = 12;
8064
+
8065
+ // Intrinsic size of a RESOLVED schema node.
8066
+ // Returns 'xs' | 'sm' | 'md' | 'lg' | 'full' | 'hidden'.
8067
+ // Hidden routing (const / single-string-enum) is authoritative: a size hint
8068
+ // on a field that renders HiddenEditor would only produce an empty cell.
8069
+ function fieldSize(schema) {
8070
+ if (!schema || typeof schema !== 'object') return 'full';
8071
+ const intrinsic = intrinsicSize(schema);
8072
+ if (intrinsic === 'hidden') return 'hidden';
8073
+ return normalizeLayoutHint(schema.layout).size || intrinsic;
8074
+ }
8075
+
8076
+ // The branch order mirrors SchemaEditor.editorComponent — keep them in sync.
8077
+ function intrinsicSize(schema) {
8078
+ if (schema.type === 'relation') return schema.multiple ? 'lg' : 'md';
8079
+ if (schema.oneOf && schema.discriminator) return 'full';
8080
+ if (isChoiceOneOf(schema.oneOf)) {
8081
+ return choiceSize(schema.oneOf.map((o) => o.title ?? o.const), schema);
8082
+ }
8083
+ if ('const' in schema) return 'hidden';
8084
+ if (schema.enum && schema.enum.length === 1 && schema.type === 'string') return 'hidden';
8085
+ // Covers ObjectEditor, JsonEditor, ArrayEditor and NullableEditor containers.
8086
+ if (schema.type === 'object' || schema.type === 'array') return 'full';
8087
+ if (schema.enum) return choiceSize(schema.enum, schema);
8088
+ if (schema.type === 'boolean') return bumpNullable('xs', schema);
8089
+ if (schema.type === 'number' || schema.type === 'integer') return bumpNullable('xs', schema);
8090
+ if (schema.type === 'string') {
8091
+ if (schema.format === 'date') return bumpNullable('sm', schema);
8092
+ if (schema.format === 'date-time') return bumpNullable('md', schema);
8093
+ // Mirrors StringEditor.isLong (textarea rendering).
8094
+ if (schema.format === 'textarea' || schema.maxLength > 255) return 'full';
8095
+ if (schema.maxLength > 0 && schema.maxLength <= 40) return bumpNullable('sm', schema);
8096
+ return 'md';
8097
+ }
8098
+ return 'full';
8099
+ }
8100
+
8101
+ // Builds the cell list for an object's properties: resolved schema, wrapper
8102
+ // classes and row-break flags. `break: 'after'` marks the NEXT visible field;
8103
+ // hidden fields neither consume nor emit a pending break.
8104
+ // Custom-editor matches default to 'full' (we can't predict their rendering)
8105
+ // unless the schema hint or the override's own `layout` says otherwise.
8106
+ function layoutCells(properties, { resolveSchema, customEditors = [], basePath = [] } = {}) {
8107
+ const cells = [];
8108
+ let pendingBreak = false;
8109
+ // Mirrors SchemaEditor: the depth guard runs before custom-editor overrides,
8110
+ // and past it every field (const included) renders a visible StringEditor.
8111
+ const pastMaxDepth = basePath.length + 1 > MAX_DEPTH;
8112
+ for (const [key, raw] of Object.entries(properties || {})) {
8113
+ const schema = resolveSchema ? resolveSchema(raw) : raw;
8114
+ const hint = normalizeLayoutHint(schema.layout);
8115
+ const override = !pastMaxDepth
8116
+ && customEditors.find((o) => o.match && o.match(schema, [...basePath, key]));
8117
+ let size;
8118
+ if (pastMaxDepth) {
8119
+ size = 'md';
8120
+ } else if (override) {
8121
+ size = hint.size || normalizeLayoutHint(override.layout).size || 'full';
8122
+ } else {
8123
+ size = fieldSize(schema);
8124
+ }
8125
+ const classes = ['sf-cell', `sf-cell-${size}`];
8126
+ // Only the plain-boolean shape reaches the label-less BooleanEditor;
8127
+ // boolean enums / choice oneOfs render SelectEditor (which has a label).
8128
+ const isCheckbox = schema.type === 'boolean' && !override && !pastMaxDepth
8129
+ && !schema.enum && !schema.oneOf && !('const' in schema);
8130
+ let breakBefore = false;
8131
+ if (size !== 'hidden') {
8132
+ if (isCheckbox) classes.push('sf-cell-bool');
8133
+ breakBefore = pendingBreak || hint.break === 'before' || hint.break === 'both';
8134
+ pendingBreak = hint.break === 'after' || hint.break === 'both';
8135
+ }
8136
+ cells.push({ key, schema, classes: classes.join(' '), breakBefore });
8137
+ }
8138
+ return cells;
8139
+ }
8140
+
7821
8141
  var script$8 = {
7822
8142
  name: 'ObjectEditor',
7823
8143
  beforeCreate() {
@@ -7825,6 +8145,9 @@ var script$8 = {
7825
8145
  this.$options.components.SchemaEditor = script$1;
7826
8146
  this.$options.components.SfIcon = script$9;
7827
8147
  },
8148
+ inject: {
8149
+ customEditors: { default: () => () => [] },
8150
+ },
7828
8151
  props: {
7829
8152
  schema: { type: Object, required: true },
7830
8153
  modelValue: { default: () => ({}) },
@@ -7835,6 +8158,9 @@ var script$8 = {
7835
8158
  data() {
7836
8159
  return {
7837
8160
  collapsed: false,
8161
+ // Values pruned when a conditional rule deactivated their field,
8162
+ // kept so toggling the controller back restores what the user typed.
8163
+ prunedStash: {},
7838
8164
  };
7839
8165
  },
7840
8166
  computed: {
@@ -7848,6 +8174,13 @@ var script$8 = {
7848
8174
  if (!hasConditionals(this.schema)) return this.schema;
7849
8175
  return applyConditionals(this.schema, this.modelValue || {}, this.form?.resolveSchema);
7850
8176
  },
8177
+ cells() {
8178
+ return layoutCells(this.effectiveSchema.properties, {
8179
+ resolveSchema: this.form?.resolveSchema,
8180
+ customEditors: this.customEditors(),
8181
+ basePath: this.path,
8182
+ });
8183
+ },
7851
8184
  summary() {
7852
8185
  const val = this.modelValue || {};
7853
8186
  const parts = [];
@@ -7882,10 +8215,21 @@ var script$8 = {
7882
8215
  const allowed = new Set(Object.keys(effective.properties || {}));
7883
8216
  let changed = false;
7884
8217
  const out = {};
8218
+ // restore stashed values for fields a rule just re-activated
8219
+ for (const k of allowed) {
8220
+ if (!(k in value) && k in this.prunedStash) {
8221
+ out[k] = this.prunedStash[k];
8222
+ delete this.prunedStash[k];
8223
+ changed = true;
8224
+ }
8225
+ }
7885
8226
  for (const k of Object.keys(value)) {
7886
8227
  if (allowed.has(k)) {
7887
8228
  out[k] = value[k];
7888
8229
  } else {
8230
+ // pruned from the emitted value (documented behavior), but kept
8231
+ // locally so a controller toggle round-trip is not destructive
8232
+ this.prunedStash[k] = value[k];
7889
8233
  changed = true;
7890
8234
  }
7891
8235
  }
@@ -7899,14 +8243,24 @@ const _hoisted_1$6 = {
7899
8243
  class: "sf-object sf-object-root"
7900
8244
  };
7901
8245
  const _hoisted_2$5 = { class: "sf-object-fields" };
7902
- const _hoisted_3$5 = { class: "sf-object-title" };
7903
- const _hoisted_4$4 = ["aria-label"];
7904
- const _hoisted_5$4 = { class: "sf-object-title-text" };
7905
- const _hoisted_6$3 = {
8246
+ const _hoisted_3$5 = {
8247
+ key: 0,
8248
+ class: "sf-flow-break",
8249
+ "aria-hidden": "true"
8250
+ };
8251
+ const _hoisted_4$4 = { class: "sf-object-title" };
8252
+ const _hoisted_5$4 = ["aria-label"];
8253
+ const _hoisted_6$3 = { class: "sf-object-title-text" };
8254
+ const _hoisted_7$2 = {
7906
8255
  key: 0,
7907
8256
  class: "sf-object-summary"
7908
8257
  };
7909
- const _hoisted_7$2 = { class: "sf-object-fields" };
8258
+ const _hoisted_8$2 = { class: "sf-object-fields" };
8259
+ const _hoisted_9$2 = {
8260
+ key: 0,
8261
+ class: "sf-flow-break",
8262
+ "aria-hidden": "true"
8263
+ };
7910
8264
 
7911
8265
  function render$7(_ctx, _cache, $props, $setup, $data, $options) {
7912
8266
  const _component_SchemaEditor = resolveComponent("SchemaEditor");
@@ -7915,15 +8269,25 @@ function render$7(_ctx, _cache, $props, $setup, $data, $options) {
7915
8269
  return ($options.isRoot)
7916
8270
  ? (openBlock(), createElementBlock("div", _hoisted_1$6, [
7917
8271
  createBaseVNode("div", _hoisted_2$5, [
7918
- (openBlock(true), createElementBlock(Fragment, null, renderList(($options.effectiveSchema.properties || {}), (propSchema, key) => {
7919
- return (openBlock(), createBlock(_component_SchemaEditor, {
7920
- key: key,
7921
- schema: $props.form.resolveSchema(propSchema),
7922
- "model-value": ($props.modelValue || {})[key],
7923
- path: [...$props.path, key],
7924
- form: $props.form,
7925
- "onUpdate:modelValue": $event => ($options.onChildChange(key, $event))
7926
- }, null, 8 /* PROPS */, ["schema", "model-value", "path", "form", "onUpdate:modelValue"]))
8272
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.cells, (cell) => {
8273
+ return (openBlock(), createElementBlock(Fragment, {
8274
+ key: cell.key
8275
+ }, [
8276
+ (cell.breakBefore)
8277
+ ? (openBlock(), createElementBlock("div", _hoisted_3$5))
8278
+ : createCommentVNode("v-if", true),
8279
+ createBaseVNode("div", {
8280
+ class: normalizeClass(cell.classes)
8281
+ }, [
8282
+ createVNode(_component_SchemaEditor, {
8283
+ schema: cell.schema,
8284
+ "model-value": ($props.modelValue || {})[cell.key],
8285
+ path: [...$props.path, cell.key],
8286
+ form: $props.form,
8287
+ "onUpdate:modelValue": $event => ($options.onChildChange(cell.key, $event))
8288
+ }, null, 8 /* PROPS */, ["schema", "model-value", "path", "form", "onUpdate:modelValue"])
8289
+ ], 2 /* CLASS */)
8290
+ ], 64 /* STABLE_FRAGMENT */))
7927
8291
  }), 128 /* KEYED_FRAGMENT */))
7928
8292
  ])
7929
8293
  ]))
@@ -7931,7 +8295,7 @@ function render$7(_ctx, _cache, $props, $setup, $data, $options) {
7931
8295
  key: 1,
7932
8296
  class: normalizeClass(["sf-object", { 'sf-object-collapsed': $data.collapsed }])
7933
8297
  }, [
7934
- createBaseVNode("legend", _hoisted_3$5, [
8298
+ createBaseVNode("legend", _hoisted_4$4, [
7935
8299
  createBaseVNode("button", {
7936
8300
  type: "button",
7937
8301
  class: "sf-collapse-btn",
@@ -7942,22 +8306,32 @@ function render$7(_ctx, _cache, $props, $setup, $data, $options) {
7942
8306
  name: $data.collapsed ? 'chevron-down' : 'chevron-up',
7943
8307
  size: 12
7944
8308
  }, null, 8 /* PROPS */, ["name"])
7945
- ], 8 /* PROPS */, _hoisted_4$4),
7946
- createBaseVNode("span", _hoisted_5$4, toDisplayString($options.title), 1 /* TEXT */),
8309
+ ], 8 /* PROPS */, _hoisted_5$4),
8310
+ createBaseVNode("span", _hoisted_6$3, toDisplayString($options.title), 1 /* TEXT */),
7947
8311
  ($data.collapsed && $options.summary)
7948
- ? (openBlock(), createElementBlock("span", _hoisted_6$3, toDisplayString($options.summary), 1 /* TEXT */))
8312
+ ? (openBlock(), createElementBlock("span", _hoisted_7$2, toDisplayString($options.summary), 1 /* TEXT */))
7949
8313
  : createCommentVNode("v-if", true)
7950
8314
  ]),
7951
- withDirectives(createBaseVNode("div", _hoisted_7$2, [
7952
- (openBlock(true), createElementBlock(Fragment, null, renderList(($options.effectiveSchema.properties || {}), (propSchema, key) => {
7953
- return (openBlock(), createBlock(_component_SchemaEditor, {
7954
- key: key,
7955
- schema: $props.form.resolveSchema(propSchema),
7956
- "model-value": ($props.modelValue || {})[key],
7957
- path: [...$props.path, key],
7958
- form: $props.form,
7959
- "onUpdate:modelValue": $event => ($options.onChildChange(key, $event))
7960
- }, null, 8 /* PROPS */, ["schema", "model-value", "path", "form", "onUpdate:modelValue"]))
8315
+ withDirectives(createBaseVNode("div", _hoisted_8$2, [
8316
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.cells, (cell) => {
8317
+ return (openBlock(), createElementBlock(Fragment, {
8318
+ key: cell.key
8319
+ }, [
8320
+ (cell.breakBefore)
8321
+ ? (openBlock(), createElementBlock("div", _hoisted_9$2))
8322
+ : createCommentVNode("v-if", true),
8323
+ createBaseVNode("div", {
8324
+ class: normalizeClass(cell.classes)
8325
+ }, [
8326
+ createVNode(_component_SchemaEditor, {
8327
+ schema: cell.schema,
8328
+ "model-value": ($props.modelValue || {})[cell.key],
8329
+ path: [...$props.path, cell.key],
8330
+ form: $props.form,
8331
+ "onUpdate:modelValue": $event => ($options.onChildChange(cell.key, $event))
8332
+ }, null, 8 /* PROPS */, ["schema", "model-value", "path", "form", "onUpdate:modelValue"])
8333
+ ], 2 /* CLASS */)
8334
+ ], 64 /* STABLE_FRAGMENT */))
7961
8335
  }), 128 /* KEYED_FRAGMENT */))
7962
8336
  ], 512 /* NEED_PATCH */), [
7963
8337
  [vShow, !$data.collapsed]
@@ -7968,45 +8342,6 @@ function render$7(_ctx, _cache, $props, $setup, $data, $options) {
7968
8342
  script$8.render = render$7;
7969
8343
  script$8.__file = "src/editors/ObjectEditor.vue";
7970
8344
 
7971
- function debounce(fn, delay) {
7972
- let timer;
7973
- return function (...args) {
7974
- clearTimeout(timer);
7975
- timer = setTimeout(() => fn.apply(this, args), delay);
7976
- };
7977
- }
7978
-
7979
- function deepClone(obj) {
7980
- if (obj === null || typeof obj !== 'object') return obj;
7981
- if (Array.isArray(obj)) return obj.map(deepClone);
7982
- const clone = {};
7983
- for (const key of Object.keys(obj)) {
7984
- clone[key] = deepClone(obj[key]);
7985
- }
7986
- return clone;
7987
- }
7988
-
7989
- function getDefaultForSchema(schema) {
7990
- if ('default' in schema) return deepClone(schema.default);
7991
- if (schema.type === 'object') {
7992
- const obj = {};
7993
- for (const [key, prop] of Object.entries(schema.properties || {})) {
7994
- if ('default' in prop) obj[key] = deepClone(prop.default);
7995
- else if (prop.type === 'string') obj[key] = '';
7996
- else if (prop.type === 'integer' || prop.type === 'number') obj[key] = 0;
7997
- else if (prop.type === 'boolean') obj[key] = false;
7998
- else if (prop.type === 'array') obj[key] = [];
7999
- }
8000
- return obj;
8001
- }
8002
- if (schema.type === 'array') return [];
8003
- if (schema.type === 'string') return '';
8004
- if (schema.type === 'integer' || schema.type === 'number') return 0;
8005
- if (schema.type === 'boolean') return false;
8006
- if (schema.type === 'relation') return schema.multiple ? [] : null;
8007
- return null;
8008
- }
8009
-
8010
8345
  let keyCounter = 0;
8011
8346
 
8012
8347
  var script$7 = {
@@ -8669,7 +9004,7 @@ function render$3(_ctx, _cache, $props, $setup, $data, $options) {
8669
9004
  const _component_SfIcon = resolveComponent("SfIcon");
8670
9005
 
8671
9006
  return (openBlock(), createElementBlock("div", {
8672
- class: normalizeClass(["sf-field sf-relation", { errors: $options.fieldErrors.length }]),
9007
+ class: normalizeClass(["sf-field sf-relation", { errors: $options.fieldErrors.length, 'sf-relation-multiple': $data.isMultiple, 'sf-relation-open': $data.dropdownVisible }]),
8673
9008
  ref: "root"
8674
9009
  }, [
8675
9010
  createBaseVNode("span", {
@@ -9016,14 +9351,13 @@ var script$2 = {
9016
9351
 
9017
9352
  script$2.__file = "src/editors/WebComponentWrapper.vue";
9018
9353
 
9019
- const MAX_DEPTH = 12;
9020
-
9021
9354
  var script$1 = {
9022
9355
  name: 'SchemaEditor',
9023
9356
  components: {
9024
- StringEditor: script$e,
9025
- NumberEditor: script$d,
9026
- BooleanEditor: script$c,
9357
+ StringEditor: script$f,
9358
+ NumberEditor: script$e,
9359
+ BooleanEditor: script$d,
9360
+ DateEditor: script$c,
9027
9361
  SelectEditor: script$b,
9028
9362
  HiddenEditor: script$a,
9029
9363
  ObjectEditor: script$8,
@@ -9073,6 +9407,9 @@ var script$1 = {
9073
9407
 
9074
9408
  if (schema.type === 'relation') return 'RelationEditor';
9075
9409
  if (schema.oneOf && schema.discriminator) return 'UnionEditor';
9410
+ // Choice-list oneOf ({const, title} options) renders as a select —
9411
+ // must be checked before the 'const' HiddenEditor routing.
9412
+ if (isChoiceOneOf(schema.oneOf)) return 'SelectEditor';
9076
9413
  if ('const' in schema) return 'HiddenEditor';
9077
9414
  if (schema.enum && schema.enum.length === 1 && schema.type === 'string') return 'HiddenEditor';
9078
9415
  if (schema._nullable && (schema.type === 'object' || schema.type === 'array')) return 'NullableEditor';
@@ -9082,6 +9419,7 @@ var script$1 = {
9082
9419
  if (schema.enum) return 'SelectEditor';
9083
9420
  if (schema.type === 'boolean') return 'BooleanEditor';
9084
9421
  if (schema.type === 'number' || schema.type === 'integer') return 'NumberEditor';
9422
+ if (schema.type === 'string' && (schema.format === 'date' || schema.format === 'date-time')) return 'DateEditor';
9085
9423
 
9086
9424
  return 'StringEditor';
9087
9425
  },
@@ -9189,29 +9527,36 @@ var script = {
9189
9527
  const hasNull = schema.anyOf.some(s => s.type === 'null');
9190
9528
  if (hasNull && nonNull.length === 1) {
9191
9529
  const resolved = this.resolveSchema(nonNull[0]);
9192
- return {
9193
- ...resolved,
9194
- _nullable: true,
9195
- title: schema.title || resolved.title,
9196
- default: 'default' in schema ? schema.default : null,
9197
- };
9530
+ // Carry ALL sibling keys through the nullable collapse: pydantic
9531
+ // emits json_schema_extra (placeholder, minLength/maxLength,
9532
+ // minimum/maximum, format, choice oneOf, ...) as SIBLINGS of
9533
+ // anyOf on Optional fields. Outer keys override the inner branch.
9534
+ const { anyOf, oneOf, ...rest } = schema;
9535
+ const out = { ...resolved, ...rest, _nullable: true };
9536
+ if (!('default' in schema)) out.default = null;
9537
+ if (isChoiceOneOf(oneOf)) out.oneOf = oneOf;
9538
+ return out;
9198
9539
  }
9199
9540
  if (nonNull.length >= 1) return this.resolveSchema(nonNull[0]);
9200
9541
  }
9201
9542
 
9202
9543
  if (schema.oneOf && schema.discriminator) return schema;
9203
9544
 
9545
+ // A oneOf of {const, title} value options is a choice list (select),
9546
+ // not alternative sub-schemas: keep it intact for SelectEditor instead
9547
+ // of collapsing to the first member (which routed to HiddenEditor and
9548
+ // overwrote stored values on mount).
9549
+ if (isChoiceOneOf(schema.oneOf)) return schema;
9550
+
9204
9551
  if (schema.oneOf) {
9205
9552
  const nonNull = schema.oneOf.filter(s => s.type !== 'null');
9206
9553
  const hasNull = schema.oneOf.some(s => s.type === 'null');
9207
9554
  if (hasNull && nonNull.length === 1) {
9208
9555
  const resolved = this.resolveSchema(nonNull[0]);
9209
- return {
9210
- ...resolved,
9211
- _nullable: true,
9212
- title: schema.title || resolved.title,
9213
- default: 'default' in schema ? schema.default : null,
9214
- };
9556
+ const { anyOf, oneOf, ...rest } = schema;
9557
+ const out = { ...resolved, ...rest, _nullable: true };
9558
+ if (!('default' in schema)) out.default = null;
9559
+ return out;
9215
9560
  }
9216
9561
  if (nonNull.length >= 1) return this.resolveSchema(nonNull[0]);
9217
9562
  }
@@ -9415,5 +9760,5 @@ function registerCustomElement(tagName = 'schema-form') {
9415
9760
  }
9416
9761
  }
9417
9762
 
9418
- export { script$7 as ArrayEditor, BaseEditorElement, script$c as BooleanEditor, script$a as HiddenEditor, script$6 as NullableEditor, script$d as NumberEditor, script$8 as ObjectEditor, script$4 as RelationEditor, script$1 as SchemaEditor, script as SchemaForm, SchemaFormElement, script$b as SelectEditor, script$e as StringEditor, script$5 as UnionEditor, script$2 as WebComponentWrapper, applyConditionals, hasConditionals, matchesSchema, registerCustomElement };
9763
+ export { script$7 as ArrayEditor, BaseEditorElement, script$d as BooleanEditor, script$c as DateEditor, script$a as HiddenEditor, script$6 as NullableEditor, script$e as NumberEditor, script$8 as ObjectEditor, script$4 as RelationEditor, script$1 as SchemaEditor, script as SchemaForm, SchemaFormElement, script$b as SelectEditor, script$f as StringEditor, script$5 as UnionEditor, script$2 as WebComponentWrapper, applyConditionals, fieldSize, hasConditionals, layoutCells, matchesSchema, normalizeLayoutHint, registerCustomElement };
9419
9764
  //# sourceMappingURL=structured-widget-editor.esm.js.map