# SchemaForm

> A form generated from a JSON Schema, with enums, booleans, dates, objects nested to any depth, lists of values and lists of objects (add, remove, reorder, collapse), foreign keys, show and hide rules by path, client validation and server field errors on nested paths.

Source: https://docs.nasaqui.com/components/schema-form

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/schema-form.json
```

Describe a record once as a JSON Schema and get its edit form: labels, inputs, validation messages in English and
Arabic, sections, lists of rows, and foreign-key pickers. It reuses [SchemaRepeater](https://docs.nasaqui.com/components/schema-repeater)'s
field types and validators, [RelationPicker](https://docs.nasaqui.com/components/relation-picker) for foreign keys, and the show, hide and
require rules of [RuleBuilder](https://docs.nasaqui.com/components/rule-builder) (the same engine as [PublicForm](https://docs.nasaqui.com/components/public-form)).

## When to use

- Admin screens driven by a schema from your API, plugin settings, or a form a customer defines.

## When not to use

- A one-off form with special layout: build it from Field and Input.
- A public embed: [PublicForm](https://docs.nasaqui.com/components/public-form).

## Import

```tsx
import { SchemaForm, schemaFormFields, schemaFormOutput } from "@fadymondy/nasaq/web";
```

## Quick start

```tsx
<SchemaForm
  schema={{
    type: "object",
    required: ["name"],
    properties: {
      name: { type: "string", title: "Name", "x-title-ar": "الاسم" },
      status: { type: "string", enum: ["draft", "live"] },
      owner: { type: "string", title: "Owner", "x-relation": { resource: "users" } },
    },
  }}
  relations={{ users: { search: (q, signal) => fetchUsers(q, signal) } }}
  onSubmit={async (value) => {
    const res = await api.save(value);
    if (!res.ok) return { fieldErrors: res.errors };
  }}
/>
```

## Supported schema

| Schema | Field |
| --- | --- |
| `string` | text. `format` `email`, `uri`, `tel` set the input type; `date` gives a date picker; `x-widget: "textarea"` or `maxLength` over 200 gives a text area. |
| `enum`, or `oneOf` of `const` with `title` | select. Numeric enums come back as numbers. |
| `number`, `integer` | number with `minimum`, `maximum`, `multipleOf` (step) and `x-unit`. |
| `boolean` | switch. |
| `array` of `string` | tag input (`maxItems` caps the tags; each tag is checked as it is added). |
| `array` of `number`, `integer`, `date` or a plain enum | repeated inputs with Add and Remove. |
| `array` of an enum with `uniqueItems: true` | checkbox group. The value keeps the order of the options. |
| `array` of objects | groups: collapsible, with add, remove, up and down, and drag. Each group is titled by `x-title` (or the first string field). |
| `object` | a fieldset. Objects nest to any depth, inside objects and inside array items. |
| `x-relation: { resource, multiple }` | RelationPicker. |
| `$ref` to `#/$defs/...` | resolved. |

Also: `title`, `description`, `default`, `required`, `pattern`, `minLength`, `maxLength`, `minItems`, `maxItems`, `uniqueItems`,
`x-title-ar`, `x-description-ar`, `x-enum-titles`, `x-enum-titles-ar`, `x-placeholder`, `x-width: "half"`, `x-order`, and `x-title`
(a property key, or a template such as `"{name} - {role}"`, that titles each group; `x-title-key` still works).
Remote `$ref`, arrays of arrays and unions are not drawn; `schemaFormTree(schema).unsupported` lists them.

## Paths

Every field has a path: `address.city`, `contacts[1].phone`, `contacts[1].phones[0]`. Paths are used for errors, rules and focus.

- **Errors.** Client errors, `fieldErrors` from `onSubmit` and the validation summary all use full paths. A server key can be
  written `contacts[1].phone`, `contacts.1.phone` or the JSON pointer `/contacts/1/phone`. A path into a group that no longer exists lands
  on its list, and a key that matches nothing is shown for the whole form. Errors of tags show on the list, since tags are one control.
- **Summary.** After a failed submit the form lists what to fix. Each line is a button that opens the collapsed groups on the way and puts
  focus in the field. A collapsed group with problems shows an issue count.
