From 47be70e596cfea54abb2a242434e3d8770af8d6c Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:51:57 +0800 Subject: [PATCH] feat(spec): complete FormView protocol with form-presentation props (conformance) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The conformance check flagged 14 object-form props as frontend-only — the ObjectForm component accepts formType variants' config (drawerSide/drawerWidth/ modalSize/split*/tabPosition/layout/…) that FormViewSchema didn't declare. Add them (all optional, additive — view isn't in the property-liveness GOVERNED set; 6650 spec tests still green; api-surface unchanged). The react contract now sources them from the spec (with descriptions), and object-form conformance is 15 matched / 6 spec-only / 0 frontend-only (was 1 / 6 / 14). Co-Authored-By: Claude Opus 4.8 --- .changeset/formview-presentation-props.md | 12 ++++ packages/spec/src/ui/react-blocks.ts | 2 +- packages/spec/src/ui/view.zod.ts | 27 ++++++++- .../contracts/react-blocks.contract.json | 56 ++++++++++++++++--- .../objectstack-ui/references/react-blocks.md | 8 ++- 5 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 .changeset/formview-presentation-props.md diff --git a/.changeset/formview-presentation-props.md b/.changeset/formview-presentation-props.md new file mode 100644 index 0000000000..bd3648025f --- /dev/null +++ b/.changeset/formview-presentation-props.md @@ -0,0 +1,12 @@ +--- +"@objectstack/spec": minor +--- + +Complete the FormView protocol with the form-presentation options the ObjectForm +component already accepts (conformance follow-up). FormViewSchema gains optional +`layout`, `columns`, `title`, `description`, `defaultTab`, `tabPosition`, +`allowSkip`, `showStepIndicator`, `splitDirection`, `splitSize`, `splitResizable`, +`drawerSide`, `drawerWidth`, `modalSize` — the per-`type` (tabbed/wizard/split/ +drawer/modal) presentation config. The spec↔frontend conformance check went from +14 frontend-only → 0 for object-form; the react-tier contract now sources these +from the spec (with descriptions) instead of a hand-authored overlay. diff --git a/packages/spec/src/ui/react-blocks.ts b/packages/spec/src/ui/react-blocks.ts index 6d8f6c1827..1362e30907 100644 --- a/packages/spec/src/ui/react-blocks.ts +++ b/packages/spec/src/ui/react-blocks.ts @@ -69,7 +69,7 @@ export const REACT_BLOCKS: ReactBlockDef[] = [ schemaType: 'object-form', summary: "Server-connected create/edit/view form for one object. Config props come from the spec FormView schema; bind + wire it with the React props below.", schema: FormViewSchema, - dataProps: ['sections', 'subforms', 'submitBehavior', 'defaultSort'], + dataProps: ['layout', 'columns', 'tabPosition', 'drawerSide', 'drawerWidth', 'modalSize', 'splitDirection', 'sections', 'subforms', 'submitBehavior'], interactions: [ OBJECT_NAME, { name: 'mode', type: "'create' | 'edit' | 'view'", kind: 'controlled', description: 'Create a new record, or edit/view an existing one — drive from React state.' }, diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index fbf92c987a..327b6375ed 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -801,7 +801,32 @@ export const FormViewSchema = lazySchema(() => z.object({ 'drawer', // Side panel 'modal' // Dialog ]).default('simple'), - + + // --- Presentation options (per `type` variant). These mirror what the + // ObjectForm component accepts so the protocol declares them; all optional. --- + /** Field layout within the form body. */ + layout: z.enum(['vertical', 'horizontal', 'inline', 'grid']).optional().describe('Field layout direction'), + /** Number of columns for the form body (grid/multi-column layouts). */ + columns: z.number().int().min(1).optional().describe('Number of columns for the form body'), + /** Optional form title / description (for embedded or standalone forms). */ + title: z.string().optional().describe('Form title'), + description: z.string().optional().describe('Form description'), + /** Tabbed (`type: 'tabbed'`). */ + defaultTab: z.string().optional().describe('Initially active tab (tabbed forms)'), + tabPosition: z.enum(['top', 'bottom', 'left', 'right']).optional().describe('Tab strip position (tabbed forms)'), + /** Wizard (`type: 'wizard'`). */ + allowSkip: z.boolean().optional().describe('Allow skipping steps (wizard forms)'), + showStepIndicator: z.boolean().optional().describe('Show the step indicator (wizard forms)'), + /** Split (`type: 'split'`). */ + splitDirection: z.enum(['horizontal', 'vertical']).optional().describe('Split orientation (split forms)'), + splitSize: z.number().optional().describe('Primary split panel size, % (split forms)'), + splitResizable: z.boolean().optional().describe('Whether the split is resizable (split forms)'), + /** Drawer (`type: 'drawer'`). */ + drawerSide: z.enum(['top', 'bottom', 'left', 'right']).optional().describe('Drawer side (drawer forms)'), + drawerWidth: z.string().optional().describe('Drawer width, e.g. "480px" (drawer forms)'), + /** Modal (`type: 'modal'`). */ + modalSize: z.enum(['sm', 'default', 'lg', 'xl', 'full']).optional().describe('Modal size (modal forms)'), + /** Data Source Configuration */ data: ViewDataSchema.optional().describe('Data source configuration (defaults to "object" provider)'), diff --git a/skills/objectstack-ui/contracts/react-blocks.contract.json b/skills/objectstack-ui/contracts/react-blocks.contract.json index b9a5e24139..31ead77312 100644 --- a/skills/objectstack-ui/contracts/react-blocks.contract.json +++ b/skills/objectstack-ui/contracts/react-blocks.contract.json @@ -80,6 +80,55 @@ "required": false, "description": "Custom persistence instead of the default create/update." }, + { + "name": "layout", + "type": "'vertical' | 'horizontal' | 'inline' | 'grid'", + "kind": "data", + "required": false, + "description": "Field layout direction" + }, + { + "name": "columns", + "type": "integer", + "kind": "data", + "required": false, + "description": "Number of columns for the form body" + }, + { + "name": "tabPosition", + "type": "'top' | 'bottom' | 'left' | 'right'", + "kind": "data", + "required": false, + "description": "Tab strip position (tabbed forms)" + }, + { + "name": "drawerSide", + "type": "'top' | 'bottom' | 'left' | 'right'", + "kind": "data", + "required": false, + "description": "Drawer side (drawer forms)" + }, + { + "name": "drawerWidth", + "type": "string", + "kind": "data", + "required": false, + "description": "Drawer width, e.g. \"480px\" (drawer forms)" + }, + { + "name": "modalSize", + "type": "'sm' | 'default' | 'lg' | 'xl' | 'full'", + "kind": "data", + "required": false, + "description": "Modal size (modal forms)" + }, + { + "name": "splitDirection", + "type": "'horizontal' | 'vertical'", + "kind": "data", + "required": false, + "description": "Split orientation (split forms)" + }, { "name": "sections", "type": "object[]", @@ -100,13 +149,6 @@ "kind": "data", "required": false, "description": "Post-submit behavior" - }, - { - "name": "defaultSort", - "type": "object[]", - "kind": "data", - "required": false, - "description": "Default sort order for related list views within this form" } ] }, diff --git a/skills/objectstack-ui/references/react-blocks.md b/skills/objectstack-ui/references/react-blocks.md index a08b9ef57d..5a270cf68f 100644 --- a/skills/objectstack-ui/references/react-blocks.md +++ b/skills/objectstack-ui/references/react-blocks.md @@ -27,10 +27,16 @@ Server-connected create/edit/view form for one object. Config props come from th | `onError` | `(error: Error) => void` | callback | | Called when the save fails. | | `onCancel` | `() => void` | callback | | Called when the user cancels. | | `submitHandler` | `(values) => any \| Promise` | callback | | Custom persistence instead of the default create/update. | +| `layout` | `'vertical' \| 'horizontal' \| 'inline' \| 'grid'` | data | | Field layout direction | +| `columns` | `integer` | data | | Number of columns for the form body | +| `tabPosition` | `'top' \| 'bottom' \| 'left' \| 'right'` | data | | Tab strip position (tabbed forms) | +| `drawerSide` | `'top' \| 'bottom' \| 'left' \| 'right'` | data | | Drawer side (drawer forms) | +| `drawerWidth` | `string` | data | | Drawer width, e.g. "480px" (drawer forms) | +| `modalSize` | `'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'` | data | | Modal size (modal forms) | +| `splitDirection` | `'horizontal' \| 'vertical'` | data | | Split orientation (split forms) | | `sections` | `object[]` | data | | | | `subforms` | `object[]` | data | | Inline master-detail child collections | | `submitBehavior` | `object` | data | | Post-submit behavior | -| `defaultSort` | `object[]` | data | | Default sort order for related list views within this form | ## `` — `list-view`