Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .changeset/6138-fields-schema-block-parity-pr1.md
Original file line numberDiff line numberDiff line change
@@ -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.
41 changes: 24 additions & 17 deletions content/docs/fields/date.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,28 +12,35 @@ The Date Field component provides a date picker for selecting dates and optional
## With Default Value

<SchemaExample id="fields-date/with-default-date" />

## 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:
Expand Down
3 changes: 2 additions & 1 deletion content/docs/fields/meta.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@
"rich-text",
"object",
"vector",
"grid"
"grid",
"widget-props"
]
}
48 changes: 29 additions & 19 deletions content/docs/fields/rich-text.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,30 +12,40 @@ The Rich Text Field component provides a WYSIWYG editor for creating formatted t
## HTML Editor

<SchemaExample id="fields-rich-text/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
Expand Down
38 changes: 21 additions & 17 deletions content/docs/fields/text.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,28 +20,32 @@ The Text Field component provides a single-line text input for collecting basic
## Read-Only

<SchemaExample id="fields-text/read-only-field" />

## 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
Expand Down
36 changes: 19 additions & 17 deletions content/docs/fields/textarea.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,28 +16,30 @@ The TextArea Field component provides a multi-line text input for collecting lon
## Required Field

<SchemaExample id="fields-textarea/required-textarea" />

## 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
Expand Down
117 changes: 117 additions & 0 deletions content/docs/fields/widget-props.mdx
Original file line numberDiff line numberDiff line change
@@ -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<string>) {
const { value, onChange, field, readonly, disabled, className, error } = props;

return (
<input
// The host plumbing, filtered to what may legitimately reach a DOM
// element. Never a bare `{...props}` spread: renderer-only props and
// authored field-config keys must not become DOM attributes.
{...toDomProps(props)}
className={className}
value={value ?? ''}
onChange={(event) => 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<string>;

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.