@structured-field/widget-editor 1.1.1 → 1.2.1

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.
package/README.md CHANGED
@@ -197,6 +197,290 @@ Query parameters sent: `_q` (search term), `page` (pagination).
197
197
  | `oneOf + discriminator` | UnionEditor | Type selector |
198
198
  | `relation` | RelationEditor | Autocomplete with search |
199
199
 
200
+ ## Custom Editors
201
+
202
+ You can override the editor used for any field by passing a `customEditors` array to `SchemaForm`. Each entry defines a `match` condition and the `component` to render when that condition is true. Overrides are evaluated **before** the built-in resolution logic, in order — the first match wins.
203
+
204
+ ### Vue component
205
+
206
+ ```vue
207
+ <template>
208
+ <SchemaForm :schema="schema" :custom-editors="customEditors" />
209
+ </template>
210
+
211
+ <script setup>
212
+ import { SchemaForm } from '@structured-field/widget-editor';
213
+ import MyDatePicker from './MyDatePicker.vue';
214
+ import MyColorPicker from './MyColorPicker.vue';
215
+
216
+ const customEditors = [
217
+ // Match by schema format
218
+ { match: (schema) => schema.format === 'date', component: MyDatePicker },
219
+ // Match by field name (last segment of the path)
220
+ { match: (schema, path) => path.at(-1) === 'color', component: MyColorPicker },
221
+ // Match by a custom schema property
222
+ { match: (schema) => schema['x-widget'] === 'rich-text', component: MyRichText },
223
+ ];
224
+ </script>
225
+ ```
226
+
227
+ ### Web Component (custom element)
228
+
229
+ ```js
230
+ const el = document.querySelector('schema-form');
231
+
232
+ el.customEditors = [
233
+ // Vue component — pass the component object
234
+ { match: (schema) => schema.format === 'date', component: MyDatePicker },
235
+ // Web Component — pass the tag name as a string
236
+ { match: (schema, path) => path.at(-1) === 'color', component: 'my-color-picker' },
237
+ ];
238
+ ```
239
+
240
+ When `component` is a **string containing a hyphen**, it is treated as a web component tag name. The editor wrapper will:
241
+
242
+ - Set `schema`, `modelValue`, `path`, and `form` as **JS properties** on the element (not HTML attributes)
243
+ - Listen for `change` or `update:model-value` `CustomEvent`s to receive the new value
244
+
245
+ ### Custom editor API
246
+
247
+ #### Vue component
248
+
249
+ A custom editor Vue component must accept the following props and emit `update:modelValue` to write values back. **Never mutate `modelValue` directly.**
250
+
251
+ | Prop | Type | Description |
252
+ |---|---|---|
253
+ | `schema` | `Object` | The resolved JSON Schema for this field |
254
+ | `modelValue` | `any` | Current field value — read-only |
255
+ | `path` | `string[]` | Path segments from the root (`['address', 'city']`) |
256
+ | `form` | `Object` | Form API: `getSchemaAtPath(path)`, `getErrorsForPath(path)`, `resolveSchema(schema)` |
257
+
258
+ | Emit | Payload | Description |
259
+ |---|---|---|
260
+ | `update:modelValue` | new value | The only way to write a value back to the form |
261
+
262
+ #### Web Component
263
+
264
+ A custom editor web component receives the same data as **JS properties** and dispatches a `change` `CustomEvent` with the new value as `detail`:
265
+
266
+ | Property | Type | Description |
267
+ |---|---|---|
268
+ | `schema` | `Object` | The resolved JSON Schema for this field |
269
+ | `modelValue` | `any` | Current field value — read-only |
270
+ | `path` | `string[]` | Path segments from the root |
271
+ | `form` | `Object` | Form API: `getSchemaAtPath(path)`, `getErrorsForPath(path)`, `resolveSchema(schema)` |
272
+
273
+ | Event | Detail | Description |
274
+ |---|---|---|
275
+ | `change` | new value | Dispatched to write a value back to the form |
276
+
277
+ ### Starter templates
278
+
279
+ #### Vue component
280
+
281
+ Copy and adapt this component as a starting point:
282
+
283
+ ```vue
284
+ <template>
285
+ <div class="sf-field" :class="{ errors: fieldErrors.length }">
286
+ <label class="sf-label" :class="{ required: isRequired }">{{ title }}</label>
287
+
288
+ <!-- Replace this with your custom input -->
289
+ <input
290
+ type="text"
291
+ class="sf-input"
292
+ :value="modelValue"
293
+ @input="$emit('update:modelValue', $event.target.value)"
294
+ />
295
+
296
+ <ul v-if="fieldErrors.length" class="errorlist">
297
+ <li v-for="(err, i) in fieldErrors" :key="i">{{ err }}</li>
298
+ </ul>
299
+ </div>
300
+ </template>
301
+
302
+ <script>
303
+ export default {
304
+ name: 'MyCustomEditor',
305
+ props: {
306
+ schema: { type: Object, required: true },
307
+ modelValue: { default: undefined },
308
+ path: { type: Array, default: () => [] },
309
+ form: { type: Object, default: null },
310
+ },
311
+ emits: ['update:modelValue'],
312
+ computed: {
313
+ title() {
314
+ return this.schema.title || this.humanize(this.path.at(-1)) || '';
315
+ },
316
+ isRequired() {
317
+ if (this.path.length < 2 || !this.form) return false;
318
+ const parentSchema = this.form.getSchemaAtPath(this.path.slice(0, -1));
319
+ return Array.isArray(parentSchema?.required) && parentSchema.required.includes(this.path.at(-1));
320
+ },
321
+ fieldErrors() {
322
+ return this.form?.getErrorsForPath?.(this.path) ?? [];
323
+ },
324
+ },
325
+ methods: {
326
+ humanize(str) {
327
+ if (!str) return '';
328
+ return str.replace(/_/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, s => s.toUpperCase());
329
+ },
330
+ },
331
+ };
332
+ </script>
333
+ ```
334
+
335
+ #### Web Component (using `BaseEditorElement`)
336
+
337
+ The `BaseEditorElement` base class handles the property contract for you. Override `render()` to build the initial DOM and `update()` to react to property changes. Use `this.emitChange(value)` to send values back and `this.getErrors()` to read validation errors.
338
+
339
+ **ESM:**
340
+
341
+ ```js
342
+ import { BaseEditorElement } from '@structured-field/widget-editor';
343
+
344
+ class MyColorPicker extends BaseEditorElement {
345
+ render() {
346
+ const wrapper = document.createElement('div');
347
+ wrapper.className = 'sf-field';
348
+
349
+ const label = document.createElement('label');
350
+ label.className = 'sf-label';
351
+ label.textContent = this.schema?.title || 'Color';
352
+ this._label = label;
353
+
354
+ const input = document.createElement('input');
355
+ input.type = 'color';
356
+ input.className = 'sf-input';
357
+ input.value = this.modelValue || '#000000';
358
+ input.addEventListener('input', () => this.emitChange(input.value));
359
+ this._input = input;
360
+
361
+ wrapper.appendChild(label);
362
+ wrapper.appendChild(input);
363
+ this.appendChild(wrapper);
364
+ }
365
+
366
+ update() {
367
+ if (this._label) this._label.textContent = this.schema?.title || 'Color';
368
+ if (this._input) this._input.value = this.modelValue || '#000000';
369
+ }
370
+ }
371
+
372
+ customElements.define('my-color-picker', MyColorPicker);
373
+ ```
374
+
375
+ **IIFE:**
376
+
377
+ ```html
378
+ <script src="https://bnznamco.github.io/structured-widget-editor/latest/structured-widget-editor.iife.js"></script>
379
+ <script>
380
+ var BaseEditorElement = StructuredWidgetEditor.BaseEditorElement;
381
+
382
+ class MyColorPicker extends BaseEditorElement {
383
+ render() {
384
+ var input = document.createElement('input');
385
+ input.type = 'color';
386
+ input.value = this.modelValue || '#000000';
387
+ input.addEventListener('input', () => this.emitChange(input.value));
388
+ this._input = input;
389
+ this.appendChild(input);
390
+ }
391
+
392
+ update() {
393
+ if (this._input) this._input.value = this.modelValue || '#000000';
394
+ }
395
+ }
396
+
397
+ customElements.define('my-color-picker', MyColorPicker);
398
+ </script>
399
+ ```
400
+
401
+ #### `BaseEditorElement` API
402
+
403
+ | Property / Method | Description |
404
+ |---|---|
405
+ | `this.schema` | The resolved JSON Schema for this field |
406
+ | `this.modelValue` | Current field value (read-only) |
407
+ | `this.path` | Path segments from the root |
408
+ | `this.form` | Form API object |
409
+ | `this.emitChange(value)` | Dispatch the new value back to the form |
410
+ | `this.getErrors()` | Returns `string[]` of validation errors for this field |
411
+ | `render()` | **Override.** Called once when connected — build the DOM here |
412
+ | `update()` | **Override.** Called on every property change after `render()` |
413
+
414
+ ## Conditional fields
415
+
416
+ The form renderer evaluates standard JSON Schema conditional keywords on every change and updates the visible/required fields accordingly. No custom keywords — anything you put in the schema is also enforced server-side by Pydantic v2.
417
+
418
+ Supported keywords on object schemas:
419
+
420
+ | Keyword | Use case |
421
+ |---|---|
422
+ | `if` / `then` / `else` | "If `status == 'archived'`, require `archive_reason`" |
423
+ | `allOf: [{ if, then }, ...]` | Multiple independent rules on the same object |
424
+ | `dependentSchemas` | "If field `publisher` is present, also show `edition`" |
425
+ | `dependentRequired` | Lighter version: only toggles `required` |
426
+
427
+ Example schema fragment:
428
+
429
+ ```json
430
+ {
431
+ "type": "object",
432
+ "properties": {
433
+ "status": { "enum": ["draft", "archived"] }
434
+ },
435
+ "allOf": [
436
+ {
437
+ "if": { "properties": { "status": { "const": "archived" } }, "required": ["status"] },
438
+ "then": {
439
+ "properties": { "archive_reason": { "type": "string" } },
440
+ "required": ["archive_reason"]
441
+ }
442
+ }
443
+ ],
444
+ "dependentSchemas": {
445
+ "publisher": { "properties": { "edition": { "type": "string" } } }
446
+ }
447
+ }
448
+ ```
449
+
450
+ Fields that disappear when a rule stops matching are pruned from the form value, so the emitted JSON stays clean.
451
+
452
+ ### Declaring conditionals on a Pydantic model
453
+
454
+ `django-structured-field` ships matching helpers in `structured.pydantic.conditionals` that compile down to the same standard keywords:
455
+
456
+ ```python
457
+ from typing import Literal, Optional
458
+ from pydantic import ConfigDict
459
+ from structured.pydantic.models import BaseModel
460
+ from structured.pydantic.conditionals import (
461
+ When, conditional_schema, dependent_schemas,
462
+ )
463
+
464
+ class Book(BaseModel):
465
+ status: Literal["draft", "review", "published", "archived"] = "draft"
466
+ archive_reason: Optional[str] = None
467
+ published_at: Optional[str] = None
468
+ publisher: Optional[str] = None
469
+ edition: Optional[str] = None
470
+
471
+ model_config = ConfigDict(json_schema_extra=conditional_schema(
472
+ When("status", equals="archived",
473
+ then={"required": ["archive_reason"]}),
474
+ When("status", equals="published",
475
+ then={"required": ["published_at"]}),
476
+ dependent_schemas(publisher={
477
+ "properties": {"edition": {"type": "string"}}
478
+ }),
479
+ ))
480
+ ```
481
+
482
+ `When(field, equals=..., in_=..., not_equals=..., then=..., else_=...)` builds a single `if/then/else` clause; `conditional_schema(...)` groups them into `allOf` and merges any `dependent_schemas` / `dependent_required` fragments.
483
+
200
484
  ## Theming
