Skip to content

Commit 302bdab

Browse files
os-zhuangclaude
andauthored
feat(spec): JSX-source page kind + Command Center showcase (ADR-0080) (#2436)
* 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 <noreply@anthropic.com> * feat(app-showcase): Command Center — a kind:'jsx' page (ADR-0080) 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 <noreply@anthropic.com> * chore: changeset for ADR-0080 jsx page kind * 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 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 955573d commit 302bdab

5 files changed

Lines changed: 118 additions & 8 deletions

File tree

.changeset/sdui-jsx-page-spec.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
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).
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { definePage } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Command Center — a `kind:'jsx'` page (ADR-0080). The entire layout is
7+
* authored as a constrained JSX + Tailwind *string*; at save time
8+
* `@objectstack/sdui-parser` compiles it (parse, never execute) into the SDUI
9+
* tree, which the normal PageRenderer / SchemaRenderer renders. Every tag is a
10+
* real registered component — `flex`, `grid`, `card`, `text`, `badge`, `stack`.
11+
*
12+
* Demonstrates what the fixed page schema cannot: Tailwind-freeform layout that
13+
* still composes the platform's real components. Browser-verified.
14+
*/
15+
export const CommandCenterJsxPage = definePage({
16+
name: 'showcase_command_center_jsx',
17+
label: 'Command Center (JSX)',
18+
type: 'home',
19+
kind: 'jsx',
20+
source: `
21+
<flex direction="col" className="min-h-screen gap-10 bg-gradient-to-br from-slate-50 via-white to-indigo-50 p-10">
22+
23+
<flex direction="col" className="gap-3">
24+
<badge className="w-fit rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold uppercase tracking-widest text-indigo-700" label="Operations · JSX-source page" />
25+
<text className="block text-5xl font-bold tracking-tight text-slate-900" content="Command Center" />
26+
<text className="block max-w-2xl text-base leading-relaxed text-slate-500" content="This whole page is authored as constrained JSX + Tailwind and compiled to the SDUI tree — parsed, never executed. Every card is a real registered component." />
27+
</flex>
28+
29+
<grid columns={4} className="gap-5">
30+
<card className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
31+
<text className="block text-sm font-medium text-slate-500" content="Open Tasks" />
32+
<text className="mt-3 block text-4xl font-bold text-slate-900" content="128" />
33+
<text className="mt-2 block text-xs font-semibold text-emerald-600" content="▲ 12% vs last week" />
34+
</card>
35+
<card className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
36+
<text className="block text-sm font-medium text-slate-500" content="In Progress" />
37+
<text className="mt-3 block text-4xl font-bold text-slate-900" content="47" />
38+
<text className="mt-2 block text-xs font-semibold text-amber-600" content="● 9 due today" />
39+
</card>
40+
<card className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
41+
<text className="block text-sm font-medium text-slate-500" content="Completed" />
42+
<text className="mt-3 block text-4xl font-bold text-slate-900" content="1,902" />
43+
<text className="mt-2 block text-xs font-semibold text-emerald-600" content="▲ 4% this month" />
44+
</card>
45+
<card className="rounded-2xl border border-indigo-300 bg-gradient-to-br from-indigo-500 to-violet-600 p-6 shadow-md">
46+
<text className="block text-sm font-medium text-indigo-100" content="Cycle Time" />
47+
<text className="mt-3 block text-4xl font-bold text-white" content="2.4d" />
48+
<text className="mt-2 block text-xs font-semibold text-indigo-100" content="▼ 18% faster" />
49+
</card>
50+
</grid>
51+
52+
<grid columns={3} className="gap-5">
53+
<card className="col-span-2 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
54+
<text className="block text-lg font-semibold text-slate-900" content="Weekly Throughput" />
55+
<flex direction="row" className="mt-8 items-end gap-4">
56+
<flex className="h-12 w-full rounded-lg bg-indigo-400" />
57+
<flex className="h-20 w-full rounded-lg bg-indigo-500" />
58+
<flex className="h-16 w-full rounded-lg bg-indigo-400" />
59+
<flex className="h-28 w-full rounded-lg bg-violet-500" />
60+
<flex className="h-14 w-full rounded-lg bg-indigo-400" />
61+
<flex className="h-24 w-full rounded-lg bg-indigo-500" />
62+
<flex className="h-10 w-full rounded-lg bg-indigo-300" />
63+
</flex>
64+
</card>
65+
<card className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
66+
<text className="block text-lg font-semibold text-slate-900" content="Recent Activity" />
67+
<stack className="mt-4 gap-3">
68+
<flex direction="row" className="items-center gap-3"><flex className="h-2 w-2 rounded-full bg-emerald-500" /><text className="text-sm text-slate-600" content="Onboarding flow shipped" /></flex>
69+
<flex direction="row" className="items-center gap-3"><flex className="h-2 w-2 rounded-full bg-indigo-500" /><text className="text-sm text-slate-600" content="12 tasks moved to Review" /></flex>
70+
<flex direction="row" className="items-center gap-3"><flex className="h-2 w-2 rounded-full bg-amber-500" /><text className="text-sm text-slate-600" content="SLA breach on #4821" /></flex>
71+
<flex direction="row" className="items-center gap-3"><flex className="h-2 w-2 rounded-full bg-slate-300" /><text className="text-sm text-slate-600" content="Sprint 42 planning" /></flex>
72+
</stack>
73+
</card>
74+
</grid>
75+
</flex>`,
76+
});

