|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | +// |
| 3 | +// React-tier component index (ADR-0081). Maps each curated public block that is |
| 4 | +// injected into `kind:'react'` page source to (a) the SPEC zod schema that |
| 5 | +// already defines its declarative/config props — the authoritative source, do |
| 6 | +// not re-author — and (b) a thin hand-authored React-interaction overlay: the |
| 7 | +// binding/controlled/callback props that are inherently React (objectName, |
| 8 | +// recordId, mode, onSuccess, onRowClick, …) and so are absent from the |
| 9 | +// declarative metadata schema. |
| 10 | +// |
| 11 | +// The contract the AI authors against (skills/objectstack-ui/references/ |
| 12 | +// react-blocks.md + .contract.json) is GENERATED from this index by |
| 13 | +// `scripts/build-react-blocks-contract.ts` — never hand-edited. |
| 14 | + |
| 15 | +import type { ZodTypeAny } from 'zod'; |
| 16 | +import { ListViewSchema, FormViewSchema } from './view.zod'; |
| 17 | +import { |
| 18 | + RecordDetailsProps, |
| 19 | + RecordRelatedListProps, |
| 20 | + RecordHighlightsProps, |
| 21 | + RecordPathProps, |
| 22 | +} from './component.zod'; |
| 23 | +import { ChartConfigSchema } from './chart.zod'; |
| 24 | + |
| 25 | +export type ReactPropKind = 'data' | 'binding' | 'controlled' | 'callback'; |
| 26 | + |
| 27 | +export interface ReactInteractionProp { |
| 28 | + name: string; |
| 29 | + type: string; |
| 30 | + kind: 'binding' | 'controlled' | 'callback'; |
| 31 | + required?: boolean; |
| 32 | + description: string; |
| 33 | +} |
| 34 | + |
| 35 | +export interface ReactBlockDef { |
| 36 | + /** PascalCase name the author writes in JSX, e.g. `<ObjectForm>`. */ |
| 37 | + tag: string; |
| 38 | + /** The registry/render type, e.g. `object-form`. */ |
| 39 | + schemaType: string; |
| 40 | + summary: string; |
| 41 | + /** |
| 42 | + * Spec zod schema that defines this block's declarative (config) props. The |
| 43 | + * generator extracts these as `data` props — authoritative, with descriptions. |
| 44 | + * Omit for blocks with no spec schema (then only the overlay is published). |
| 45 | + */ |
| 46 | + schema?: ZodTypeAny; |
| 47 | + /** |
| 48 | + * Curate which spec-schema props to surface (high-signal subset; ADR-0080 |
| 49 | + * "capability ≠ contract"). Definitions still come from the schema — this only |
| 50 | + * selects + orders. Omit to surface all of the schema's props. |
| 51 | + */ |
| 52 | + dataProps?: string[]; |
| 53 | + /** React-only props absent from the declarative schema (hand-authored). */ |
| 54 | + interactions: ReactInteractionProp[]; |
| 55 | +} |
| 56 | + |
| 57 | +// Shared overlays ---------------------------------------------------------- |
| 58 | +const OBJECT_NAME: ReactInteractionProp = { |
| 59 | + name: 'objectName', |
| 60 | + type: 'string', |
| 61 | + kind: 'binding', |
| 62 | + required: true, |
| 63 | + description: 'The object this block binds to (server-connected).', |
| 64 | +}; |
| 65 | + |
| 66 | +export const REACT_BLOCKS: ReactBlockDef[] = [ |
| 67 | + { |
| 68 | + tag: 'ObjectForm', |
| 69 | + schemaType: 'object-form', |
| 70 | + 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.", |
| 71 | + schema: FormViewSchema, |
| 72 | + dataProps: ['sections', 'subforms', 'submitBehavior', 'defaultSort'], |
| 73 | + interactions: [ |
| 74 | + OBJECT_NAME, |
| 75 | + { name: 'mode', type: "'create' | 'edit' | 'view'", kind: 'controlled', description: 'Create a new record, or edit/view an existing one — drive from React state.' }, |
| 76 | + { 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).' }, |
| 77 | + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'Which record to load (edit/view). The hook for master/detail.' }, |
| 78 | + { name: 'fields', type: 'string[]', kind: 'binding', description: 'Limit/order the fields shown (defaults to the object form fields).' }, |
| 79 | + { name: 'initialValues', type: 'Record<string, any>', kind: 'binding', description: 'Prefill values in create mode.' }, |
| 80 | + { name: 'onSuccess', type: '(record) => void', kind: 'callback', description: 'Called after a successful save with the saved record (e.g. close a panel + reload).' }, |
| 81 | + { name: 'onError', type: '(error: Error) => void', kind: 'callback', description: 'Called when the save fails.' }, |
| 82 | + { name: 'onCancel', type: '() => void', kind: 'callback', description: 'Called when the user cancels.' }, |
| 83 | + { name: 'submitHandler', type: '(values) => any | Promise<any>', kind: 'callback', description: 'Custom persistence instead of the default create/update.' }, |
| 84 | + ], |
| 85 | + }, |
| 86 | + { |
| 87 | + tag: 'ListView', |
| 88 | + schemaType: 'list-view', |
| 89 | + summary: "Server-connected object table with toolbar and switchable visualizations (grid/kanban/calendar/gantt/…). Config props come from the spec ListView schema.", |
| 90 | + schema: ListViewSchema, |
| 91 | + dataProps: ['columns', 'sort', 'searchableFields', 'userFilters', 'pagination', 'grouping', 'rowHeight', 'selection', 'rowActions', 'inlineEdit'], |
| 92 | + interactions: [ |
| 93 | + OBJECT_NAME, |
| 94 | + { 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.' }, |
| 95 | + { 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\", […], […]]).' }, |
| 96 | + { 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.' }, |
| 97 | + { name: 'onRowClick', type: '(record) => void', kind: 'callback', description: "Called with the clicked row's record — the hook for master/detail." }, |
| 98 | + { name: 'onNavigate', type: "(recordId, action: 'view' | 'edit') => void", kind: 'callback', description: 'Called for page-level navigation.' }, |
| 99 | + ], |
| 100 | + }, |
| 101 | + { |
| 102 | + tag: 'ObjectChart', |
| 103 | + schemaType: 'object-chart', |
| 104 | + summary: 'Chart over an object’s aggregated data. Config props come from the spec Chart config schema.', |
| 105 | + schema: ChartConfigSchema, |
| 106 | + dataProps: ['title', 'series', 'xAxis', 'yAxis', 'colors', 'showLegend'], |
| 107 | + interactions: [ |
| 108 | + OBJECT_NAME, |
| 109 | + { name: 'filter', type: 'FilterArray', kind: 'controlled', description: 'ObjectQL filter scoping the data; drive from React state.' }, |
| 110 | + { name: 'aggregate', type: '{ field, function, groupBy }', kind: 'binding', description: 'Aggregation: function (sum/avg/count) over field, grouped by groupBy.' }, |
| 111 | + ], |
| 112 | + }, |
| 113 | + { |
| 114 | + tag: 'RecordDetails', |
| 115 | + schemaType: 'record:details', |
| 116 | + summary: 'Field-detail panel for the bound record. Config props from the spec RecordDetails schema.', |
| 117 | + schema: RecordDetailsProps, |
| 118 | + interactions: [ |
| 119 | + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'The record to show.' }, |
| 120 | + { name: 'objectName', type: 'string', kind: 'binding', description: 'The record’s object.' }, |
| 121 | + ], |
| 122 | + }, |
| 123 | + { |
| 124 | + tag: 'RecordHighlights', |
| 125 | + schemaType: 'record:highlights', |
| 126 | + summary: 'Highlights panel — a strip of key fields. Config props from the spec RecordHighlights schema.', |
| 127 | + schema: RecordHighlightsProps, |
| 128 | + interactions: [ |
| 129 | + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'The record to summarize.' }, |
| 130 | + { name: 'objectName', type: 'string', kind: 'binding', description: 'The record’s object.' }, |
| 131 | + ], |
| 132 | + }, |
| 133 | + { |
| 134 | + tag: 'RecordRelatedList', |
| 135 | + schemaType: 'record:related_list', |
| 136 | + summary: 'Related child records via a lookup. Config props from the spec RecordRelatedList schema.', |
| 137 | + schema: RecordRelatedListProps, |
| 138 | + interactions: [ |
| 139 | + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'The parent record.' }, |
| 140 | + { name: 'objectName', type: 'string', kind: 'binding', description: 'The parent object.' }, |
| 141 | + ], |
| 142 | + }, |
| 143 | + { |
| 144 | + tag: 'RecordPath', |
| 145 | + schemaType: 'record:path', |
| 146 | + summary: 'Stage/progress bar driven by a status field. Config props from the spec RecordPath schema.', |
| 147 | + schema: RecordPathProps, |
| 148 | + interactions: [ |
| 149 | + { name: 'recordId', type: 'string | number', kind: 'controlled', description: 'The record whose stage to show.' }, |
| 150 | + { name: 'objectName', type: 'string', kind: 'binding', description: 'The record’s object.' }, |
| 151 | + ], |
| 152 | + }, |
| 153 | + { |
| 154 | + tag: 'Block', |
| 155 | + schemaType: '(any)', |
| 156 | + summary: 'Escape hatch — render any registered component by type. <Block type="object-kanban" objectName="task" /> etc.', |
| 157 | + interactions: [ |
| 158 | + { name: 'type', type: 'string', kind: 'binding', required: true, description: 'The registered component type to render.' }, |
| 159 | + ], |
| 160 | + }, |
| 161 | +]; |
0 commit comments