- **Rules.** `config.target` and a condition's `field` accept an absolute path (`company.type`), a pattern for every item
  (`contacts[].phone`) and, in conditions, a path relative to the target: `./kind` is a sibling (inside an array item, the same item) and
  `../kind` is a sibling of the enclosing object or item. Hidden fields, and everything inside a hidden object or item, are not validated or submitted.

```tsx
// Show the tax id of a contact only while that contact is the billing one.
{ conditions: /* ./kind is "billing" */, actions: [{ type: "show", config: { target: "contacts[].taxId" } }] }
```

Removing a group that has data asks first. `minItems` disables Remove at the minimum; `maxItems` disables Add at the limit and says so.

## Anatomy

```
SchemaForm               <form data-slot="schema-form">
├─ grid                  fields, two columns from the small breakpoint
├─ fieldset              one per nested object, at any depth
├─ list                  tags, repeated inputs, checkbox group, or groups of fields
│  └─ group              header (drag, title, issues, up, down, remove) and a collapsible body
├─ summary               how many fields to fix, with a button for each
├─ Alert                 server error, saved, or how many fields to fix
└─ actions               Save, Reset
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `schema` | `SchemaFormJson` | required | |
| `value` / `defaultValue` / `onValueChange` | `Record<string, unknown>` | none | Output shape: nested, numbers as numbers, empty text as `null`. |
| `onSubmit` | `(value) => Promise<void \| { error?, fieldErrors? }>` | none | Runs only when valid. Return server errors, keyed by path, to show them on the fields. |
| `rules` | `FormRule[]` | none | Show, hide and require by field path (`config.target`). See Paths. |
| `relations` | `Record<resource, { search, options, resolve, onCreate }>` | none | Sources for `x-relation`. |
| `submitLabel`, `hideActions`, `disabled`, `label` | | | |
| `locale`, `labels`, `className` | | | |

`labels` overrides any string, including the group actions (`add`, `remove`, `moveUp`, `moveDown`, `removeTitle`, `removeBody`, ...).

Helpers without React, for tests and for the server:

- Tree: `schemaFormTree(schema)`, `schemaFormTreeDefaults`, `schemaFormTreeInitial`, `schemaFormTreeOutput`.
- Checks: `schemaFormTreeValidate(root, values)` returns `{ [path]: message }`. `schemaFormTreeStates(root, values, rules)` returns visibility and required by path. `schemaFormMapErrors(root, values, errors)` returns `{ fields, unmatched }`.
- Paths: `schemaPathParse`, `schemaPathGet`, `schemaPathSet` (immutable), `schemaPathResolve`, `schemaPathMatches`.
- Flat, one level, kept for compatibility: `schemaFormFields`, `schemaFormInitial`, `schemaFormOutput`, `schemaFormFlatten`, `schemaFormResolve`, `schemaFormHumanize`.

## Accessibility

- Every input has a label; required fields carry `*` and `required`.
- Errors show when a field was edited or after a failed submit; the first invalid field takes focus.
- A summary tells how many fields to fix, with a button per problem that opens collapsed groups and moves focus to the field. Hidden fields are removed, not just dimmed, and are not validated or submitted.
- Lists are fieldsets with a legend. Each group header is a button with `aria-expanded`. Up, down and remove buttons name the group ("Move Layla up"), so reordering works without a pointer. After add, remove or move, focus stays where the person was working.

## RTL & i18n

- Titles use `x-title-ar` when the locale is Arabic and fall back to a humanised key. Messages come from the Arabic set.
- Emails, URLs, phone numbers and numbers are entered left to right. The group chevron flips in RTL, and every group string comes from the Arabic set.

## Styling & tokens

Field, Alert and Button tokens. Extend with `className`.

## Do / Don't

- Do run the same validation on the server.
- Do give every enum a title for Arabic, or `x-enum-titles-ar`.
- Do not rely on `x-` hints for security; they only shape the form.
- Do give each group a title (`x-title`) so a collapsed group is still recognisable.
- Do not use `.`, `[` or `]` in property names: they are path separators.

## Related

- [SchemaRepeater](https://docs.nasaqui.com/components/schema-repeater)
- [RelationPicker](https://docs.nasaqui.com/components/relation-picker)
- [RuleBuilder](https://docs.nasaqui.com/components/rule-builder)
- [Infolist](https://docs.nasaqui.com/components/infolist)

## Lab

https://docs.nasaqui.com/?path=/docs/components-form-builders-schema-form--docs

## Code

### React

```tsx
<SchemaForm
  schema={{
    type: "object",
    required: ["name"],
    properties: {
      name: { type: "string", title: "Name", "x-title-ar": "الاسم" },
      status: { type: "string", enum: ["draft", "live"] },
      owner: { type: "string", title: "Owner", "x-relation": { resource: "users" } },
    },
  }}
  relations={{ users: { search: (q, signal) => fetchUsers(q, signal) } }}
  onSubmit={async (value) => {
    const res = await api.save(value);
    if (!res.ok) return { fieldErrors: res.errors };
  }}