201
485
 
202
486
  All styles use CSS custom properties with sensible defaults. Override them to match your design system:
@@ -1 +1 @@
1
- .structured-field-editor{width:100%;border-radius:4px;font-size:.8125rem}.structured-field-editor *,.structured-field-editor *::before,.structured-field-editor *::after{box-sizing:border-box}.structured-field-editor .sf-label{display:block;font-weight:600;margin-bottom:4px;text-transform:capitalize;color:var(--body-quiet-color, #666);font-size:.8125rem}.structured-field-editor .sf-label.required::after{content:" *";color:var(--error-fg, #ba2121)}.structured-field-editor .sf-input{display:block;width:100%;padding:6px 8px;border:1px solid var(--border-color, #ccc);border-radius:4px;background:var(--body-bg, #fff);color:var(--body-fg, #333);font-size:.8125rem;font-family:inherit;transition:border-color .15s}.structured-field-editor .sf-input:focus{outline:none;border-color:var(--primary, #79aec8);box-shadow:0 0 0 2px rgba(121,174,200,.2)}.structured-field-editor .sf-textarea{resize:vertical;min-height:60px}.structured-field-editor .sf-select{appearance:none;cursor:pointer;padding-right:28px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='12' height='12'%3E%3Cpath fill='%23888' d='M12 15L5 8h14z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;background-size:12px 12px}.structured-field-editor .sf-checkbox{margin-right:6px;accent-color:var(--primary, #79aec8)}.structured-field-editor .sf-checkbox-label{display:flex;align-items:center;font-weight:normal;cursor:pointer;padding:6px 0}.structured-field-editor .sf-btn{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;background:var(--object-tools-bg, #888);color:var(--object-tools-fg, #fff);font-weight:400;font-size:.6875rem;text-transform:uppercase;letter-spacing:.5px;border-radius:15px;border:none;cursor:pointer;transition:background-color .15s}.structured-field-editor .sf-btn:hover{background-color:var(--object-tools-hover-bg, #666)}.structured-field-editor .sf-btn i{font-size:.6rem}.structured-field-editor .sf-btn-sm{padding:2px 8px;font-size:.625rem}.structured-field-editor .sf-btn-add{background:var(--object-tools-bg, #888);color:var(--object-tools-fg, #fff)}.structured-field-editor .sf-btn-danger{background:var(--delete-button-bg, #ba2121);color:var(--delete-button-fg, #fff)}.structured-field-editor .sf-btn-danger:hover{background:#a41515}.structured-field-editor .sf-error{color:var(--error-fg, #ba2121);font-size:.75rem;margin-top:2px}.structured-field-editor .errors .sf-input{border-color:var(--error-fg, #ba2121)}.structured-field-editor .errorlist{margin:4px 0 0 0;padding:0;list-style:none;color:var(--error-fg, #ba2121);font-size:.75rem}.structured-field-editor .errorlist li{padding:2px 0}.form-row{overflow:visible !important}.structured-field-editor .sf-field{margin-bottom:12px}.structured-field-editor .sf-field-boolean{margin-bottom:8px}.structured-field-editor .sf-object:not(.sf-object-root){border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-bottom:12px;background:var(--body-bg, #fff)}.structured-field-editor .sf-object:not(.sf-object-root).sf-object-collapsed{padding:0}.structured-field-editor .sf-object-title{display:flex;align-items:center;gap:6px;font-weight:600;font-size:.8125rem;color:var(--body-fg, #333);text-transform:capitalize;padding:0 4px}.structured-field-editor .sf-object-title-text{flex-shrink:0}.structured-field-editor .sf-object-summary{font-weight:400;font-size:.75rem;color:var(--body-quiet-color, #888);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.structured-field-editor .sf-collapse-btn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:none;background:rgba(0,0,0,0);cursor:pointer;color:var(--body-quiet-color, #888);border-radius:3px;flex-shrink:0}.structured-field-editor .sf-collapse-btn:hover{background:var(--darkened-bg, #f0f0f0);color:var(--body-fg, #333)}.structured-field-editor .sf-object-fields{display:flex;flex-direction:column}.structured-field-editor .sf-array{margin-bottom:12px}.structured-field-editor .sf-array-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.structured-field-editor .sf-array-header .sf-label{margin-bottom:0}.structured-field-editor .sf-array-collapse-toggle{margin-left:auto;display:inline-flex;align-items:center;cursor:pointer;color:var(--body-quiet-color, #aaa);line-height:0}.structured-field-editor .sf-array-collapse-toggle:hover{color:var(--body-fg, #333)}.structured-field-editor .sf-array-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;background:var(--darkened-bg, #f0f0f0);border-radius:10px;font-size:.6875rem;font-weight:600;color:var(--body-quiet-color, #666)}.structured-field-editor .sf-array-items{display:flex;flex-direction:column;gap:8px}.structured-field-editor .sf-array-item{border:1px solid var(--border-color, #ccc);border-radius:4px;background:var(--darkened-bg, #f8f8f8);overflow:hidden}.structured-field-editor .sf-array-item[draggable=true]{cursor:grab}.structured-field-editor .sf-array-item.sf-dragging{opacity:.4}.structured-field-editor .sf-array-item.sf-drag-over{outline:2px solid var(--primary-color, #3b82f6);outline-offset:-2px}.structured-field-editor .sf-array-item-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:var(--darkened-bg, #f0f0f0);border-bottom:1px solid var(--border-color, #ccc)}.structured-field-editor .sf-array-item-left{display:flex;align-items:center;gap:6px}.structured-field-editor .sf-drag-handle{display:inline-flex;align-items:center;color:var(--body-quiet-color, #aaa);cursor:grab;padding:0 2px;line-height:0}.structured-field-editor .sf-drag-handle:active{cursor:grabbing}.structured-field-editor .sf-drag-handle:hover{color:var(--body-fg, #555)}.structured-field-editor .sf-array-item-index{font-size:.6875rem;font-weight:600;color:var(--body-quiet-color, #666)}.structured-field-editor .sf-array-item-actions{display:flex;gap:4px}.structured-field-editor .sf-array-item-body{padding:10px}.structured-field-editor .sf-array-item-body>.sf-object.sf-object-root,.structured-field-editor .sf-array-item-body>.sf-object:not(.sf-object-root){border:none;padding:0;margin:0;background:rgba(0,0,0,0)}.structured-field-editor .sf-nullable{margin-bottom:12px}.structured-field-editor .sf-nullable-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.structured-field-editor .sf-nullable-header .sf-label{margin-bottom:0}.structured-field-editor .sf-nullable-body{padding-left:0}.structured-field-editor .sf-nullable-body:not(:empty){border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-top:4px;background:var(--body-bg, #fff)}.structured-field-editor .sf-union{margin-bottom:12px}.structured-field-editor .sf-union-body{border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-top:8px;background:var(--body-bg, #fff)}.structured-field-editor .sf-union-body>.sf-object{border:none;padding:0;margin:0;background:rgba(0,0,0,0)}.structured-field-editor .sf-relation{position:relative}.structured-field-editor .sf-relation-wrapper{display:flex;flex-direction:column;gap:6px}.structured-field-editor .sf-relation-selected{display:flex;flex-wrap:wrap;gap:6px}.structured-field-editor .sf-relation-selected:empty{display:none}.structured-field-editor .sf-relation-tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;background:var(--darkened-bg, #f0f0f0);border:1px solid var(--border-color, #ccc);border-radius:4px;font-size:.8125rem;color:var(--body-fg, #333);max-width:100%}.structured-field-editor .sf-relation-tag-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.structured-field-editor .sf-relation-tag-remove{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:none;border-radius:50%;background:rgba(0,0,0,0);color:var(--body-quiet-color, #666);cursor:pointer;font-size:.6875rem;flex-shrink:0;transition:color .15s,background .15s}.structured-field-editor .sf-relation-tag-remove:hover{color:var(--error-fg, #ba2121);background:rgba(186,33,33,.1)}.structured-field-editor .sf-relation-search{position:relative}.structured-field-editor .sf-relation-input{padding-left:28px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:8px center;background-size:14px}.structured-field-editor .sf-relation-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;max-height:240px;overflow-y:auto;background:var(--body-bg, #fff);border:1px solid var(--border-color, #ccc);border-top:none;border-radius:0 0 4px 4px;box-shadow:0 4px 12px rgba(0,0,0,.1)}.structured-field-editor .sf-relation-dropdown-item{padding:8px 12px;cursor:pointer;font-size:.8125rem;color:var(--body-fg, #333);transition:background .1s;border-bottom:1px solid var(--hairline-color, #eee)}.structured-field-editor .sf-relation-dropdown-item:last-child{border-bottom:none}.structured-field-editor .sf-relation-dropdown-item:hover,.structured-field-editor .sf-relation-dropdown-item.highlighted{background:var(--selected-bg, #e4e4e4)}.structured-field-editor .sf-relation-dropdown-empty{padding:12px;text-align:center;color:var(--body-quiet-color, #999);font-size:.8125rem;font-style:italic}.structured-field-editor .sf-relation-dropdown-more{padding:8px 12px;text-align:center;cursor:pointer;font-size:.75rem;color:var(--link-fg, #417690);font-weight:600;border-top:1px solid var(--hairline-color, #eee);transition:background .1s}.structured-field-editor .sf-relation-dropdown-more:hover{background:var(--darkened-bg, #f0f0f0)}
1
+ .structured-field-editor{width:100%;border-radius:4px;font-size:.8125rem}.structured-field-editor *,.structured-field-editor *::before,.structured-field-editor *::after{box-sizing:border-box}.structured-field-editor .sf-label{display:block;font-weight:600;margin-bottom:4px;text-transform:capitalize;color:var(--body-quiet-color, #666);font-size:.8125rem}.structured-field-editor .sf-label.required::after{content:" *";color:var(--error-fg, #ba2121)}.structured-field-editor .sf-input{display:block;width:100%;padding:6px 8px;border:1px solid var(--border-color, #ccc);border-radius:4px;background:var(--body-bg, #fff);color:var(--body-fg, #333);font-size:.8125rem;font-family:inherit;transition:border-color .15s}.structured-field-editor .sf-input:focus{outline:none;border-color:var(--primary, #79aec8);box-shadow:0 0 0 2px rgba(121,174,200,.2)}.structured-field-editor .sf-textarea{resize:vertical;min-height:60px}.structured-field-editor .sf-select{appearance:none;cursor:pointer;padding-right:28px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='12' height='12'%3E%3Cpath fill='%23888' d='M12 15L5 8h14z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;background-size:12px 12px}.structured-field-editor .sf-checkbox{margin-right:6px;accent-color:var(--primary, #79aec8)}.structured-field-editor .sf-checkbox-label{display:flex;align-items:center;font-weight:normal;cursor:pointer;padding:6px 0}.structured-field-editor .sf-btn{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;background:var(--object-tools-bg, #888);color:var(--object-tools-fg, #fff);font-weight:400;font-size:.6875rem;text-transform:uppercase;letter-spacing:.5px;border-radius:15px;border:none;cursor:pointer;transition:background-color .15s}.structured-field-editor .sf-btn:hover{background-color:var(--object-tools-hover-bg, #666)}.structured-field-editor .sf-btn i{font-size:.6rem}.structured-field-editor .sf-btn-sm{padding:2px 8px;font-size:.625rem}.structured-field-editor .sf-btn-add{background:var(--object-tools-bg, #888);color:var(--object-tools-fg, #fff)}.structured-field-editor .sf-btn-danger{background:var(--delete-button-bg, #ba2121);color:var(--delete-button-fg, #fff)}.structured-field-editor .sf-btn-danger:hover{background:#a41515}.structured-field-editor .sf-error{color:var(--error-fg, #ba2121);font-size:.75rem;margin-top:2px}.structured-field-editor .errors .sf-input{border-color:var(--error-fg, #ba2121)}.structured-field-editor .errorlist{margin:4px 0 0 0;padding:0;list-style:none;color:var(--error-fg, #ba2121);font-size:.75rem}.structured-field-editor .errorlist li{padding:2px 0}.form-row{overflow:visible !important}.structured-field-editor .sf-field{margin-bottom:12px}.structured-field-editor .sf-field-boolean{margin-bottom:8px}.structured-field-editor .sf-object:not(.sf-object-root){border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-bottom:12px;background:var(--body-bg, #fff)}.structured-field-editor .sf-object:not(.sf-object-root).sf-object-collapsed{padding:0}.structured-field-editor .sf-object-title{display:flex;align-items:center;gap:6px;font-weight:600;font-size:.8125rem;color:var(--body-fg, #333);text-transform:capitalize;padding:0 4px}.structured-field-editor .sf-object-title-text{flex-shrink:0}.structured-field-editor .sf-object-summary{font-weight:400;font-size:.75rem;color:var(--body-quiet-color, #888);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}.structured-field-editor .sf-collapse-btn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:none;background:rgba(0,0,0,0);cursor:pointer;color:var(--body-quiet-color, #888);border-radius:3px;flex-shrink:0}.structured-field-editor .sf-collapse-btn:hover{background:var(--darkened-bg, #f0f0f0);color:var(--body-fg, #333)}.structured-field-editor .sf-object-fields{display:flex;flex-direction:column}.structured-field-editor .sf-array{margin-bottom:12px}.structured-field-editor .sf-array-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.structured-field-editor .sf-array-header .sf-label{margin-bottom:0}.structured-field-editor .sf-array-collapse-toggle{margin-left:auto;display:inline-flex;align-items:center;cursor:pointer;color:var(--body-quiet-color, #aaa);line-height:0}.structured-field-editor .sf-array-collapse-toggle:hover{color:var(--body-fg, #333)}.structured-field-editor .sf-array-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;background:var(--darkened-bg, #f0f0f0);border-radius:10px;font-size:.6875rem;font-weight:600;color:var(--body-quiet-color, #666)}.structured-field-editor .sf-array-items{display:flex;flex-direction:column;gap:8px}.structured-field-editor .sf-array-item{border:1px solid var(--border-color, #ccc);border-radius:4px;background:var(--darkened-bg, #f8f8f8);overflow:visible;position:relative}.structured-field-editor .sf-array-item[draggable=true]{cursor:grab}.structured-field-editor .sf-array-item.sf-dragging{opacity:.4}.structured-field-editor .sf-array-item.sf-drag-over{outline:2px solid var(--primary-color, #3b82f6);outline-offset:-2px}.structured-field-editor .sf-array-item-header{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:var(--darkened-bg, #f0f0f0);border-bottom:1px solid var(--border-color, #ccc);border-radius:4px 4px 0 0}.structured-field-editor .sf-array-item-left{display:flex;align-items:center;gap:6px}.structured-field-editor .sf-drag-handle{display:inline-flex;align-items:center;color:var(--body-quiet-color, #aaa);cursor:grab;padding:0 2px;line-height:0}.structured-field-editor .sf-drag-handle:active{cursor:grabbing}.structured-field-editor .sf-drag-handle:hover{color:var(--body-fg, #555)}.structured-field-editor .sf-array-item-index{font-size:.6875rem;font-weight:600;color:var(--body-quiet-color, #666)}.structured-field-editor .sf-array-item-actions{display:flex;gap:4px}.structured-field-editor .sf-array-item-body{padding:10px}.structured-field-editor .sf-array-item-body>.sf-object.sf-object-root,.structured-field-editor .sf-array-item-body>.sf-object:not(.sf-object-root){border:none;padding:0;margin:0;background:rgba(0,0,0,0)}.structured-field-editor .sf-nullable{margin-bottom:12px}.structured-field-editor .sf-nullable-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}.structured-field-editor .sf-nullable-header .sf-label{margin-bottom:0}.structured-field-editor .sf-nullable-body{padding-left:0}.structured-field-editor .sf-nullable-body:not(:empty){border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-top:4px;background:var(--body-bg, #fff)}.structured-field-editor .sf-union{margin-bottom:12px}.structured-field-editor .sf-union-body{border:1px solid var(--border-color, #ccc);border-radius:4px;padding:12px;margin-top:8px;background:var(--body-bg, #fff)}.structured-field-editor .sf-union-body>.sf-object{border:none;padding:0;margin:0;background:rgba(0,0,0,0)}.structured-field-editor .sf-relation{position:relative}.structured-field-editor .sf-relation-wrapper{display:flex;flex-direction:column;gap:6px}.structured-field-editor .sf-relation-selected{display:flex;flex-wrap:wrap;gap:6px}.structured-field-editor .sf-relation-selected:empty{display:none}.structured-field-editor .sf-relation-tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;background:var(--darkened-bg, #f0f0f0);border:1px solid var(--border-color, #ccc);border-radius:4px;font-size:.8125rem;color:var(--body-fg, #333);max-width:100%}.structured-field-editor .sf-relation-tag-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.structured-field-editor .sf-relation-tag-remove{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:none;border-radius:50%;background:rgba(0,0,0,0);color:var(--body-quiet-color, #666);cursor:pointer;font-size:.6875rem;flex-shrink:0;transition:color .15s,background .15s}.structured-field-editor .sf-relation-tag-remove:hover{color:var(--error-fg, #ba2121);background:rgba(186,33,33,.1)}.structured-field-editor .sf-relation-search{position:relative}.structured-field-editor .sf-relation-input{padding-left:28px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:8px center;background-size:14px}.structured-field-editor .sf-relation-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;max-height:240px;overflow-y:auto;background:var(--body-bg, #fff);border:1px solid var(--border-color, #ccc);border-top:none;border-radius:0 0 4px 4px;box-shadow:0 4px 12px rgba(0,0,0,.1)}.structured-field-editor .sf-relation-dropdown-item{padding:8px 12px;cursor:pointer;font-size:.8125rem;color:var(--body-fg, #333);transition:background .1s;border-bottom:1px solid var(--hairline-color, #eee)}.structured-field-editor .sf-relation-dropdown-item:last-child{border-bottom:none}.structured-field-editor .sf-relation-dropdown-item:hover,.structured-field-editor .sf-relation-dropdown-item.highlighted{background:var(--selected-bg, #e4e4e4)}.structured-field-editor .sf-relation-dropdown-empty{padding:12px;text-align:center;color:var(--body-quiet-color, #999);font-size:.8125rem;font-style:italic}.structured-field-editor .sf-relation-dropdown-more{padding:8px 12px;text-align:center;cursor:pointer;font-size:.75rem;color:var(--link-fg, #417690);font-weight:600;border-top:1px solid var(--hairline-color, #eee);transition:background .1s}.structured-field-editor .sf-relation-dropdown-more:hover{background:var(--darkened-bg, #f0f0f0)}