@structured-field/widget-editor 1.2.1 → 1.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.
@@ -1,22 +1,28 @@
1
1
  <template>
2
2
  <div class="sf-field" :class="{ errors: fieldErrors.length }">
3
- <label class="sf-label" :class="{ required: isRequired }">{{ title }}</label>
4
- <textarea
5
- v-if="isLong"
6
- class="sf-input sf-textarea"
7
- rows="3"
8
- :value="modelValue"
9
- :placeholder="schema.placeholder || ''"
10
- @input="$emit('update:modelValue', $event.target.value)"
11
- />
12
- <input
13
- v-else
14
- type="text"
15
- class="sf-input"
16
- :value="modelValue != null ? String(modelValue) : ''"
17
- :placeholder="schema.placeholder || ''"
18
- @input="$emit('update:modelValue', $event.target.value)"
19
- />
3
+ <span class="sf-label" :class="{ required: isRequired }">
4
+ {{ title }}
5
+ <span v-if="isNullable && isNullValue" class="sf-null-badge">null</span>
6
+ </span>
7
+ <div :class="isNullable ? 'sf-input-row' : null">
8
+ <textarea
9
+ v-if="isLong"
10
+ class="sf-input sf-textarea"
11
+ rows="3"
12
+ :value="isNullValue ? '' : modelValue"
13
+ :placeholder="isNullValue ? 'null' : (schema.placeholder || '')"
14
+ @input="$emit('update:modelValue', $event.target.value)"
15
+ />
16
+ <input
17
+ v-else
18
+ type="text"
19
+ class="sf-input"
20
+ :value="isNullValue ? '' : (modelValue != null ? String(modelValue) : '')"
21
+ :placeholder="isNullValue ? 'null' : (schema.placeholder || '')"
22
+ @input="$emit('update:modelValue', $event.target.value)"
23
+ />
24
+ <button v-if="isNullable && !isNullValue" type="button" class="sf-null-clear-btn" title="Set to null" @click="$emit('update:modelValue', null)">&#x2715;</button>
25
+ </div>
20
26
  <ul v-if="fieldErrors.length" class="errorlist">
21
27
  <li v-for="(err, i) in fieldErrors" :key="i">{{ err }}</li>
22
28
  </ul>
@@ -47,6 +53,12 @@ export default {
47
53
  isLong() {
48
54
  return this.schema.maxLength > 255 || this.schema.format === 'textarea';
49
55
  },
56
+ isNullable() {
57
+ return !!this.schema._nullable;
58
+ },
59
+ isNullValue() {
60
+ return this.modelValue === null || this.modelValue === undefined;
61
+ },
50
62
  fieldErrors() {
51
63
  if (!this.form || !this.form.getErrorsForPath) return [];
52
64
  return this.form.getErrorsForPath(this.path);
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="sf-union">
3
3
  <div class="sf-field">
4
- <label class="sf-label">{{ title }}</label>
4
+ <span class="sf-label">{{ title }}</span>
5
5
  <select class="sf-input sf-select" :value="currentType" @change="onTypeChange">
6
6
  <option v-for="key in typeKeys" :key="key" :value="key">{{ humanize(key) }}</option>
7
7
  </select>
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ export { default as SchemaEditor } from './editors/SchemaEditor.vue';
5
5
  export { default as StringEditor } from './editors/StringEditor.vue';
6
6
  export { default as NumberEditor } from './editors/NumberEditor.vue';
7
7
  export { default as BooleanEditor } from './editors/BooleanEditor.vue';
8
+ export { default as DateEditor } from './editors/DateEditor.vue';
8
9
  export { default as SelectEditor } from './editors/SelectEditor.vue';
9
10
  export { default as HiddenEditor } from './editors/HiddenEditor.vue';
10
11
  export { default as ObjectEditor } from './editors/ObjectEditor.vue';
@@ -206,6 +206,63 @@
206
206
  }
207
207
  }
208
208
 