/>
```

### shadcn

```tsx
<SchemaForm
  schema={{
    type: "object",
    required: ["name"],
    properties: {
      name: { type: "string", title: "Name", "x-title-ar": "الاسم" },
      status: { type: "string", enum: ["draft", "live"] },
      owner: { type: "string", title: "Owner", "x-relation": { resource: "users" } },
    },
  }}
  relations={{ users: { search: (q, signal) => fetchUsers(q, signal) } }}
  onSubmit={async (value) => {
    const res = await api.save(value);
    if (!res.ok) return { fieldErrors: res.errors };
  }}
/>
```

### Vue

```vue
<script setup lang="ts">
import { NqSchemaForm } from "@fadymondy/nasaq/vue";

const schema = {
  type: "object",
  required: ["name"],
  properties: {
    name: { type: "string", title: "Name", "x-title-ar": "الاسم" },
    status: { type: "string", enum: ["draft", "live"] },
    owner: { type: "string", title: "Owner", "x-relation": { resource: "users" } },
  },
};

// Your API calls.
const users = [
  { value: "u1", label: "Layla Hassan" },
  { value: "u2", label: "Omar Nasser" },
];
const relations = {
  users: { search: async (query: string) => users.filter((u) => u.label.toLowerCase().includes(query.toLowerCase())) },
};

async function save(value: Record<string, unknown>) {
  const res = await fetch("/api/projects", { method: "POST", body: JSON.stringify(value) });
  if (!res.ok) return { fieldErrors: (await res.json()).errors as Record<string, string> };
}
</script>

<template>
  <NqSchemaForm :schema="schema" :relations="relations" :on-submit="save" />
