diff --git a/.changeset/6138-fields-schema-block-parity-pr1.md b/.changeset/6138-fields-schema-block-parity-pr1.md
new file mode 100644
index 0000000000..ac0d7216fe
--- /dev/null
+++ b/.changeset/6138-fields-schema-block-parity-pr1.md
@@ -0,0 +1,50 @@
+---
+---
+
+Docs only, publishes nothing: the `Field Schema` block on every
+`content/docs/fields` page declared an interface of its own, so
+`check-doc-snippet-types` compiled it **vacuously** — a self-declared interface
+with no imports type-checks no matter what it says, because nothing in the block
+refers to anything the packages export (objectui#6138). The gate reported those
+pages green and structurally could not see whether the documented shape matched
+the shipped one. This is batch 1 of 2: the shared page plus four converted
+pages, proving the mechanism end to end before it is applied to the rest.
+
+Each converted `Field Schema` block is now a literal **annotated** with that
+field type's exported `*FieldMetadata`, so the sealed type's excess-property
+check judges every documented key. The page becomes structurally incapable of
+teaching a key the type does not have.
+
+Measured before converting: all 26 pages diverged from their exported type, and
+on 24 of them the divergence was **entirely** `FieldWidgetComponentProps`
+members — a real, exported, reader-facing surface filed under a heading that
+says "Field Schema". Deleting them would have deleted correct API, so the shapes
+are separated instead of one being dropped: a new `content/docs/fields/widget-props.mdx`
+documents `FieldWidgetComponentProps` once, with a gate-compiled example and the
+type named as the source of truth, and the field pages link to it. That page
+carries no hand-maintained key list — the type has 76 members and a prose
+restatement of a declared surface is the defect class objectui#6086 is open for.
+
+Three documentation defects the conversion forced out into the open, each a page
+teaching something no shipped type declares:
+
+- `date.mdx` documented the range bounds as `min` / `max`; `DateFieldMetadata`
+ declares `min_date` / `max_date`, and the sibling `datetime.mdx` already
+ documented that spelling. Two adjacent reference pages taught two spellings of
+ one concept and one of them did not exist. The docs are corrected; the type is
+ not touched.
+- `textarea.mdx` named `TextAreaFieldMetadata`; the export is
+ `TextareaFieldMetadata` (lowercase `a`). A name that must resolve, so it now
+ does.
+- `rich-text.mdx` named `RichTextFieldMetadata`, which does not exist at all. The
+ page's own block says `type: 'markdown' | 'html'`, so it resolves against the
+ existing `MarkdownFieldMetadata` / `HtmlFieldMetadata` pair rather than a
+ minted type. It also documented `toolbar`, `preview`, `minHeight` and
+ `maxHeight`, which `RichTextField` reads nowhere (`minHeight` / `maxHeight`
+ have zero occurrences in `packages/fields/src`), and `rows`, which it does read
+ through an `as any` while neither metadata type declares it — filed as
+ objectui#6140 with that measurement in it.
+
+The gate's blocks-to-compile count rises from 225 to 227 — the new page's two
+blocks, the conversions being one-block-for-one-block — with diagnostics at 0, no
+new `FRAGMENT_MARKER` declarations, and the declared-fragment count unmoved at 111.
diff --git a/content/docs/fields/date.mdx b/content/docs/fields/date.mdx
index dc856d2d1e..4b67d16029 100644
--- a/content/docs/fields/date.mdx
+++ b/content/docs/fields/date.mdx
@@ -12,28 +12,35 @@ The Date Field component provides a date picker for selecting dates and optional
## With Default Value
-
## Field Schema
+A date field is authored as `DateFieldMetadata` (`@object-ui/types`), which is the
+source of truth for the key set: it extends `BaseFieldMetadata` with a display format
+and the two range bounds.
+
```ts
-interface DateFieldSchema {
- type: 'date' | 'datetime';
- name: string; // Field name/ID
- label?: string; // Field label
- placeholder?: string; // Placeholder text
- value?: string | Date; // Default value (ISO string or Date)
- required?: boolean; // Is field required
- readonly?: boolean; // Read-only mode
- disabled?: boolean; // Disabled state
- className?: string; // Additional CSS classes
-
- // Validation
- min?: string | Date; // Minimum date
- max?: string | Date; // Maximum date
- format?: string; // Display format
-}
+import type { DateFieldMetadata } from '@object-ui/types';
+
+const closeDate: DateFieldMetadata = {
+ type: 'date',
+ name: 'close_date',
+ label: 'Close Date',
+ placeholder: 'Pick a date',
+ required: true,
+ format: 'yyyy-MM-dd',
+ min_date: '2024-01-01',
+ max_date: '2030-12-31',
+ dueLike: true,
+};
```
+The range bounds are `min_date` and `max_date` — the same spelling the datetime field
+uses. `datetime` is its own type with its own metadata (`DateTimeFieldMetadata`); see
+[DateTime Field](/docs/fields/datetime).
+
+The value being edited, and the `className` / `disabled` a host supplies, are **not**
+metadata keys — they are runtime widget props. See [Field Widget Props](/docs/fields/widget-props).
+
## Date Formats
The date field supports various display formats:
diff --git a/content/docs/fields/meta.json b/content/docs/fields/meta.json
index d65baeab73..9625fd439e 100644
--- a/content/docs/fields/meta.json
+++ b/content/docs/fields/meta.json
@@ -26,6 +26,7 @@
"rich-text",
"object",
"vector",
- "grid"
+ "grid",
+ "widget-props"
]
}
diff --git a/content/docs/fields/rich-text.mdx b/content/docs/fields/rich-text.mdx
index 463b4104fa..848f78ba90 100644
--- a/content/docs/fields/rich-text.mdx
+++ b/content/docs/fields/rich-text.mdx
@@ -12,30 +12,40 @@ The Rich Text Field component provides a WYSIWYG editor for creating formatted t
## HTML Editor
-
## Field Schema
+`markdown` and `html` are two field types served by one widget, and each has its own
+exported metadata type — `MarkdownFieldMetadata` and `HtmlFieldMetadata`
+(`@object-ui/types`). Both extend `BaseFieldMetadata` and add a single length bound;
+there is no combined "rich text" metadata type.
+
```ts
-interface RichTextFieldSchema {
- type: 'markdown' | 'html';
- name: string; // Field name/ID
- label?: string; // Field label
- placeholder?: string; // Placeholder text
- value?: string; // Default value
- rows?: number; // Editor height in rows
- required?: boolean; // Is field required
- readonly?: boolean; // Read-only mode
- disabled?: boolean; // Disabled state
- className?: string; // Additional CSS classes
-
- // Editor Configuration
- toolbar?: boolean; // Show formatting toolbar
- preview?: boolean; // Show preview panel
- minHeight?: number; // Minimum height in pixels
- maxHeight?: number; // Maximum height in pixels
-}
+import type { HtmlFieldMetadata, MarkdownFieldMetadata } from '@object-ui/types';
+
+const releaseNotes: MarkdownFieldMetadata = {
+ type: 'markdown',
+ name: 'release_notes',
+ label: 'Release Notes',
+ placeholder: 'Write the notes…',
+ max_length: 20000,
+};
+
+const emailBody: HtmlFieldMetadata = {
+ type: 'html',
+ name: 'email_body',
+ label: 'Email Body',
+ max_length: 50000,
+};
```
+The editor is a plain textarea today — there is no formatting toolbar, no preview pane
+and no pixel height to configure, so neither metadata type declares one. The widget
+does size its inline editor from a `rows` key, but neither type declares that either;
+that gap is tracked as [objectui#6140](https://github.com/objectstack-ai/objectui/issues/6140).
+
+The value being edited, and the `className` / `disabled` a host supplies, are **not**
+metadata keys — they are runtime widget props. See [Field Widget Props](/docs/fields/widget-props).
+
## Supported Formats
### Markdown
diff --git a/content/docs/fields/text.mdx b/content/docs/fields/text.mdx
index 42e045b2b7..3d6f6f77cd 100644
--- a/content/docs/fields/text.mdx
+++ b/content/docs/fields/text.mdx
@@ -20,28 +20,32 @@ The Text Field component provides a single-line text input for collecting basic
## Read-Only
-
## Field Schema
+A text field is authored as `TextFieldMetadata` (`@object-ui/types`), which is the
+source of truth for the key set: it extends `BaseFieldMetadata` with the
+text-specific validation keys.
+
```ts
-interface TextFieldSchema {
- type: 'text';
- name: string; // Field name/ID
- label?: string; // Field label
- placeholder?: string; // Placeholder text
- value?: string; // Default value
- required?: boolean; // Is field required
- readonly?: boolean; // Read-only mode
- disabled?: boolean; // Disabled state
- className?: string; // Additional CSS classes
-
- // Validation
- min_length?: number; // Minimum character length
- max_length?: number; // Maximum character length
- pattern?: string | RegExp; // Validation pattern
-}
+import type { TextFieldMetadata } from '@object-ui/types';
+
+const productName: TextFieldMetadata = {
+ type: 'text',
+ name: 'product_name',
+ label: 'Product Name',
+ placeholder: 'Enter a product name',
+ help: 'Shown on the storefront and in search results.',
+ required: true,
+ min_length: 2,
+ max_length: 120,
+ pattern: '^[A-Za-z0-9 -]+$',
+ pattern_message: 'Letters, digits, spaces and hyphens only.',
+};
```
+The value being edited, and the `className` / `disabled` a host supplies, are **not**
+metadata keys — they are runtime widget props. See [Field Widget Props](/docs/fields/widget-props).
+
## Use Cases
- **User Names**: Collecting first names, last names, or usernames
diff --git a/content/docs/fields/textarea.mdx b/content/docs/fields/textarea.mdx
index 0c8723d10a..201ce05999 100644
--- a/content/docs/fields/textarea.mdx
+++ b/content/docs/fields/textarea.mdx
@@ -16,28 +16,30 @@ The TextArea Field component provides a multi-line text input for collecting lon
## Required Field
-
## Field Schema
+A textarea field is authored as `TextareaFieldMetadata` (`@object-ui/types`), which is
+the source of truth for the key set: it extends `BaseFieldMetadata` with the length
+bounds and the editor's visible row count.
+
```ts
-interface TextAreaFieldSchema {
- type: 'textarea';
- name: string; // Field name/ID
- label?: string; // Field label
- placeholder?: string; // Placeholder text
- rows?: number; // Number of visible rows (default: 3)
- value?: string; // Default value
- required?: boolean; // Is field required
- readonly?: boolean; // Read-only mode
- disabled?: boolean; // Disabled state
- className?: string; // Additional CSS classes
-
- // Validation
- min_length?: number; // Minimum character length
- max_length?: number; // Maximum character length
-}
+import type { TextareaFieldMetadata } from '@object-ui/types';
+
+const description: TextareaFieldMetadata = {
+ type: 'textarea',
+ name: 'description',
+ label: 'Description',
+ placeholder: 'Describe this record…',
+ rows: 6,
+ required: false,
+ min_length: 0,
+ max_length: 2000,
+};
```
+The value being edited, and the `className` / `disabled` a host supplies, are **not**
+metadata keys — they are runtime widget props. See [Field Widget Props](/docs/fields/widget-props).
+
## Use Cases
- **Descriptions**: Product descriptions, bio information
diff --git a/content/docs/fields/widget-props.mdx b/content/docs/fields/widget-props.mdx
new file mode 100644
index 0000000000..93b4422e49
--- /dev/null
+++ b/content/docs/fields/widget-props.mdx
@@ -0,0 +1,117 @@
+---
+title: "Field Widget Props"
+description: "What a field widget receives at runtime, and why those keys are not field metadata"
+---
+
+Every field reference page in this section documents one thing: the **metadata you
+author** for that field type. This page documents the other half — the props a field
+**widget** receives when it renders.
+
+The two are different shapes with different producers, and confusing them is the most
+common authoring mistake this section can cause. A key like the live value, the
+host-supplied `className`, or the form's `disabled` state belongs to the widget at
+runtime; writing it on an object's field definition publishes a key
+`@objectstack/spec`'s strict schemas reject.
+
+## The type is the source of truth
+
+`FieldWidgetComponentProps` is exported from `@object-ui/fields`. It is a **closed**
+type — there is no `[key: string]: any` in it (objectui#3221), so a misspelled prop is
+a compile error rather than a permanent `undefined`. That is what makes it usable as a
+reference: the compiler answers "is this a real prop", and this page does not have to.
+
+A widget implements it by taking it as its props type:
+
+```tsx
+import { toDomProps, type FieldWidgetComponentProps } from '@object-ui/fields';
+
+/** A custom single-line widget, registered for a field type of your own. */
+export function SlugWidget(props: FieldWidgetComponentProps) {
+ const { value, onChange, field, readonly, disabled, className, error } = props;
+
+ return (
+ onChange(event.target.value)}
+ placeholder={field.placeholder}
+ readOnly={readonly}
+ disabled={disabled}
+ // The widget drives the a11y state; the message TEXT stays with the form
+ // renderer, and the required MARKER is drawn by its label.
+ aria-invalid={Boolean(error)}
+ aria-required={Boolean(field.required)}
+ />
+ );
+}
+```
+
+Read the full member list from the type — in your editor, or from
+`packages/fields/src/widgets/types.ts`, where every key carries a doc comment naming
+its producer and its consumer. This page deliberately does not copy that list: a
+hand-maintained restatement of a declared surface is exactly the drift these pages are
+being fixed for.
+
+## What the categories are
+
+The type is assembled from five groups, and knowing which group a key is in tells you
+who supplies it:
+
+1. **The controlled-input contract.** The current value, the callback that changes it,
+ the field's metadata carrier, and the display-state flags every widget interprets.
+ Every widget in the package implements this group; the rest are optional.
+2. **Host plumbing.** What a rendering host forwards to widgets that need more than a
+ value — a data source for widgets that query records, live sibling-field values for
+ cascading and dependent options, resolved labels and hints for the copy those gates
+ render, a compact mode for grid cells, and record-selection callbacks for pickers.
+ A widget that needs none of it destructures none of it.
+3. **DOM pass-through.** The identity, focus and event keys that may legitimately land
+ on the element a widget renders — the field's `id`, the `aria-describedby` the form
+ control minted, and so on. `toDomProps` is this group's runtime executor, bound to
+ the declaration in both directions by compile-time assertions, so a key cannot be
+ declared here and silently never delivered.
+4. **The ARIA attribute family**, intersected in whole from React's `AriaAttributes`.
+ This group is the bulk of the member count, which is why the count is not a useful
+ thing to quote.
+5. **`data-*` attributes**, open by design and expressed as a template-literal key so
+ `keyof` stays finite — an undeclared prop still fails.
+
+## Metadata and props are two shapes, not one
+
+The authored metadata arrives at the widget under a single carrier
+(objectui#3233 converged it at the producers; there is no second key to check). The
+live value never lives on metadata, and the metadata never lives on the DOM:
+
+```ts
+import type { FieldWidgetComponentProps } from '@object-ui/fields';
+import type { FieldMetadata, TextFieldMetadata } from '@object-ui/types';
+
+// What you AUTHOR: object metadata, validated at publish.
+const slug: TextFieldMetadata = {
+ type: 'text',
+ name: 'slug',
+ label: 'Slug',
+ max_length: 80,
+};
+
+// What the widget RECEIVES at runtime.
+declare const props: FieldWidgetComponentProps;
+
+const carrier: FieldMetadata = props.field; // the authored metadata, unchanged
+const live: string = props.value; // never a metadata key
+
+export { slug, carrier, live };
+```
+
+Assigning `slug` into `props.field` type-checks; the reverse — writing `props.value`
+into `slug` — does not, and that asymmetry is the whole distinction.
+
+## Where each half is documented
+
+- **Metadata keys** — the `Field Schema` section of each field page in this section,
+ as a literal annotated with that field type's exported `*FieldMetadata`.
+- **Runtime props** — this page, and the type it names.