209
+ // --- Inline nullable controls ---
210
+ .sf-null-badge {
211
+ display: inline-block;
212
+ font-size: 0.65rem;
213
+ font-weight: 600;
214
+ text-transform: uppercase;
215
+ letter-spacing: 0.04em;
216
+ color: var(--body-quiet-color, #888);
217
+ background: var(--darkened-bg, #f0f0f0);
218
+ border: 1px solid var(--border-color, #ccc);
219
+ border-radius: 3px;
220
+ padding: 1px 5px;
221
+ vertical-align: middle;
222
+ margin-left: 4px;
223
+ }
224
+
225
+ .sf-input-row {
226
+ display: flex;
227
+ align-items: stretch;
228
+ gap: 4px;
229
+
230
+ .sf-input {
231
+ flex: 1;
232
+ min-width: 0;
233
+ }
234
+ }
235
+
236
+ .sf-boolean-row {
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 6px;
240
+ }
241
+
242
+ .sf-null-clear-btn {
243
+ flex-shrink: 0;
244
+ display: inline-flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ width: 26px;
248
+ height: 26px;
249
+ padding: 0;
250
+ border: 1px solid var(--border-color, #ccc);
251
+ border-radius: 4px;
252
+ background: transparent;
253
+ cursor: pointer;
254
+ color: var(--body-quiet-color, #888);
255
+ font-size: 0.75rem;
256
+ line-height: 1;
257
+ align-self: center;
258
+
259
+ &:hover {
260
+ background: var(--error-bg, #fff2f2);
261
+ border-color: var(--error-fg, #ba2121);
262
+ color: var(--error-fg, #ba2121);
263
+ }
264
+ }
265
+
209
266
  // --- Nullable editor ---
210
267
  .sf-nullable {
211
268
  margin-bottom: 12px;
@@ -234,6 +291,63 @@
234
291
  }
235
292
  }
236
293
 
294
+ // --- JSON editor ---
295
+ .sf-field-json {
296
+ .sf-json-editor {
297
+ position: relative;
298
+ border: 1px solid var(--border-color, #ccc);
299
+ border-radius: 4px;
300
+ overflow: hidden;
301
+
302
+ &.sf-json-error {
303
+ border-color: var(--error-fg, #ba2121);
304
+ }
305
+ }
306
+
307
+ .sf-json-toolbar {
308
+ display: flex;
309
+ align-items: center;
310
+ justify-content: flex-end;
311
+ gap: 8px;
312
+ padding: 4px 6px;
313
+ background: var(--darkened-bg, #f8f8f8);
314
+ border-bottom: 1px solid var(--border-color, #ccc);
315
+ }
316
+
317
+ .sf-json-error-msg {
318
+ font-size: 0.72rem;
319
+ color: var(--error-fg, #ba2121);
320
+ flex: 1;
321
+ white-space: nowrap;
322
+ overflow: hidden;
323
+ text-overflow: ellipsis;
324
+ }
325
+
326
+ .sf-json-format-btn {
327
+ flex-shrink: 0;
328
+ }
329
+
330
+ .sf-json-ace-container {
331
+ width: 100%;
332
+ font-size: 0.8rem;
333
+ // Ace sets its own height via maxLines/minLines
334
+ .ace_editor {
335
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
336
+ font-size: 0.8rem !important;
337
+ }
338
+ }
339
+
340
+ .sf-json-textarea-fallback {
341
+ width: 100%;
342
+ min-height: 120px;
343
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
344
+ font-size: 0.8rem;
345
+ border: none;
346
+ border-radius: 0;
347
+ resize: vertical;
348
+ }
349
+ }
350
+
237
351
  // --- Union editor ---
238
352
  .sf-union {
239
353
  margin-bottom: 12px;
package/src/utils.js CHANGED
@@ -16,6 +16,24 @@ export function deepClone(obj) {
16
16
  return clone;
17
17
  }
18
18
 
19
+ export function isChoiceOneOf(list) {
20
+ // A "choice list" oneOf: every member is a {const, title?} value option
21
+ // (the shape emitted for enumerated choices, e.g. metaobjects' select
22
+ // kind), as opposed to a oneOf of alternative sub-schemas.
23
+ return (
24
+ Array.isArray(list) &&
25
+ list.length > 0 &&
26
+ list.every(
27
+ (m) =>
28
+ m &&
29
+ typeof m === 'object' &&
30
+ 'const' in m &&
31
+ !('properties' in m) &&
32
+ m.type !== 'null'
33
+ )
34
+ );
35
+ }
36
+
19
37
  export function getDefaultForSchema(schema) {
20
38
  if ('default' in schema) return deepClone(schema.default);
21
39
  if (schema.type === 'object') {