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).
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 {
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."
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. */