diff --git a/.changeset/react-blocks-contract.md b/.changeset/react-blocks-contract.md new file mode 100644 index 0000000000..3a26f784fe --- /dev/null +++ b/.changeset/react-blocks-contract.md @@ -0,0 +1,13 @@ +--- +"@objectstack/spec": minor +--- + +Add the react-tier component contract index (`REACT_BLOCKS`, ADR-0081): +`packages/spec/src/ui/react-blocks.ts` maps each curated public block injected +into `kind:'react'` page source to the **spec zod schema** that defines its +declarative config props (FormView, ListView, RecordDetails/Highlights/ +RelatedList/Path, Chart) plus a hand-authored React-interaction overlay +(binding/controlled/callback — objectName, recordId, mode, onSuccess, +onRowClick, …). `pnpm --filter @objectstack/spec gen:react-blocks` generates the +AI-facing contract (skills/objectstack-ui/references/react-blocks.md + .json) +from it — the `data` props come from the spec (single source, no re-authoring). diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 85b7b289b5..a56b445e67 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -3266,6 +3266,10 @@ "PortalUrlNavItemSchema (const)", "PortalViewNavItem (type)", "PortalViewNavItemSchema (const)", + "REACT_BLOCKS (const)", + "ReactBlockDef (interface)", + "ReactInteractionProp (interface)", + "ReactPropKind (type)", "RecordActivityProps (const)", "RecordChatterProps (const)", "RecordDetailsProps (const)", diff --git a/packages/spec/package.json b/packages/spec/package.json index 2d43595d20..a251ee4655 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -195,7 +195,8 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "check:liveness": "tsx scripts/liveness/check-liveness.mts" + "check:liveness": "tsx scripts/liveness/check-liveness.mts", + "gen:react-blocks": "tsx scripts/build-react-blocks-contract.ts" }, "keywords": [ "objectstack", diff --git a/packages/spec/scripts/build-react-blocks-contract.ts b/packages/spec/scripts/build-react-blocks-contract.ts new file mode 100644 index 0000000000..11a9baad05 --- /dev/null +++ b/packages/spec/scripts/build-react-blocks-contract.ts @@ -0,0 +1,140 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Generates the react-tier component contract from packages/spec/src/ui/ +// react-blocks.ts: the `data` (config) props are read from each block's SPEC +// zod schema via z.toJSONSchema (single source — no re-authoring); the +// binding/controlled/callback props come from the hand-authored interaction +// overlay. Emits: +// - skills/objectstack-ui/contracts/react-blocks.contract.json (machine) +// - skills/objectstack-ui/references/react-blocks.md (AI-facing) +// +// Run: pnpm --filter @objectstack/spec gen:react-blocks + +process.env.OS_EAGER_SCHEMAS = '1'; + +import fs from 'fs'; +import path from 'path'; +import { z } from 'zod'; +import { REACT_BLOCKS, type ReactInteractionProp } from '../src/ui/react-blocks'; + +const REPO = path.resolve(__dirname, '../../..'); +const OUT_JSON = path.join(REPO, 'skills/objectstack-ui/contracts/react-blocks.contract.json'); +const OUT_MD = path.join(REPO, 'skills/objectstack-ui/references/react-blocks.md'); + +// ---- JSON-schema prop extraction ----------------------------------------- +function resolveRoot(js: any): any { + // zod v4 may wrap the root in { $ref: '#/$defs/X', $defs: { X: {...} } }. + if (js && js.$ref && js.$defs) { + const key = String(js.$ref).split('/').pop()!; + return js.$defs[key] ?? js; + } + return js; +} + +function renderType(node: any): string { + if (!node || typeof node !== 'object') return 'any'; + if (Array.isArray(node.enum)) return node.enum.map((v: any) => (typeof v === 'string' ? `'${v}'` : String(v))).join(' | '); + if (Array.isArray(node.anyOf) || Array.isArray(node.oneOf)) { + const alts = (node.anyOf ?? node.oneOf).map(renderType).filter((t: string) => t && t !== 'any'); + return [...new Set(alts)].join(' | ') || 'any'; + } + if (node.type === 'array') return `${renderType(node.items)}[]`; + if (node.$ref) return String(node.$ref).split('/').pop() ?? 'object'; + if (node.type) return Array.isArray(node.type) ? node.type.join(' | ') : String(node.type); + if (node.properties) return 'object'; + return 'any'; +} + +const clip = (s: unknown, n = 160): string => { + const t = String(s ?? '').replace(/\s+/g, ' ').trim(); + return t.length > n ? t.slice(0, n - 1) + '…' : t; +}; + +interface Prop { name: string; type: string; kind: string; required: boolean; description: string } + +function dataProps(schema: any, allow?: string[]): Prop[] { + let js: any; + try { + js = resolveRoot(z.toJSONSchema(schema, { unrepresentable: 'any' } as any)); + } catch { + return []; + } + const props = js?.properties ?? {}; + const required: string[] = Array.isArray(js?.required) ? js.required : []; + const SKIP = new Set(['aria', 'type', 'id', 'className', 'style']); + let entries = Object.entries(props).filter(([name]) => !SKIP.has(name)); + if (allow && allow.length) { + const order = new Map(allow.map((n, i) => [n, i])); + entries = entries.filter(([n]) => order.has(n)).sort((a, b) => order.get(a[0])! - order.get(b[0])!); + } + return entries + .map(([name, node]: [string, any]) => ({ + name, + type: renderType(node), + kind: 'data', + required: required.includes(name), + description: clip(node?.description), + })); +} + +function mergeProps(dataPs: Prop[], overlay: ReactInteractionProp[]): Prop[] { + const out: Prop[] = overlay.map((o) => ({ name: o.name, type: o.type, kind: o.kind, required: !!o.required, description: o.description })); + const seen = new Set(out.map((p) => p.name)); + for (const d of dataPs) if (!seen.has(d.name)) out.push(d); + return out; +} + +// ---- build ---------------------------------------------------------------- +const KIND_ORDER: Record = { binding: 0, controlled: 1, callback: 2, data: 3 }; +const blocks = REACT_BLOCKS.map((b) => { + const props = mergeProps(b.schema ? dataProps(b.schema, b.dataProps) : [], b.interactions).sort( + (a, z2) => (KIND_ORDER[a.kind] - KIND_ORDER[z2.kind]) || (Number(z2.required) - Number(a.required)), + ); + return { tag: b.tag, schemaType: b.schemaType, summary: b.summary, specSchema: b.schema ? true : false, props }; +}); + +const contract = { + version: 2, + adr: 'ADR-0081', + source: 'GENERATED from packages/spec/src/ui/react-blocks.ts — data props from the spec zod schemas, binding/controlled/callback from the React overlay.', + note: "Props each component accepts in kind:'react' page source. Reference blocks by their PascalCase tag. kind: data=declarative config (from the spec schema) · binding=connects to data · controlled=React state · callback=React function. Layout = plain HTML+Tailwind; these blocks are for DATA. Live data: const adapter = useAdapter(); adapter.find/findOne/create/update.", + blocks, +}; +fs.writeFileSync(OUT_JSON, JSON.stringify(contract, null, 2) + '\n'); + +// markdown +const esc = (s: string) => String(s).replace(/\|/g, '\\|'); +const L: string[] = []; +L.push('---'); +L.push('title: React-tier component contract'); +L.push("description: Props each injected component accepts in kind:'react' page source (ADR-0081). GENERATED from packages/spec/src/ui/react-blocks.ts — do not edit by hand."); +L.push('---'); +L.push(''); +L.push('{/* GENERATED by packages/spec/scripts/build-react-blocks-contract.ts — do not edit. */}'); +L.push(''); +L.push(`# React-tier component contract (${contract.adr})`); +L.push(''); +L.push(contract.note); +L.push(''); +L.push('**kind**: `data` = declarative config (from the spec schema — the authoritative source) · `binding` = connects the block to data · `controlled` = drive from React state · `callback` = a React function the block calls.'); +L.push(''); +for (const b of blocks) { + L.push(`## \`<${b.tag}>\` — \`${b.schemaType}\`${b.specSchema ? '' : ' *(no spec schema — overlay only)*'}`); + L.push(''); + L.push(b.summary); + L.push(''); + L.push('| prop | type | kind | required | description |'); + L.push('|------|------|------|:--------:|-------------|'); + for (const p of b.props) { + L.push(`| \`${p.name}\` | \`${esc(p.type)}\` | ${p.kind} | ${p.required ? '✓' : ''} | ${esc(p.description)} |`); + } + L.push(''); +} +L.push('## Injected scope (closure variables, reference directly — not props)'); +L.push(''); +L.push('`React` · `useAdapter` · `data` · `variables` · `page`. Kanban/calendar/gantt/timeline/map of an object = `` with the matching visualization, or ``.'); +L.push(''); +fs.writeFileSync(OUT_MD, L.join('\n')); + +console.log(`✅ react-blocks contract: ${blocks.length} blocks → ${path.relative(REPO, OUT_JSON)} + ${path.relative(REPO, OUT_MD)}`); +for (const b of blocks) console.log(` <${b.tag}> ${b.props.length} props`); diff --git a/packages/spec/src/ui/index.ts b/packages/spec/src/ui/index.ts index 2b537db3e2..730fbebbb6 100644 --- a/packages/spec/src/ui/index.ts +++ b/packages/spec/src/ui/index.ts @@ -29,6 +29,7 @@ export * from './action.zod'; export * from './page.zod'; export * from './widget.zod'; export * from './component.zod'; +export * from './react-blocks'; export * from './theme.zod'; export * from './touch.zod'; export * from './offline.zod'; diff --git a/packages/spec/src/ui/react-blocks.ts b/packages/spec/src/ui/react-blocks.ts new file mode 100644 index 0000000000..6d8f6c1827 --- /dev/null +++ b/packages/spec/src/ui/react-blocks.ts @@ -0,0 +1,161 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// React-tier component index (ADR-0081). Maps each curated public block that is +// injected into `kind:'react'` page source to (a) the SPEC zod schema that +// already defines its declarative/config props — the authoritative source, do +// not re-author — and (b) a thin hand-authored React-interaction overlay: the +// binding/controlled/callback props that are inherently React (objectName, +// recordId, mode, onSuccess, onRowClick, …) and so are absent from the +// declarative metadata schema. +// +// The contract the AI authors against (skills/objectstack-ui/references/ +// react-blocks.md + .contract.json) is GENERATED from this index by +// `scripts/build-react-blocks-contract.ts` — never hand-edited. + +import type { ZodTypeAny } from 'zod'; +import { ListViewSchema, FormViewSchema } from './view.zod'; +import { + RecordDetailsProps, + RecordRelatedListProps, + RecordHighlightsProps, + RecordPathProps, +} from './component.zod'; +import { ChartConfigSchema } from './chart.zod'; + +export type ReactPropKind = 'data' | 'binding' | 'controlled' | 'callback'; + +export interface ReactInteractionProp { + name: string; + type: string; + kind: 'binding' | 'controlled' | 'callback'; + required?: boolean; + description: string; +} + +export interface ReactBlockDef { + /** PascalCase name the author writes in JSX, e.g. ``. */ + tag: string; + /** The registry/render type, e.g. `object-form`. */ + schemaType: string; + summary: string; + /** + * Spec zod schema that defines this block's declarative (config) props. The + * generator extracts these as `data` props — authoritative, with descriptions. + * Omit for blocks with no spec schema (then only the overlay is published). + */ + schema?: ZodTypeAny; + /** + * Curate which spec-schema props to surface (high-signal subset; ADR-0080 + * "capability ≠ contract"). Definitions still come from the schema — this only + * selects + orders. Omit to surface all of the schema's props. + */ + dataProps?: string[]; + /** React-only props absent from the declarative schema (hand-authored). */ + interactions: ReactInteractionProp[]; +} + +// Shared overlays ---------------------------------------------------------- +const OBJECT_NAME: ReactInteractionProp = { + name: 'objectName', + type: 'string', + kind: 'binding', + required: true, + description: 'The object this block binds to (server-connected).', +}; + +export const REACT_BLOCKS: ReactBlockDef[] = [ + { + tag: 'ObjectForm', + 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'], + 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.' }, + { name: 'formType', type: "'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'", kind: 'binding', description: 'Form presentation; drawer/modal render the form in a built-in overlay (use drawerSide/drawerWidth/modalSize).' }, + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'Which record to load (edit/view). The hook for master/detail.' }, + { name: 'fields', type: 'string[]', kind: 'binding', description: 'Limit/order the fields shown (defaults to the object form fields).' }, + { name: 'initialValues', type: 'Record', kind: 'binding', description: 'Prefill values in create mode.' }, + { name: 'onSuccess', type: '(record) => void', kind: 'callback', description: 'Called after a successful save with the saved record (e.g. close a panel + reload).' }, + { name: 'onError', type: '(error: Error) => void', kind: 'callback', description: 'Called when the save fails.' }, + { name: 'onCancel', type: '() => void', kind: 'callback', description: 'Called when the user cancels.' }, + { name: 'submitHandler', type: '(values) => any | Promise', kind: 'callback', description: 'Custom persistence instead of the default create/update.' }, + ], + }, + { + tag: 'ListView', + schemaType: 'list-view', + summary: "Server-connected object table with toolbar and switchable visualizations (grid/kanban/calendar/gantt/…). Config props come from the spec ListView schema.", + schema: ListViewSchema, + dataProps: ['columns', 'sort', 'searchableFields', 'userFilters', 'pagination', 'grouping', 'rowHeight', 'selection', 'rowActions', 'inlineEdit'], + interactions: [ + OBJECT_NAME, + { name: 'viewType', type: "'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map'", kind: 'binding', description: 'Which visualization to render (default grid). How you get a kanban/calendar/gantt of the object.' }, + { name: 'filters', type: "FilterArray e.g. ['status','=','active']", kind: 'controlled', description: 'ObjectQL base filter; drive from React state for tabbed/searched lists. ([field, op, value]; ops =, !=, >, <, contains, in; compound: [\"and\", […], […]]).' }, + { name: 'navigation', type: "{ mode: 'page' | 'drawer' | 'modal' | 'split' | 'none' }", kind: 'binding', description: 'What a row click does. Use { mode: \"none\" } when you handle clicks via onRowClick.' }, + { name: 'onRowClick', type: '(record) => void', kind: 'callback', description: "Called with the clicked row's record — the hook for master/detail." }, + { name: 'onNavigate', type: "(recordId, action: 'view' | 'edit') => void", kind: 'callback', description: 'Called for page-level navigation.' }, + ], + }, + { + tag: 'ObjectChart', + schemaType: 'object-chart', + summary: 'Chart over an object’s aggregated data. Config props come from the spec Chart config schema.', + schema: ChartConfigSchema, + dataProps: ['title', 'series', 'xAxis', 'yAxis', 'colors', 'showLegend'], + interactions: [ + OBJECT_NAME, + { name: 'filter', type: 'FilterArray', kind: 'controlled', description: 'ObjectQL filter scoping the data; drive from React state.' }, + { name: 'aggregate', type: '{ field, function, groupBy }', kind: 'binding', description: 'Aggregation: function (sum/avg/count) over field, grouped by groupBy.' }, + ], + }, + { + tag: 'RecordDetails', + schemaType: 'record:details', + summary: 'Field-detail panel for the bound record. Config props from the spec RecordDetails schema.', + schema: RecordDetailsProps, + interactions: [ + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'The record to show.' }, + { name: 'objectName', type: 'string', kind: 'binding', description: 'The record’s object.' }, + ], + }, + { + tag: 'RecordHighlights', + schemaType: 'record:highlights', + summary: 'Highlights panel — a strip of key fields. Config props from the spec RecordHighlights schema.', + schema: RecordHighlightsProps, + interactions: [ + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'The record to summarize.' }, + { name: 'objectName', type: 'string', kind: 'binding', description: 'The record’s object.' }, + ], + }, + { + tag: 'RecordRelatedList', + schemaType: 'record:related_list', + summary: 'Related child records via a lookup. Config props from the spec RecordRelatedList schema.', + schema: RecordRelatedListProps, + interactions: [ + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'The parent record.' }, + { name: 'objectName', type: 'string', kind: 'binding', description: 'The parent object.' }, + ], + }, + { + tag: 'RecordPath', + schemaType: 'record:path', + summary: 'Stage/progress bar driven by a status field. Config props from the spec RecordPath schema.', + schema: RecordPathProps, + interactions: [ + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'The record whose stage to show.' }, + { name: 'objectName', type: 'string', kind: 'binding', description: 'The record’s object.' }, + ], + }, + { + tag: 'Block', + schemaType: '(any)', + summary: 'Escape hatch — render any registered component by type. etc.', + interactions: [ + { name: 'type', type: 'string', kind: 'binding', required: true, description: 'The registered component type to render.' }, + ], + }, +]; diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index f8b71592f9..265923d71b 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -782,8 +782,10 @@ blocks for data. Real component props/callbacks flow through — e.g. ` **[React-tier component contract](./references/react-blocks.md)**, generated from > [`contracts/react-blocks.contract.json`](./contracts/react-blocks.contract.json). > It is the authoritative answer to "what props does ``/``/… -> take?" — author against it, not from memory. Regenerate after editing the JSON -> with `node skills/objectstack-ui/contracts/build-ref.mjs`. +> take?" — author against it, not from memory. The `data` props are sourced from the platform's spec schemas (FormView, +> ListView, RecordDetails, Chart, …) — the same protocol the server validates; +> `binding`/`controlled`/`callback` are the React overlay. Regenerate with +> `pnpm --filter @objectstack/spec gen:react-blocks`. Master/detail (click a row → edit it → save refreshes the list): diff --git a/skills/objectstack-ui/contracts/build-ref.mjs b/skills/objectstack-ui/contracts/build-ref.mjs deleted file mode 100644 index 5b79436668..0000000000 --- a/skills/objectstack-ui/contracts/build-ref.mjs +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env node -// Generates the AI-facing markdown reference from react-blocks.contract.json. -// Run: node skills/objectstack-ui/contracts/build-ref.mjs -import { readFileSync, writeFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; - -const here = dirname(fileURLToPath(import.meta.url)); -const contract = JSON.parse(readFileSync(join(here, 'react-blocks.contract.json'), 'utf8')); -const out = join(here, '..', 'references', 'react-blocks.md'); - -const esc = (s) => String(s).replace(/\|/g, '\\|'); -const KIND_ORDER = { data: 0, controlled: 1, callback: 2, layout: 3 }; - -const lines = []; -lines.push('---'); -lines.push('title: React-tier component contract'); -lines.push('description: The props each injected component accepts in kind:\'react\' page source (ADR-0081). GENERATED from contracts/react-blocks.contract.json — do not edit by hand.'); -lines.push('---'); -lines.push(''); -lines.push('{/* GENERATED by contracts/build-ref.mjs from react-blocks.contract.json — do not edit. */}'); -lines.push(''); -lines.push(`# React-tier component contract (${contract.adr})`); -lines.push(''); -lines.push(contract.note); -lines.push(''); -lines.push('Prop **kind**: `data` = static/declarative (mirrors the registry inputs) · `controlled` = drive from React state · `callback` = a React function the component calls.'); -lines.push(''); - -for (const b of contract.blocks) { - lines.push(`## \`<${b.tag}>\` — \`${b.schemaType}\``); - lines.push(''); - lines.push(b.summary); - lines.push(''); - lines.push('| prop | type | kind | required | description |'); - lines.push('|------|------|------|:--------:|-------------|'); - const props = [...b.props].sort((a, z) => (KIND_ORDER[a.kind] - KIND_ORDER[z.kind])); - for (const p of props) { - lines.push(`| \`${p.name}\` | \`${esc(p.type)}\` | ${p.kind} | ${p.required ? '✓' : ''} | ${esc(p.description)} |`); - } - lines.push(''); -} - -if (contract.alsoAvailable) { - lines.push('## Also available'); - lines.push(''); - lines.push(contract.alsoAvailable.note); - lines.push(''); - lines.push('Injected scope (closure variables, reference directly — not props): ' + contract.alsoAvailable.scope.map((s) => `\`${s}\``).join(', ') + '.'); - lines.push(''); -} - -writeFileSync(out, lines.join('\n')); -console.log('✅ wrote', out, `(${contract.blocks.length} blocks)`); diff --git a/skills/objectstack-ui/contracts/react-blocks.contract.json b/skills/objectstack-ui/contracts/react-blocks.contract.json index d578baea07..b9a5e24139 100644 --- a/skills/objectstack-ui/contracts/react-blocks.contract.json +++ b/skills/objectstack-ui/contracts/react-blocks.contract.json @@ -1,90 +1,527 @@ { - "version": 1, + "version": 2, "adr": "ADR-0081", - "note": "Curated contract for the components injected into kind:'react' page source. Reference these by their PascalCase `tag` in JSX. `data` props mirror the objectui registry `inputs` (the same surface the html tier validates); `controlled` props are React-driven state (recordId, mode, filters…); `callback` props are React functions the component invokes. Layout is plain HTML + Tailwind — these blocks are for DATA. Filters use the ObjectQL array form: [field, op, value], e.g. ['status','=','active'] (ops: =, !=, >, <, contains, in). Live data: `const adapter = useAdapter(); await adapter.find(obj, {$filter, $top}); .findOne; .create; .update`.", + "source": "GENERATED from packages/spec/src/ui/react-blocks.ts — data props from the spec zod schemas, binding/controlled/callback from the React overlay.", + "note": "Props each component accepts in kind:'react' page source. Reference blocks by their PascalCase tag. kind: data=declarative config (from the spec schema) · binding=connects to data · controlled=React state · callback=React function. Layout = plain HTML+Tailwind; these blocks are for DATA. Live data: const adapter = useAdapter(); adapter.find/findOne/create/update.", "blocks": [ { "tag": "ObjectForm", "schemaType": "object-form", - "summary": "Server-connected create / edit / view form for one object. Auto-builds fields from the object schema. Built-in drawer/modal variants via formType.", + "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.", + "specSchema": true, "props": [ - { "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "The object whose record this form edits." }, - { "name": "mode", "type": "'create' | 'edit' | 'view'", "kind": "controlled", "description": "Create a new record, edit/view an existing one." }, - { "name": "recordId", "type": "string | number", "kind": "controlled", "description": "Which record to load (edit/view mode). Drive from React state for master/detail." }, - { "name": "fields", "type": "string[]", "kind": "data", "description": "Limit/order the fields shown (defaults to the object's form fields)." }, - { "name": "initialValues", "type": "Record", "kind": "data", "description": "Prefill values in create mode." }, - { "name": "formType", "type": "'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'", "kind": "data", "description": "Form presentation; 'drawer'/'modal' render the form in a built-in overlay (use drawerSide/drawerWidth/modalSize)." }, - { "name": "onSuccess", "type": "(record) => void", "kind": "callback", "description": "Called after a successful create/update with the saved record. Typical: close a panel + bump a reload key." }, - { "name": "onError", "type": "(error: Error) => void", "kind": "callback", "description": "Called when the save fails." }, - { "name": "onCancel", "type": "() => void", "kind": "callback", "description": "Called when the user cancels." }, - { "name": "submitHandler", "type": "(values) => any | Promise", "kind": "callback", "description": "Custom persistence instead of the default create/update (the form just validates and hands over values)." }, - { "name": "navigateOnSuccess", "type": "string", "kind": "data", "description": "URL to navigate to after save (supports {id})." } + { + "name": "objectName", + "type": "string", + "kind": "binding", + "required": true, + "description": "The object this block binds to (server-connected)." + }, + { + "name": "formType", + "type": "'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'", + "kind": "binding", + "required": false, + "description": "Form presentation; drawer/modal render the form in a built-in overlay (use drawerSide/drawerWidth/modalSize)." + }, + { + "name": "fields", + "type": "string[]", + "kind": "binding", + "required": false, + "description": "Limit/order the fields shown (defaults to the object form fields)." + }, + { + "name": "initialValues", + "type": "Record", + "kind": "binding", + "required": false, + "description": "Prefill values in create mode." + }, + { + "name": "mode", + "type": "'create' | 'edit' | 'view'", + "kind": "controlled", + "required": false, + "description": "Create a new record, or edit/view an existing one — drive from React state." + }, + { + "name": "recordId", + "type": "string | number", + "kind": "controlled", + "required": false, + "description": "Which record to load (edit/view). The hook for master/detail." + }, + { + "name": "onSuccess", + "type": "(record) => void", + "kind": "callback", + "required": false, + "description": "Called after a successful save with the saved record (e.g. close a panel + reload)." + }, + { + "name": "onError", + "type": "(error: Error) => void", + "kind": "callback", + "required": false, + "description": "Called when the save fails." + }, + { + "name": "onCancel", + "type": "() => void", + "kind": "callback", + "required": false, + "description": "Called when the user cancels." + }, + { + "name": "submitHandler", + "type": "(values) => any | Promise", + "kind": "callback", + "required": false, + "description": "Custom persistence instead of the default create/update." + }, + { + "name": "sections", + "type": "object[]", + "kind": "data", + "required": false, + "description": "" + }, + { + "name": "subforms", + "type": "object[]", + "kind": "data", + "required": false, + "description": "Inline master-detail child collections" + }, + { + "name": "submitBehavior", + "type": "object", + "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" + } ] }, { "tag": "ListView", "schemaType": "list-view", - "summary": "Server-connected object table with toolbar (search/filter/sort), and switchable views (grid/kanban/gallery/calendar/timeline/gantt/map). The primary object-list block.", + "summary": "Server-connected object table with toolbar and switchable visualizations (grid/kanban/calendar/gantt/…). Config props come from the spec ListView schema.", + "specSchema": true, "props": [ - { "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "The object to list." }, - { "name": "fields", "type": "string[]", "kind": "data", "description": "Columns to show." }, - { "name": "filters", "type": "FilterArray", "kind": "controlled", "description": "ObjectQL filter, e.g. ['status','=','active'] or ['and', [...], [...]]. Drive from React state for tabbed/searched lists." }, - { "name": "sort", "type": "SortArray", "kind": "data", "description": "Sort spec." }, - { "name": "viewType", "type": "'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map'", "kind": "data", "description": "Which view to render (default grid). This is how you get a kanban/calendar/gantt of the object." }, - { "name": "navigation", "type": "{ mode: 'page' | 'drawer' | 'modal' | 'split' | 'none' }", "kind": "data", "description": "What a row click does. Set { mode: 'none' } when you handle clicks yourself via onRowClick." }, - { "name": "onRowClick", "type": "(record) => void", "kind": "callback", "description": "Called with the clicked row's record. The hook for master/detail." }, - { "name": "onNavigate", "type": "(recordId, action) => void", "kind": "callback", "description": "Called for page-level navigation (action: 'view' | 'edit')." }, - { "name": "searchableFields", "type": "string[]", "kind": "data", "description": "Fields the toolbar search matches." } + { + "name": "objectName", + "type": "string", + "kind": "binding", + "required": true, + "description": "The object this block binds to (server-connected)." + }, + { + "name": "viewType", + "type": "'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map'", + "kind": "binding", + "required": false, + "description": "Which visualization to render (default grid). How you get a kanban/calendar/gantt of the object." + }, + { + "name": "navigation", + "type": "{ mode: 'page' | 'drawer' | 'modal' | 'split' | 'none' }", + "kind": "binding", + "required": false, + "description": "What a row click does. Use { mode: \"none\" } when you handle clicks via onRowClick." + }, + { + "name": "filters", + "type": "FilterArray e.g. ['status','=','active']", + "kind": "controlled", + "required": false, + "description": "ObjectQL base filter; drive from React state for tabbed/searched lists. ([field, op, value]; ops =, !=, >, <, contains, in; compound: [\"and\", […], […]])." + }, + { + "name": "onRowClick", + "type": "(record) => void", + "kind": "callback", + "required": false, + "description": "Called with the clicked row's record — the hook for master/detail." + }, + { + "name": "onNavigate", + "type": "(recordId, action: 'view' | 'edit') => void", + "kind": "callback", + "required": false, + "description": "Called for page-level navigation." + }, + { + "name": "columns", + "type": "string[] | object[]", + "kind": "data", + "required": true, + "description": "Fields to display as columns" + }, + { + "name": "sort", + "type": "string | object[]", + "kind": "data", + "required": false, + "description": "" + }, + { + "name": "searchableFields", + "type": "string[]", + "kind": "data", + "required": false, + "description": "Fields enabled for search" + }, + { + "name": "userFilters", + "type": "object", + "kind": "data", + "required": false, + "description": "End-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields" + }, + { + "name": "pagination", + "type": "object", + "kind": "data", + "required": false, + "description": "Pagination configuration" + }, + { + "name": "grouping", + "type": "object", + "kind": "data", + "required": false, + "description": "Group records by one or more fields" + }, + { + "name": "rowHeight", + "type": "'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'", + "kind": "data", + "required": false, + "description": "Row height / density setting" + }, + { + "name": "selection", + "type": "object", + "kind": "data", + "required": false, + "description": "Row selection configuration" + }, + { + "name": "rowActions", + "type": "string[]", + "kind": "data", + "required": false, + "description": "Actions available for individual row items" + }, + { + "name": "inlineEdit", + "type": "boolean", + "kind": "data", + "required": false, + "description": "Allow inline editing of records directly in the list view" + } ] }, { - "tag": "ObjectGrid", - "schemaType": "object-grid", - "summary": "Lower-level data grid for an object (columns + filters). Prefer ListView for a full toolbar; use ObjectGrid for an embedded table.", + "tag": "ObjectChart", + "schemaType": "object-chart", + "summary": "Chart over an object’s aggregated data. Config props come from the spec Chart config schema.", + "specSchema": true, "props": [ - { "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "The object to render." }, - { "name": "columns", "type": "ColumnDef[]", "kind": "data", "description": "Column definitions." }, - { "name": "filters", "type": "FilterArray", "kind": "controlled", "description": "ObjectQL filter." } + { + "name": "objectName", + "type": "string", + "kind": "binding", + "required": true, + "description": "The object this block binds to (server-connected)." + }, + { + "name": "aggregate", + "type": "{ field, function, groupBy }", + "kind": "binding", + "required": false, + "description": "Aggregation: function (sum/avg/count) over field, grouped by groupBy." + }, + { + "name": "filter", + "type": "FilterArray", + "kind": "controlled", + "required": false, + "description": "ObjectQL filter scoping the data; drive from React state." + }, + { + "name": "showLegend", + "type": "boolean", + "kind": "data", + "required": true, + "description": "Display legend" + }, + { + "name": "title", + "type": "string", + "kind": "data", + "required": false, + "description": "Chart title" + }, + { + "name": "series", + "type": "object[]", + "kind": "data", + "required": false, + "description": "Defined series configuration" + }, + { + "name": "xAxis", + "type": "object", + "kind": "data", + "required": false, + "description": "X-Axis configuration" + }, + { + "name": "yAxis", + "type": "object[]", + "kind": "data", + "required": false, + "description": "Y-Axis configuration (support dual axis)" + }, + { + "name": "colors", + "type": "string[] | object", + "kind": "data", + "required": false, + "description": "Color palette (string[]) or value→color map ({ value: color })" + } ] }, { - "tag": "ObjectMasterDetailForm", - "schemaType": "object-master-detail-form", - "summary": "Parent record + child line-items, edited and saved atomically (e.g. Invoice + line items).", + "tag": "RecordDetails", + "schemaType": "record:details", + "summary": "Field-detail panel for the bound record. Config props from the spec RecordDetails schema.", + "specSchema": true, "props": [ - { "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "Parent object." }, - { "name": "mode", "type": "'create' | 'edit'", "kind": "controlled", "description": "Create or edit the parent + children." }, - { "name": "recordId", "type": "string | number", "kind": "controlled", "description": "Parent record to edit." }, - { "name": "childObject", "type": "string", "kind": "data", "required": true, "description": "Child (line-item) object." }, - { "name": "relationshipField", "type": "string", "kind": "data", "required": true, "description": "Field on the child that points to the parent." }, - { "name": "columns", "type": "ColumnDef[]", "kind": "data", "required": true, "description": "Child line-item grid columns." }, - { "name": "totalField", "type": "string", "kind": "data", "description": "Parent field that rolls up the child amounts." }, - { "name": "onSuccess", "type": "(record) => void", "kind": "callback", "description": "After the atomic save." }, - { "name": "onCancel", "type": "() => void", "kind": "callback", "description": "On cancel." } + { + "name": "objectName", + "type": "string", + "kind": "binding", + "required": false, + "description": "The record’s object." + }, + { + "name": "recordId", + "type": "string | number", + "kind": "controlled", + "required": false, + "description": "The record to show." + }, + { + "name": "columns", + "type": "'1' | '2' | '3' | '4'", + "kind": "data", + "required": true, + "description": "Number of columns for field layout (1-4)" + }, + { + "name": "layout", + "type": "'auto' | 'custom'", + "kind": "data", + "required": true, + "description": "Layout mode: auto uses object compactLayout, custom uses explicit sections" + }, + { + "name": "sections", + "type": "string[]", + "kind": "data", + "required": false, + "description": "Section IDs to show (required when layout is \"custom\")" + }, + { + "name": "fields", + "type": "string[]", + "kind": "data", + "required": false, + "description": "Explicit field list to display (optional, overrides compactLayout)" + } ] }, { - "tag": "ObjectChart", - "schemaType": "object-chart", - "summary": "Chart over an object's aggregated data (bar/line/area/pie/donut/…).", + "tag": "RecordHighlights", + "schemaType": "record:highlights", + "summary": "Highlights panel — a strip of key fields. Config props from the spec RecordHighlights schema.", + "specSchema": true, + "props": [ + { + "name": "objectName", + "type": "string", + "kind": "binding", + "required": false, + "description": "The record’s object." + }, + { + "name": "recordId", + "type": "string | number", + "kind": "controlled", + "required": false, + "description": "The record to summarize." + }, + { + "name": "fields", + "type": "string | object[]", + "kind": "data", + "required": true, + "description": "Key fields to highlight (1-7 fields max, typically displayed as prominent cards). Each item may be a bare field name or {name, label?, icon?, type?} for inline…" + }, + { + "name": "layout", + "type": "'horizontal' | 'vertical'", + "kind": "data", + "required": true, + "description": "Layout orientation for highlight fields" + } + ] + }, + { + "tag": "RecordRelatedList", + "schemaType": "record:related_list", + "summary": "Related child records via a lookup. Config props from the spec RecordRelatedList schema.", + "specSchema": true, + "props": [ + { + "name": "objectName", + "type": "string", + "kind": "binding", + "required": false, + "description": "The parent object." + }, + { + "name": "recordId", + "type": "string | number", + "kind": "controlled", + "required": false, + "description": "The parent record." + }, + { + "name": "relationshipField", + "type": "string", + "kind": "data", + "required": true, + "description": "Field on related object that points to this record (e.g., \"account_id\")" + }, + { + "name": "columns", + "type": "string[]", + "kind": "data", + "required": true, + "description": "Fields to display in the related list" + }, + { + "name": "limit", + "type": "integer", + "kind": "data", + "required": true, + "description": "Number of records to display initially" + }, + { + "name": "showViewAll", + "type": "boolean", + "kind": "data", + "required": true, + "description": "Show \"View All\" link to see all related records" + }, + { + "name": "sort", + "type": "string | object[]", + "kind": "data", + "required": false, + "description": "Sort order for related records" + }, + { + "name": "filter", + "type": "object[]", + "kind": "data", + "required": false, + "description": "Additional filter criteria for related records" + }, + { + "name": "title", + "type": "string", + "kind": "data", + "required": false, + "description": "Custom title for the related list" + }, + { + "name": "actions", + "type": "string[]", + "kind": "data", + "required": false, + "description": "Action IDs available for related records" + }, + { + "name": "add", + "type": "object", + "kind": "data", + "required": false, + "description": "Add-existing-via-picker config (generic m2m/junction assignment)." + } + ] + }, + { + "tag": "RecordPath", + "schemaType": "record:path", + "summary": "Stage/progress bar driven by a status field. Config props from the spec RecordPath schema.", + "specSchema": true, "props": [ - { "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "The object to aggregate." }, - { "name": "chartType", "type": "'bar' | 'line' | 'area' | 'pie' | 'donut' | 'scatter' | 'radar' | '…'", "kind": "data", "description": "Chart variant." }, - { "name": "aggregate", "type": "{ field, function, groupBy }", "kind": "data", "description": "Aggregation: function (sum/avg/count) over field, grouped by groupBy." }, - { "name": "filter", "type": "FilterArray", "kind": "controlled", "description": "ObjectQL filter to scope the data." } + { + "name": "objectName", + "type": "string", + "kind": "binding", + "required": false, + "description": "The record’s object." + }, + { + "name": "recordId", + "type": "string | number", + "kind": "controlled", + "required": false, + "description": "The record whose stage to show." + }, + { + "name": "statusField", + "type": "string", + "kind": "data", + "required": true, + "description": "Field name representing the current status/stage" + }, + { + "name": "stages", + "type": "object[]", + "kind": "data", + "required": false, + "description": "Explicit stage definitions (if not using field metadata)" + } ] }, { "tag": "Block", "schemaType": "(any)", - "summary": "Escape hatch — render any registered component by type when it isn't injected as a named block.", + "summary": "Escape hatch — render any registered component by type. etc.", + "specSchema": false, "props": [ - { "name": "type", "type": "string", "kind": "data", "required": true, "description": "The registered component type, e.g. 'object-kanban'." } + { + "name": "type", + "type": "string", + "kind": "binding", + "required": true, + "description": "The registered component type to render." + } ] } - ], - "alsoAvailable": { - "note": "Other curated public blocks are reachable too. Kanban/calendar/gantt/timeline/map of an object = . Metrics/dashboards/pivots and record:* panels can be rendered via / until they are added to this contract.", - "scope": ["React", "useAdapter", "data", "variables", "page"] - } + ] } diff --git a/skills/objectstack-ui/references/react-blocks.md b/skills/objectstack-ui/references/react-blocks.md index 72ffd32ec4..a08b9ef57d 100644 --- a/skills/objectstack-ui/references/react-blocks.md +++ b/skills/objectstack-ui/references/react-blocks.md @@ -1,97 +1,137 @@ --- title: React-tier component contract -description: The props each injected component accepts in kind:'react' page source (ADR-0081). GENERATED from contracts/react-blocks.contract.json — do not edit by hand. +description: Props each injected component accepts in kind:'react' page source (ADR-0081). GENERATED from packages/spec/src/ui/react-blocks.ts — do not edit by hand. --- -{/* GENERATED by contracts/build-ref.mjs from react-blocks.contract.json — do not edit. */} +{/* GENERATED by packages/spec/scripts/build-react-blocks-contract.ts — do not edit. */} # React-tier component contract (ADR-0081) -Curated contract for the components injected into kind:'react' page source. Reference these by their PascalCase `tag` in JSX. `data` props mirror the objectui registry `inputs` (the same surface the html tier validates); `controlled` props are React-driven state (recordId, mode, filters…); `callback` props are React functions the component invokes. Layout is plain HTML + Tailwind — these blocks are for DATA. Filters use the ObjectQL array form: [field, op, value], e.g. ['status','=','active'] (ops: =, !=, >, <, contains, in). Live data: `const adapter = useAdapter(); await adapter.find(obj, {$filter, $top}); .findOne; .create; .update`. +Props each component accepts in kind:'react' page source. Reference blocks by their PascalCase tag. kind: data=declarative config (from the spec schema) · binding=connects to data · controlled=React state · callback=React function. Layout = plain HTML+Tailwind; these blocks are for DATA. Live data: const adapter = useAdapter(); adapter.find/findOne/create/update. -Prop **kind**: `data` = static/declarative (mirrors the registry inputs) · `controlled` = drive from React state · `callback` = a React function the component calls. +**kind**: `data` = declarative config (from the spec schema — the authoritative source) · `binding` = connects the block to data · `controlled` = drive from React state · `callback` = a React function the block calls. ## `` — `object-form` -Server-connected create / edit / view form for one object. Auto-builds fields from the object schema. Built-in drawer/modal variants via formType. +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. | prop | type | kind | required | description | |------|------|------|:--------:|-------------| -| `objectName` | `string` | data | ✓ | The object whose record this form edits. | -| `fields` | `string[]` | data | | Limit/order the fields shown (defaults to the object's form fields). | -| `initialValues` | `Record` | data | | Prefill values in create mode. | -| `formType` | `'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'` | data | | Form presentation; 'drawer'/'modal' render the form in a built-in overlay (use drawerSide/drawerWidth/modalSize). | -| `navigateOnSuccess` | `string` | data | | URL to navigate to after save (supports {id}). | -| `mode` | `'create' \| 'edit' \| 'view'` | controlled | | Create a new record, edit/view an existing one. | -| `recordId` | `string \| number` | controlled | | Which record to load (edit/view mode). Drive from React state for master/detail. | -| `onSuccess` | `(record) => void` | callback | | Called after a successful create/update with the saved record. Typical: close a panel + bump a reload key. | +| `objectName` | `string` | binding | ✓ | The object this block binds to (server-connected). | +| `formType` | `'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'` | binding | | Form presentation; drawer/modal render the form in a built-in overlay (use drawerSide/drawerWidth/modalSize). | +| `fields` | `string[]` | binding | | Limit/order the fields shown (defaults to the object form fields). | +| `initialValues` | `Record` | binding | | Prefill values in create mode. | +| `mode` | `'create' \| 'edit' \| 'view'` | controlled | | Create a new record, or edit/view an existing one — drive from React state. | +| `recordId` | `string \| number` | controlled | | Which record to load (edit/view). The hook for master/detail. | +| `onSuccess` | `(record) => void` | callback | | Called after a successful save with the saved record (e.g. close a panel + reload). | | `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 (the form just validates and hands over values). | +| `submitHandler` | `(values) => any \| Promise` | callback | | Custom persistence instead of the default create/update. | +| `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` -Server-connected object table with toolbar (search/filter/sort), and switchable views (grid/kanban/gallery/calendar/timeline/gantt/map). The primary object-list block. +Server-connected object table with toolbar and switchable visualizations (grid/kanban/calendar/gantt/…). Config props come from the spec ListView schema. | prop | type | kind | required | description | |------|------|------|:--------:|-------------| -| `objectName` | `string` | data | ✓ | The object to list. | -| `fields` | `string[]` | data | | Columns to show. | -| `sort` | `SortArray` | data | | Sort spec. | -| `viewType` | `'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map'` | data | | Which view to render (default grid). This is how you get a kanban/calendar/gantt of the object. | -| `navigation` | `{ mode: 'page' \| 'drawer' \| 'modal' \| 'split' \| 'none' }` | data | | What a row click does. Set { mode: 'none' } when you handle clicks yourself via onRowClick. | -| `searchableFields` | `string[]` | data | | Fields the toolbar search matches. | -| `filters` | `FilterArray` | controlled | | ObjectQL filter, e.g. ['status','=','active'] or ['and', [...], [...]]. Drive from React state for tabbed/searched lists. | -| `onRowClick` | `(record) => void` | callback | | Called with the clicked row's record. The hook for master/detail. | -| `onNavigate` | `(recordId, action) => void` | callback | | Called for page-level navigation (action: 'view' \| 'edit'). | +| `objectName` | `string` | binding | ✓ | The object this block binds to (server-connected). | +| `viewType` | `'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map'` | binding | | Which visualization to render (default grid). How you get a kanban/calendar/gantt of the object. | +| `navigation` | `{ mode: 'page' \| 'drawer' \| 'modal' \| 'split' \| 'none' }` | binding | | What a row click does. Use { mode: "none" } when you handle clicks via onRowClick. | +| `filters` | `FilterArray e.g. ['status','=','active']` | controlled | | ObjectQL base filter; drive from React state for tabbed/searched lists. ([field, op, value]; ops =, !=, >, <, contains, in; compound: ["and", […], […]]). | +| `onRowClick` | `(record) => void` | callback | | Called with the clicked row's record — the hook for master/detail. | +| `onNavigate` | `(recordId, action: 'view' \| 'edit') => void` | callback | | Called for page-level navigation. | +| `columns` | `string[] \| object[]` | data | ✓ | Fields to display as columns | +| `sort` | `string \| object[]` | data | | | +| `searchableFields` | `string[]` | data | | Fields enabled for search | +| `userFilters` | `object` | data | | End-user quick-filter bar: dropdown/toggle fields or tab presets. Omit to let the renderer derive filters from select/boolean fields | +| `pagination` | `object` | data | | Pagination configuration | +| `grouping` | `object` | data | | Group records by one or more fields | +| `rowHeight` | `'compact' \| 'short' \| 'medium' \| 'tall' \| 'extra_tall'` | data | | Row height / density setting | +| `selection` | `object` | data | | Row selection configuration | +| `rowActions` | `string[]` | data | | Actions available for individual row items | +| `inlineEdit` | `boolean` | data | | Allow inline editing of records directly in the list view | -## `` — `object-grid` +## `` — `object-chart` -Lower-level data grid for an object (columns + filters). Prefer ListView for a full toolbar; use ObjectGrid for an embedded table. +Chart over an object’s aggregated data. Config props come from the spec Chart config schema. | prop | type | kind | required | description | |------|------|------|:--------:|-------------| -| `objectName` | `string` | data | ✓ | The object to render. | -| `columns` | `ColumnDef[]` | data | | Column definitions. | -| `filters` | `FilterArray` | controlled | | ObjectQL filter. | +| `objectName` | `string` | binding | ✓ | The object this block binds to (server-connected). | +| `aggregate` | `{ field, function, groupBy }` | binding | | Aggregation: function (sum/avg/count) over field, grouped by groupBy. | +| `filter` | `FilterArray` | controlled | | ObjectQL filter scoping the data; drive from React state. | +| `showLegend` | `boolean` | data | ✓ | Display legend | +| `title` | `string` | data | | Chart title | +| `series` | `object[]` | data | | Defined series configuration | +| `xAxis` | `object` | data | | X-Axis configuration | +| `yAxis` | `object[]` | data | | Y-Axis configuration (support dual axis) | +| `colors` | `string[] \| object` | data | | Color palette (string[]) or value→color map ({ value: color }) | -## `` — `object-master-detail-form` +## `` — `record:details` -Parent record + child line-items, edited and saved atomically (e.g. Invoice + line items). +Field-detail panel for the bound record. Config props from the spec RecordDetails schema. | prop | type | kind | required | description | |------|------|------|:--------:|-------------| -| `objectName` | `string` | data | ✓ | Parent object. | -| `childObject` | `string` | data | ✓ | Child (line-item) object. | -| `relationshipField` | `string` | data | ✓ | Field on the child that points to the parent. | -| `columns` | `ColumnDef[]` | data | ✓ | Child line-item grid columns. | -| `totalField` | `string` | data | | Parent field that rolls up the child amounts. | -| `mode` | `'create' \| 'edit'` | controlled | | Create or edit the parent + children. | -| `recordId` | `string \| number` | controlled | | Parent record to edit. | -| `onSuccess` | `(record) => void` | callback | | After the atomic save. | -| `onCancel` | `() => void` | callback | | On cancel. | +| `objectName` | `string` | binding | | The record’s object. | +| `recordId` | `string \| number` | controlled | | The record to show. | +| `columns` | `'1' \| '2' \| '3' \| '4'` | data | ✓ | Number of columns for field layout (1-4) | +| `layout` | `'auto' \| 'custom'` | data | ✓ | Layout mode: auto uses object compactLayout, custom uses explicit sections | +| `sections` | `string[]` | data | | Section IDs to show (required when layout is "custom") | +| `fields` | `string[]` | data | | Explicit field list to display (optional, overrides compactLayout) | -## `` — `object-chart` +## `` — `record:highlights` -Chart over an object's aggregated data (bar/line/area/pie/donut/…). +Highlights panel — a strip of key fields. Config props from the spec RecordHighlights schema. | prop | type | kind | required | description | |------|------|------|:--------:|-------------| -| `objectName` | `string` | data | ✓ | The object to aggregate. | -| `chartType` | `'bar' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'scatter' \| 'radar' \| '…'` | data | | Chart variant. | -| `aggregate` | `{ field, function, groupBy }` | data | | Aggregation: function (sum/avg/count) over field, grouped by groupBy. | -| `filter` | `FilterArray` | controlled | | ObjectQL filter to scope the data. | +| `objectName` | `string` | binding | | The record’s object. | +| `recordId` | `string \| number` | controlled | | The record to summarize. | +| `fields` | `string \| object[]` | data | ✓ | Key fields to highlight (1-7 fields max, typically displayed as prominent cards). Each item may be a bare field name or {name, label?, icon?, type?} for inline… | +| `layout` | `'horizontal' \| 'vertical'` | data | ✓ | Layout orientation for highlight fields | + +## `` — `record:related_list` -## `` — `(any)` +Related child records via a lookup. Config props from the spec RecordRelatedList schema. -Escape hatch — render any registered component by type when it isn't injected as a named block. +| prop | type | kind | required | description | +|------|------|------|:--------:|-------------| +| `objectName` | `string` | binding | | The parent object. | +| `recordId` | `string \| number` | controlled | | The parent record. | +| `relationshipField` | `string` | data | ✓ | Field on related object that points to this record (e.g., "account_id") | +| `columns` | `string[]` | data | ✓ | Fields to display in the related list | +| `limit` | `integer` | data | ✓ | Number of records to display initially | +| `showViewAll` | `boolean` | data | ✓ | Show "View All" link to see all related records | +| `sort` | `string \| object[]` | data | | Sort order for related records | +| `filter` | `object[]` | data | | Additional filter criteria for related records | +| `title` | `string` | data | | Custom title for the related list | +| `actions` | `string[]` | data | | Action IDs available for related records | +| `add` | `object` | data | | Add-existing-via-picker config (generic m2m/junction assignment). | + +## `` — `record:path` + +Stage/progress bar driven by a status field. Config props from the spec RecordPath schema. | prop | type | kind | required | description | |------|------|------|:--------:|-------------| -| `type` | `string` | data | ✓ | The registered component type, e.g. 'object-kanban'. | +| `objectName` | `string` | binding | | The record’s object. | +| `recordId` | `string \| number` | controlled | | The record whose stage to show. | +| `statusField` | `string` | data | ✓ | Field name representing the current status/stage | +| `stages` | `object[]` | data | | Explicit stage definitions (if not using field metadata) | -## Also available +## `` — `(any)` *(no spec schema — overlay only)* + +Escape hatch — render any registered component by type. etc. + +| prop | type | kind | required | description | +|------|------|------|:--------:|-------------| +| `type` | `string` | binding | ✓ | The registered component type to render. | -Other curated public blocks are reachable too. Kanban/calendar/gantt/timeline/map of an object = . Metrics/dashboards/pivots and record:* panels can be rendered via / until they are added to this contract. +## Injected scope (closure variables, reference directly — not props) -Injected scope (closure variables, reference directly — not props): `React`, `useAdapter`, `data`, `variables`, `page`. +`React` · `useAdapter` · `data` · `variables` · `page`. Kanban/calendar/gantt/timeline/map of an object = `` with the matching visualization, or ``.