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.