examples/app-showcase/src/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export { MyWorkPage } from './my-work.page.js';
1515
export { SettingsPage } from './settings.page.js';
1616
export { StylingGalleryPage } from './styling-gallery.page.js';
1717
export { CommandCenterPage } from './command-center.page.js';
18+
export { CommandCenterJsxPage } from './command-center-jsx.page.js';
1819
export { PageVariablesPage } from './page-variables.page.js';
1920
export { ContactFormPage } from './contact-form.page.js';
2021
export {

packages/spec/liveness/page.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
"type": "page",
33
"_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.",
44
"props": {
5+
"source": {
6+
"status": "live",
7+
"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."
8+
},
9+
"requires": {
10+
"status": "planned",
11+
"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."
12+
},
513
"name": {
614
"status": "live",
715
"note": "page identity; objectui page renderer + runtime route/metadata key."

packages/spec/src/ui/page.zod.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,8 @@ export const PageSchema = lazySchema(() => z.object({
358358
*
359359
* Only meaningful when `type === 'record'`. Ignored otherwise.
360360
*/
361-
kind: z.enum(['full', 'slotted']).default('full')
362-
.describe('Page override mode: full (default) or slotted (partial overrides)'),
361+
kind: z.enum(['full', 'slotted', 'jsx']).default('full')
362+
.describe('Page override mode: full | slotted | jsx (ADR-0080 JSX-source authoring)'),
363363

364364
/**
365365
* Slot override map for slotted record pages.
@@ -384,13 +384,33 @@ export const PageSchema = lazySchema(() => z.object({
384384
tabs: z.union([PageComponentSchema, z.array(PageComponentSchema)]).optional(),
385385
discussion: z.union([PageComponentSchema, z.array(PageComponentSchema)]).optional(),
386386
}).optional().describe('Slot override map for slotted pages'),
387+
388+
/**
389+
* JSX-source authoring (ADR-0080). When `kind === 'jsx'`, `source` is the
390+
* source-of-truth: a constrained JSX/HTML+Tailwind text compiled by
391+
* `@objectstack/sdui-parser` into the SchemaNode tree at SAVE time — parse,
392+
* never execute. `regions` then hold the DERIVED tree (a cache; the source
393+
* wins on any mismatch). For `full`/`slotted` pages `source` is unused.
394+
*/
395+
source: z.string().optional()
396+
.describe("JSX-source page text — authoritative when kind==='jsx'; compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute)"),
397+
/** Plugin namespaces the JSX source references — inferred at compile, checked at save AND load (ADR-0048 provenance). */
398+
requires: z.array(z.string()).optional()
399+
.describe('Plugin namespaces the JSX source references (validated at save and load)'),
400+
}).superRefine((page, ctx) => {
401+
// ADR-0080 + ADR-0078 (completeness): a `kind:'jsx'` page with no `source`
402+
// is silently inert — fail loudly at author time, do not render an empty page.
403+
if (page.kind === 'jsx' && !(typeof page.source === 'string' && page.source.trim().length > 0)) {
404+
ctx.addIssue({
405+
code: 'custom',
406+
path: ['source'],
407+
message: "A jsx page requires a non-empty `source` (ADR-0080: JSX is the source-of-truth).",
408+
});
409+
}
387410
}));
388-
// PageSchema has no cross-field (`superRefine`) requirements by design. It once
389-
// required `recordReview`/`blankLayout` for the `record_review`/`blank` types
390-
// (both removed — unrendered roadmap, see PAGE_TYPE_ROADMAP) and `slots` for
391-
// `kind: 'slotted'` (dropped — an empty slotted page validly renders the
392-
// synthesized default). Each of those was a "required-but-unauthorable field
393-
// blocks the Studio create form" trap; none survives.
411+
// PageSchema's only cross-field rule is the ADR-0080 jsx-source completeness
412+
// check above. It once also required `recordReview`/`blankLayout` and `slots`
413+
// (all removed — unrendered roadmap / "required-but-unauthorable" Studio traps).
394414

395415
export type Page = z.infer<typeof PageSchema>;
396416
/** Authoring input for {@link Page} — defaulted fields are optional. */

0 commit comments

Comments
 (0)