</template>
```

### Blade

```blade
{{-- The form comes from a JSON Schema (a PHP array). Save fires nq-schema-form-submit once it is valid; resolve the promise with { fieldErrors } to show the server's errors. --}}
<x-nq::schema-form
    :schema="[
        'type' => 'object',
        'required' => ['name'],
        'properties' => [
            'name' => ['type' => 'string', 'title' => 'Name', 'x-title-ar' => 'الاسم'],
            'status' => ['type' => 'string', 'enum' => ['draft', 'live']],
            'owner' => ['type' => 'string', 'title' => 'Owner', 'x-relation' => ['resource' => 'users']],
        ],
    ]"
    :relations="['users' => ['options' => [['value' => 'u1', 'label' => 'Layla Hassan'], ['value' => 'u2', 'label' => 'Omar Khaled']]]]"
    x-on:nq-schema-form-submit="$event.detail.waitUntil(fetch('/api/projects', { method: 'POST', body: JSON.stringify($event.detail.value) }).then(() => ({})))" />
```

### HTML + Alpine

```html
<form data-slot="schema-form" x-data="nqSchemaForm(JSON.parse('{\u0022tree\u0022:{\u0022kind\u0022:\u0022object\u0022,\u0022key\u0022:\u0022\u0022,\u0022path\u0022:\u0022\u0022,\u0022label\u0022:\u0022\u0022,\u0022required\u0022:true,\u0022children\u0022:[{\u0022kind\u0022:\u0022field\u0022,\u0022key\u0022:\u0022name\u0022,\u0022path\u0022:\u0022name\u0022,\u0022label\u0022:\u0022Name\u0022,\u0022required\u0022:true,\u0022field\u0022:{\u0022key\u0022:\u0022name\u0022,\u0022label\u0022:\u0022Name\u0022,\u0022required\u0022:true,\u0022type\u0022:\u0022text\u0022,\u0022inputType\u0022:\u0022text\u0022},\u0022numeric\u0022:false},{\u0022kind\u0022:\u0022field\u0022,\u0022key\u0022:\u0022status\u0022,\u0022path\u0022:\u0022status\u0022,\u0022label\u0022:\u0022Status\u0022,\u0022required\u0022:false,\u0022field\u0022:{\u0022key\u0022:\u0022status\u0022,\u0022label\u0022:\u0022Status\u0022,\u0022type\u0022:\u0022select\u0022,\u0022options\u0022:[{\u0022value\u0022:\u0022draft\u0022,\u0022label\u0022:\u0022Draft\u0022},{\u0022value\u0022:\u0022live\u0022,\u0022label\u0022:\u0022Live\u0022}]},\u0022numeric\u0022:false},{\u0022kind\u0022:\u0022field\u0022,\u0022key\u0022:\u0022owner\u0022,\u0022path\u0022:\u0022owner\u0022,\u0022label\u0022:\u0022Owner\u0022,\u0022required\u0022:false,\u0022field\u0022:{\u0022key\u0022:\u0022owner\u0022,\u0022label\u0022:\u0022Owner\u0022,\u0022type\u0022:\u0022text\u0022,\u0022relation\u0022:{\u0022resource\u0022:\u0022users\u0022,\u0022multiple\u0022:false}},\u0022numeric\u0022:false}]},\u0022value\u0022:{},\u0022rules\u0022:[],\u0022ar\u0022:false,\u0022disabled\u0022:false,\u0022submitLabel\u0022:\u0022Save\u0022,\u0022labels\u0022:{}}'))" novalidate x-on:submit.prevent="submit()" x-on:reset.prevent="reset()"
        x-on:nq-schema-form-submit="$event.detail.waitUntil(fetch(&#039;/api/projects&#039;, { method: &#039;POST&#039;, body: JSON.stringify($event.detail.value) }).then(() =&gt; ({})))" class="flex min-w-0 flex-col gap-6">
    <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
                                <div x-show="visible(`name`)" style="display: none" data-schema-path="name" class="min-w-0 sm:col-span-2">
                                    <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Name<span x-show="required(`name`)" aria-hidden="true" class="ms-0.5 text-nq-danger-text">*</span></label>
        <input data-slot="input" type="text"
                    x-model="m[`name`]" x-bind:aria-invalid="ariaInvalid(`name`)" x-bind:data-invalid="invalidAttr(`name`)" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control">
                <div data-slot="field-error" role="alert" x-show="msg(`name`)" x-text="msg(`name`)" style="display: none" class="text-caption text-nq-danger-text"></div>
</div>
                            </div>
                                <div x-show="visible(`status`)" style="display: none" data-schema-path="status" class="min-w-0 sm:col-span-2">
                                    <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Status<span x-show="required(`status`)" aria-hidden="true" class="ms-0.5 text-nq-danger-text" style="display: none">*</span></label>
        <div data-slot="select" x-data="nqSelect(null, false)" x-modelable="value" x-id="['nq-select']" x-model="m[`status`]" class="contents">
    <button data-slot="select-trigger" x-ref="trigger" x-bind="trigger"
        x-bind:data-invalid="invalidAttr(`status`)" x-bind:aria-invalid="ariaInvalid(`status`)" class="flex h-control w-full min-w-0 items-center justify-between gap-2 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] cursor-default select-none outline-none transition-colors duration-150 ease-nq focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-popup-open:border-nq-focus data-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px]">
    <span data-slot="select-value" x-text="label() ?? 'Select\u2026'" :data-placeholder="empty() ? '' : undefined"
    class="min-w-0 flex-1 truncate text-start data-placeholder:text-muted-foreground"></span>
    <span class="flex shrink-0 text-muted-foreground [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg></span>
</button>
        <template x-teleport="body">
    <div data-slot="select-content" x-ref="popup" x-bind="popup" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.trigger"
        class="z-50 min-w-[var(--anchor-width)] max-h-[var(--available-height)] overflow-y-auto rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
        <div data-slot="select-item" x-bind="item('draft', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('draft')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate">Draft</span>
</div>
                            <div data-slot="select-item" x-bind="item('live', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('live')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="select-item-text" class="min-w-0 flex-1 truncate">Live</span>
</div>
    </div>
</template>
    </div>
                <div data-slot="field-error" role="alert" x-show="msg(`status`)" x-text="msg(`status`)" style="display: none" class="text-caption text-nq-danger-text"></div>
</div>
                            </div>
                                <div x-show="visible(`owner`)" style="display: none" data-schema-path="owner" class="min-w-0 sm:col-span-2">
                                    <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Owner<span x-show="required(`owner`)" aria-hidden="true" class="ms-0.5 text-nq-danger-text" style="display: none">*</span></label>
        <div data-slot="relation-picker" x-data="nqRelationPicker(JSON.parse('{\u0022value\u0022:null,\u0022multiple\u0022:false,\u0022options\u0022:[{\u0022value\u0022:\u0022u1\u0022,\u0022label\u0022:\u0022Layla Hassan\u0022},{\u0022value\u0022:\u0022u2\u0022,\u0022label\u0022:\u0022Omar Khaled\u0022}],\u0022searchUrl\u0022:null,\u0022resolveUrl\u0022:null,\u0022createUrl\u0022:null,\u0022debounce\u0022:250,\u0022ar\u0022:false,\u0022t\u0022:{\u0022empty\u0022:\u0022No results\u0022,\u0022hint\u0022:\u0022Type to search\u0022,\u0022searching\u0022:\u0022Searching\\u2026\u0022,\u0022error\u0022:\u0022Could not load results.\u0022,\u0022create\u0022:\u0022Create \\u201c{q}\\u201d\u0022,\u0022createFailed\u0022:\u0022Could not create it.\u0022}}'))" x-modelable="value" x-id="['nq-relation']"
    x-bind:data-busy="status === 'loading' || createState === 'busy' ? '' : undefined" x-model="m[`owner`]" x-bind:aria-invalid="ariaInvalid(`owner`)" class="min-w-0">
            <div data-slot="combobox-input-group" x-ref="anchor" class="flex min-h-control min-w-0 w-full items-center gap-1 rounded-control border border-input bg-card text-body text-foreground transition-colors duration-150 ease-nq focus-within:border-nq-focus focus-within:outline-1 focus-within:outline-nq-focus has-[[data-invalid]]:border-nq-danger has-[[aria-invalid=true]]:border-nq-danger has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 h-control ps-3 pe-1.5">
            <input data-slot="combobox-input" x-ref="input" x-bind="input" placeholder="Search…" aria-label="Owner"
                                                class="h-full min-w-0 flex-1 border-0 bg-transparent text-body text-foreground outline-none placeholder:text-muted-foreground pointer-coarse:text-[16px]">
            <button data-slot="combobox-clear" x-bind="clear" aria-label="Clear"
                class="flex size-6 shrink-0 cursor-default items-center justify-center rounded-control text-muted-foreground outline-none hover:text-foreground focus-visible:outline-1 focus-visible:outline-nq-focus [&_svg]:size-4 data-[hidden]:hidden"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg></button>
            <button data-slot="combobox-trigger" x-bind="trigger" aria-label="Open list"
                class="flex size-6 shrink-0 cursor-default items-center justify-center rounded-control text-muted-foreground outline-none hover:text-foreground focus-visible:outline-1 focus-visible:outline-nq-focus [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg></button>
        </div>
        <template x-teleport="body">
        <div data-slot="combobox-content" x-ref="popup" x-bind="popup" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.anchor"
            class="z-50 w-[var(--anchor-width)] max-h-[min(var(--available-height),20rem)] overflow-y-auto rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
            <div data-slot="combobox-empty" x-bind="emptyState" class="px-2.5 py-2 text-body-sm text-muted-foreground">
                <span class="flex items-center justify-center gap-2" role="status">
                    <span x-show="status === 'loading'" x-cloak class="contents"><svg aria-hidden="true" class="size-4 animate-spin" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg></span>
                    <span x-show="status === 'error'" x-cloak class="contents"><svg aria-hidden="true" class="size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/>
  <path d="M12 9v4"/>
  <path d="M12 17h.01"/>
</svg></span>
                    <span x-text="emptyText()"></span>
                    <button type="button" x-show="status === 'error'" x-cloak x-on:click="refresh(true)" class="text-foreground underline underline-offset-2">Try again</button>
                </span>
            </div>
            <template x-for="o in rows()" :key="o.value">
                <div data-slot="combobox-item" x-bind="row(o)"
                    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
                    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
                        <span x-show="isSelected(o.value)" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
                    </span>
                    <span data-slot="combobox-item-text" class="min-w-0 flex-1 truncate">
                        <template x-if="isCreate(o)">
                            <span class="flex items-center gap-2 text-foreground"><svg aria-hidden="true" class="size-4 shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
  <path d="M12 5v14"/>
</svg><bdi dir="auto" class="truncate" x-text="o.label"></bdi></span>
                        </template>
                        <template x-if="!isCreate(o)">
                            <span class="flex min-w-0 flex-col leading-tight">
                                <bdi dir="auto" class="truncate" x-text="text(o)"></bdi>
                                <bdi x-show="o.description" dir="auto" class="truncate text-caption text-muted-foreground" x-text="o.description"></bdi>
                            </span>
                        </template>
                    </span>
                </div>
            </template>
        </div>
    </template>
    <p x-show="createState === 'failed'" x-cloak role="alert" class="mt-1.5 text-caption text-nq-danger-text">Could not create it.</p>
    </div>
                <div data-slot="field-error" role="alert" x-show="msg(`owner`)" x-text="msg(`owner`)" style="display: none" class="text-caption text-nq-danger-text"></div>
</div>
                            </div>
            </div>
    <div data-slot="alert" data-tone="danger" role="alert"
        x-show="formError" style="display: none" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground"><span x-text="formError"></span></div>
            </div>
    </div>
    <div data-slot="schema-form-unmatched" data-tone="danger" role="alert"
        x-show="hasUnmatched()" style="display: none" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground"><p>The server reported problems that do not belong to a field.</p>
        <ul class="mt-1 list-disc ps-5">
            <template x-for="u in unmatched()" :key="u.path"><li x-text="u.path + ': ' + u.message"></li></template>
        </ul></div>
            </div>
    </div>
    <div data-slot="alert" data-tone="success" role="status"
        x-show="isSaved()" style="display: none" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-success/30 bg-nq-success-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-success-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m9 12 2 2 4-4"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground">Saved.</div>
            </div>
    </div>
    <div data-slot="schema-form-summary" data-tone="warning" role="alert"
        x-show="showSummary()" style="display: none" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-warning/30 bg-nq-warning-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-warning-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <line x1="12" x2="12" y1="8" y2="12"/>
  <line x1="12" x2="12.01" y1="16" y2="16"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground"><p x-text="t.fix(issueTotal())"></p>
        <ul aria-label="Fields to fix" class="mt-1 flex flex-col items-start gap-0.5">
            <template x-for="p in summary()" :key="p">
                <li>
                    <button type="button" x-bind:data-path="p" x-on:click="focusPath(p)" x-text="summaryText(p)"
                        class="rounded-control text-start underline underline-offset-2 outline-none focus-visible:outline-2 focus-visible:outline-nq-focus"></button>
                </li>
            </template>
        </ul></div>
            </div>
    </div>
            <div class="flex flex-wrap items-center gap-2">
            <button data-slot="button"
     type="submit"                         x-bind:aria-busy="busyAttr()" x-bind:disabled="cannotSubmit()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        <span x-text="isBusy() ? t.saving : submitLabel">Save</span></button>
            <button data-slot="button"
     type="reset"                         x-bind:disabled="cannotReset()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Reset</button>
        </div>
    </form>
```
