-
0b1326d: Documentation no longer teaches the "JSX/HTML + Tailwind" framing for a page's
source, which ADR-0080's own 2026-06-30 header amendment (under ADR-0065, Accepted) retracted. objectui#5461 corrected three sites; a multiline census found eight more, in three spellings a line-oriented grep could not see.A page's
sourceis runtime metadata. The console's Tailwind is compiled at build time by scanning the console's ownsrc, and there is no safelist, so it never sees your page: an authored utility class produces CSS only by coincidence (when objectui already ships that exact class) and otherwise produces nothing, with no error anywhere. That is the ADR-0065 "works only by coincidence" failure mode, and it is how a modal'sbg-black/50backdrop reached production fully transparent.os validatereports it aspage-source-className-tailwind, a warning on kindshtml,reactandjsx, shipped in@objectstack/lint@11.5.0.The tiers themselves are unchanged and every load-bearing claim survives — parse-never-execute, the untrusted-author safety argument for
html, and the deprecated'jsx'alias. Only the styling primitive is corrected, to the wordingcontent/docs/guide/react-pages.md§Styling already uses:kindStyle with "html"The blocks' own structured props ( `<flex direction gap>`,`<grid columns>`) plus a JSONstyleobject."react"Inline styleobjects.Colors on both tiers come from the theme as
hsl(var(--token)).Why each package has an entry — each was measured against its built artefact, not assumed:
@object-ui/react-runtime:README.mdis published to npm (npm includesREADME.mdin the tarball regardless offiles). Its "no sandbox" callout is the paragraph that routes untrusted-author work to thehtmltier, and it carried the retracted framing line-wrapped across:17-18. It also gains the §Styling section it was missing — the absence is why the framing survived here.@object-ui/sdui-parser: the corrected header ofsrc/types.tsprojects verbatim into the publisheddist/types.d.ts.@object-ui/components: the corrected header ofsrc/renderers/basic/html-elements.tsxprojects verbatim into the publisheddist/renderers/basic/html-elements.d.ts. Thekind === 'html'dispatch-arm comment insrc/renderers/layout/page.tsxdoes not project (it is inside a function body) and is included here only because the same package already owes an entry.
No behaviour change: this is prose only.
CHANGELOG.mdoccurrences are deliberately untouched — immutable release history.
-
d11996e: Give
@object-ui/react-runtime's React peer range an upper bound:peerDependencies.reactnarrows from>=18to^18.0.0 || ^19.0.0, the spelling the other 30 react peers in the fixed version group already declare (objectui#3741).An unbounded range is a promise that grows on its own.
>=18satisfies React 20 the day React 20 is published — a major this package has never been built against, let alone tested — and it makes that claim from an already-published manifest, with no commit and no review to point at. This package is the least appropriate place in the workspace for such a promise: it vendors react-runner and evaluates author-supplied JSX against the host's React, so a host on an untested major does not fail at this package's boundary, it fails somewhere inside the page it rendered.Nothing in the package wanted the wider range. Its entire React surface is
Component,createElement,isValidElement,ReactElementandReactNode, all unchanged in React 19 and none of them touching anything React 19 removed. The range was never a stated constraint either:>=18was written when the package was created on 2026-06-30 (d23d6ebfa, PR #2105) and no commit since revisited the line. The workspace pins React to19.2.8through the rootpnpm.overrides, so 19 is the only major this package's tests have ever exercised — the unbounded upper end was untested by construction.The README sentence restating the range moves with it, and the doc gate that compares the two (
doc-version-claims.test.ts, objectui#3717) keeps them in step from here.A new pin,
scripts/__tests__/react-peer-range-norm-3741.test.ts, now asserts the norm across every workspace manifest, because this was the third package born off-norm and the first two were each corrected in isolation:plugin-dashboardwas born narrow and fixed on 2026-05-08 (d2b6ecec6),plugin-reportwas born narrow and fixed in objectui#3690 (PR #3727) after a React 19 consumer hitERESOLVEon install, and this one was born unbounded. The existing doc gate could not have caught any of them — it checks a README against its own manifest, and react-runtime's two sides agreed with each other while both said>=18.
-
2374a49: fix(sdui): a react page no longer loses its state to a memo that never held, and a source that exports nothing fails loudly
Writing the regression guard for objectui#2954's "latent hazard" found it was already real.
evaluatedSchemawas memoised on values rebuilt every render.SchemaRendererfell back to a fresh{}when noSchemaRendererProvidersat above it, andusePageVariables()returned a brand-new object literal outside aPageVariablesProvider. Both feed theevaluatedSchemamemo's dependency list, so for any tree without those providers the memo never hit: the schema was re-cloned and the ExpressionEvaluator re-run on every render, and children got a new schema identity every time. Akind:'react'page memoises its compiled source on that identity, so the page was recompiled — a new page function, a new element type — and React remounted it, silently discarding the user'suseState. Any registry notification (every lazy plugin's first load) triggered it. Both fallbacks are now module constants.A source that exports nothing now throws instead of rendering blank.
generateElementinserts the implicitexport defaultonly when the source starts with JSX, afunctiondeclaration,()orclass— so the very commonconst Page = () => …exported nothing, and the page rendered blank with no error reported anywhere. It now throws with a message naming the fix, whichReactRunner's error panel surfaces.export default nullstill means "render nothing"; a default export that is not a component throws too.PageSchema['kind']matches@objectstack/spec. It declared'full' | 'slotted'while the renderer had shipped'react'and'html'/'jsx'since ADR-0080 and read the field through a cast. The union now spells all five and the cast is gone.Docs: new
content/docs/guide/react-pages.md(choosing between the executed and parsed tiers, the capability gate, the injected scope, flat props,Block,useAdapter, source shapes, error handling) and a@object-ui/react-runtimeREADME — the package had neither, while being the tier AI-authored pages target.
-
aa1240a: fix(sdui): lazily-registered public blocks reach a
kind:'react'page's scope, and ReactRunner keeps the errors it catchesTwo defects in the trusted
kind:'react'page tier.objectui#2953 — the contract skipped lazy blocks.
getPublicConfigs()resolved every curatedPUBLIC_BLOCKStag throughgetConfig(), which reads loaded registrations only, so a block registered withregisterLazy()was absent from the contract until its plugin chunk happened to be imported. Inapps/consolethat silently droppedobject-kanban,object-calendar,object-gantt,object-timeline,object-mapandmarkdownfrom every react page's scope — writing<ObjectKanban/>threwReferenceErroreven though the tag is a first-class contract member, and whether it threw depended on load order.getPublicConfigs()now resolves pending lazy stubs too, returning them withlazy: trueand nocomponent(newPublicComponentConfigtype); the injected wrapper renders throughSchemaRenderer, which triggers the loader and shows its placeholder.getConfig()stays loaded-only by design.objectui#2954 — ReactRunner discarded its own error state.
getDerivedStateFromPropsre-transpiled and re-evaluated the page source on every render and unconditionally seterror: null. React runs it before the re-render that followsgetDerivedStateFromError, so the boundary threw away the error it had just caught, rebuilt an identical throwing element, and the throw escaped past its ownfallbackto the renderer's generic panel;onErrorwas gated on state that had already been cleared and never fired for a compile-time error at all; and each compile minted a fresh page function — a new element type — that remounted the subtree and wiped the page'suseState. The transpile+eval is now memoised on(code, scope), errors persist until the inputs actually change, andonErrorreports each error exactly once.
-
d23d6eb: Three-tier AI page authoring:
kind:'html'and a trustedkind:'react'tier.@object-ui/react-runtime(new) — the trusted runtime-React tier forkind:'react'pages (vendored react-runner: Sucrase transpile + scope-eval, no sandbox). Renders real JSX/TSX (any HTML + JS + hooks/useState/map/onClick) in the main React tree with an injected scope (React, the public data blocks, page data) and a built-in error boundary.@object-ui/core— new runtime capability gate (enableCapability/disableCapability/isCapabilityEnabled,CAP_REACT_PAGES).react-pagesdefaults ON (the platform trusts reviewed, draft-gated authors); a deployment turns it OFF server-side (the runtime injects the disable global whenOS_DISABLE_REACT_PAGESis set). Never controlled from authored metadata.@object-ui/components— PageRenderer now routeskind:'react'(capability-gated, lazy-loads the runtime) and renderskind:'html'(the formerkind:'jsx', still accepted as a deprecated alias). Thehtmltier now resolves the full safe native HTML tag set (h1–h6, p, a, ul/ol/li, img, blockquote, pre, strong/em, …) so authored HTML lives up to its name.