From c295ab73d166dee0f50656f7b51d3f51da2f79d1 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 29 Jun 2026 00:59:19 +0800 Subject: [PATCH 1/4] feat(spec): JSX-source page kind (ADR-0080 M3a) PageSchema gains kind:'jsx' + source (the authoritative JSX text, compiled to the tree at save time) + requires (plugin provenance). A superRefine enforces ADR-0078 completeness: a jsx page with no source fails loudly at author time. full/slotted pages unchanged. Co-Authored-By: Claude Opus 4.8 --- packages/spec/src/ui/page.zod.ts | 36 +++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index e6a216ccab..1d63266b11 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -358,8 +358,8 @@ export const PageSchema = lazySchema(() => z.object({ * * Only meaningful when `type === 'record'`. Ignored otherwise. */ - kind: z.enum(['full', 'slotted']).default('full') - .describe('Page override mode: full (default) or slotted (partial overrides)'), + kind: z.enum(['full', 'slotted', 'jsx']).default('full') + .describe('Page override mode: full | slotted | jsx (ADR-0080 JSX-source authoring)'), /** * Slot override map for slotted record pages. @@ -384,13 +384,33 @@ export const PageSchema = lazySchema(() => z.object({ tabs: z.union([PageComponentSchema, z.array(PageComponentSchema)]).optional(), discussion: z.union([PageComponentSchema, z.array(PageComponentSchema)]).optional(), }).optional().describe('Slot override map for slotted pages'), + + /** + * JSX-source authoring (ADR-0080). When `kind === 'jsx'`, `source` is the + * source-of-truth: a constrained JSX/HTML+Tailwind text compiled by + * `@objectstack/sdui-parser` into the SchemaNode tree at SAVE time — parse, + * never execute. `regions` then hold the DERIVED tree (a cache; the source + * wins on any mismatch). For `full`/`slotted` pages `source` is unused. + */ + source: z.string().optional() + .describe("JSX-source page text — authoritative when kind==='jsx'; compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute)"), + /** Plugin namespaces the JSX source references — inferred at compile, checked at save AND load (ADR-0048 provenance). */ + requires: z.array(z.string()).optional() + .describe('Plugin namespaces the JSX source references (validated at save and load)'), +}).superRefine((page, ctx) => { + // ADR-0080 + ADR-0078 (completeness): a `kind:'jsx'` page with no `source` + // is silently inert — fail loudly at author time, do not render an empty page. + if (page.kind === 'jsx' && !(typeof page.source === 'string' && page.source.trim().length > 0)) { + ctx.addIssue({ + code: 'custom', + path: ['source'], + message: "A jsx page requires a non-empty `source` (ADR-0080: JSX is the source-of-truth).", + }); + } })); -// PageSchema has no cross-field (`superRefine`) requirements by design. It once -// required `recordReview`/`blankLayout` for the `record_review`/`blank` types -// (both removed — unrendered roadmap, see PAGE_TYPE_ROADMAP) and `slots` for -// `kind: 'slotted'` (dropped — an empty slotted page validly renders the -// synthesized default). Each of those was a "required-but-unauthorable field -// blocks the Studio create form" trap; none survives. +// PageSchema's only cross-field rule is the ADR-0080 jsx-source completeness +// check above. It once also required `recordReview`/`blankLayout` and `slots` +// (all removed — unrendered roadmap / "required-but-unauthorable" Studio traps). export type Page = z.infer; /** Authoring input for {@link Page} — defaulted fields are optional. */ From de335f9586c5b6cd00fa368cd94703ee52c8fdbb Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 29 Jun 2026 01:26:41 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(app-showcase):=20Command=20Center=20?= =?UTF-8?q?=E2=80=94=20a=20kind:'jsx'=20page=20(ADR-0080)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A polished home page authored entirely as constrained JSX + Tailwind source, compiled to the SDUI tree at save time and rendered by the normal PageRenderer. Browser-verified against the live registry (flex/grid/card/text/badge/stack). Co-Authored-By: Claude Opus 4.8 --- .../src/pages/command-center-jsx.page.ts | 76 +++++++++++++++++++ examples/app-showcase/src/pages/index.ts | 1 + 2 files changed, 77 insertions(+) create mode 100644 examples/app-showcase/src/pages/command-center-jsx.page.ts diff --git a/examples/app-showcase/src/pages/command-center-jsx.page.ts b/examples/app-showcase/src/pages/command-center-jsx.page.ts new file mode 100644 index 0000000000..950f0316f4 --- /dev/null +++ b/examples/app-showcase/src/pages/command-center-jsx.page.ts @@ -0,0 +1,76 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { definePage } from '@objectstack/spec/ui'; + +/** + * Command Center — a `kind:'jsx'` page (ADR-0080). The entire layout is + * authored as a constrained JSX + Tailwind *string*; at save time + * `@objectstack/sdui-parser` compiles it (parse, never execute) into the SDUI + * tree, which the normal PageRenderer / SchemaRenderer renders. Every tag is a + * real registered component — `flex`, `grid`, `card`, `text`, `badge`, `stack`. + * + * Demonstrates what the fixed page schema cannot: Tailwind-freeform layout that + * still composes the platform's real components. Browser-verified. + */ +export const CommandCenterJsxPage = definePage({ + name: 'showcase_command_center_jsx', + label: 'Command Center (JSX)', + type: 'home', + kind: 'jsx', + source: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`, +}); diff --git a/examples/app-showcase/src/pages/index.ts b/examples/app-showcase/src/pages/index.ts index 39afe4399c..96a2e874fa 100644 --- a/examples/app-showcase/src/pages/index.ts +++ b/examples/app-showcase/src/pages/index.ts @@ -15,6 +15,7 @@ export { MyWorkPage } from './my-work.page.js'; export { SettingsPage } from './settings.page.js'; export { StylingGalleryPage } from './styling-gallery.page.js'; export { CommandCenterPage } from './command-center.page.js'; +export { CommandCenterJsxPage } from './command-center-jsx.page.js'; export { PageVariablesPage } from './page-variables.page.js'; export { ContactFormPage } from './contact-form.page.js'; export { From 83bb345fd10f9e83c4a853e01b882bcc29474e01 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 29 Jun 2026 01:34:53 +0800 Subject: [PATCH 3/4] chore: changeset for ADR-0080 jsx page kind --- .changeset/sdui-jsx-page-spec.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sdui-jsx-page-spec.md diff --git a/.changeset/sdui-jsx-page-spec.md b/.changeset/sdui-jsx-page-spec.md new file mode 100644 index 0000000000..dfe992b209 --- /dev/null +++ b/.changeset/sdui-jsx-page-spec.md @@ -0,0 +1,5 @@ +--- +"@objectstack/spec": minor +--- + +ADR-0080: `PageSchema` gains `kind: 'jsx'` + `source` (the authoritative JSX text, compiled to the tree at save time) + `requires`, with a completeness `superRefine` — a jsx page with no source fails loudly (ADR-0078). From c1120cf67e620b6614f8db320fe3aa27511343d6 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 29 Jun 2026 01:52:15 +0800 Subject: [PATCH 4/4] chore(spec): classify ADR-0080 page props in liveness ledger source = live (objectui PageRenderer compiles it via @object-ui/sdui-parser, browser-verified); requires = planned (save/load enforcement is M3b). Co-Authored-By: Claude Opus 4.8 --- packages/spec/liveness/page.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/spec/liveness/page.json b/packages/spec/liveness/page.json index 4bc8886d11..4e9035ba6f 100644 --- a/packages/spec/liveness/page.json +++ b/packages/spec/liveness/page.json @@ -2,6 +2,14 @@ "type": "page", "_note": "PageSchema (UI). Renderers live in objectui, so consumers are cited as prose in `note` (not `evidence`, which resolves framework file:line). Seeded from the Studio page-design dogfood (framework#2254/#2261/#2265). Containers (variables/regions/interfaceConfig/slots/aria) classified at top level — one drill level, no divergent sub-statuses. The removed roadmap page types (record_review/blank) and their config fields (recordReview/blankLayout) were hard-removed — no longer authorable.", "props": { + "source": { + "status": "live", + "note": "JSX-source page authoring (ADR-0080). Consumer: objectui PageRenderer compiles `source` via @object-ui/sdui-parser into the SchemaNode tree (parse, never execute) and renders it — components/src/renderers/layout/page.tsx (kind:'jsx' branch). Browser-verified in the Command Center showcase." + }, + "requires": { + "status": "planned", + "note": "Plugin namespaces the JSX `source` references (ADR-0080). Inferred at compile time; save/load enforcement of plugin presence is deferred (M3b) — declared, not enforced yet." + }, "name": { "status": "live", "note": "page identity; objectui page renderer + runtime route/metadata key."