Skip to content

Commit d7e8f3e

Browse files
claude[bot]claude
andauthored
docs(react-pages): scope the react-only half of the page to the react tier (#13955)
`content/docs/ui/pages.mdx` routes BOTH source-authoring tiers to `content/docs/ui/react-pages.mdx` (links at :66, :116, :295). On that page only the first two sections are tier-neutral; everything from `## What is in scope` down was react-only material carrying no tier marking, so an `html`-tier reader arriving from any of those links read it as their own. That is the mechanism behind the naming trap closed earlier with one sentence; this closes the rest of the class the same way -- marking, not a split. Nine react-only sections were audited against source. Seven were actively misleading, and all seven are consequences of one fact the page already states twice up top: an `html` page's source is parsed, never executed. - the closure-scope table is the react runtime's injected scope; an html page has none (`parse.ts` never evaluates) - `on[A-Z]` attributes are a hard `forbidden-attr` on html, so the callback wiring has no counterpart; the `type` -> `specType` rescue is the react runtime's, and `object-chart` declares no `type` input in the manifest - `block` is not one of the 57 manifest keys, so `<Block>` is not a tag an html page may write - `useAdapter` and hooks exist only where the source runs - the accepted-source-shapes verdicts INVERT: the html grammar is `document := element`, so `function Page() {}` and `() => ...` fail `no-root` and the prescribed `export default Page;` fix is `multiple-roots` - html errors are save-time diagnostics, not a React error panel - the `record:*` withdrawal is react-only (`validateReactPageProps` skips every page whose `kind !== 'react'`), and `record:details` / `record:related_list` are registered html tags Two sections inside that run are both-tier and are marked as such rather than swept up: `## Styling`'s Tailwind rule and `## How you check your work`'s three commands. That is why a single marker at the top of the run would have been wrong. Each marker is one bold lead-in naming the tier and then the html counterpart -- the existing convention, with "On this tier" spelled as "On the `react` tier" so it cannot be read as either. The three bare occurrences already on the page were normalised to match; the page now contains none. The `record:*` heading is retitled with its explicit anchor preserved. `pages.mdx:295` no longer claims the page covers both tiers "in full" -- it never did, and the audit makes the gap explicit. Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC Co-authored-by: Claude <noreply@anthropic.com>
1 parent cf4cab5 commit d7e8f3e

3 files changed

Lines changed: 132 additions & 10 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
"@objectstack/docs": patch
3+
---
4+
5+
docs(react-pages): scope the react-only half of the page to the `react` tier (#13737)
6+
7+
`content/docs/ui/pages.mdx` routes **both** source-authoring tiers to
8+
`content/docs/ui/react-pages.mdx` — the links at `:66`, `:116` and `:295`, the
9+
last of which advertised the target as "The `html` and `react` source-authoring
10+
tiers in full". On that page only the first two sections were tier-neutral.
11+
Everything from `## What is in scope` down was react-only material carrying no
12+
tier marking, so an `html`-tier reader arriving from any of those links read it
13+
as their own.
14+
15+
That is the mechanism behind the naming trap #13734 closed with one sentence.
16+
This closes the rest of the class the same way — **marking, not a split**: no
17+
new page, no repointed links, no section moved between files.
18+
19+
Nine react-only sections were audited against source for the one question "is
20+
there a statement here an `html` author could act on and be wrong?". Seven were
21+
**actively misleading**, and all seven are consequences of the same fact the
22+
page already states twice up top — an `html` page's source is *parsed, never
23+
executed*:
24+
25+
- `## What is in scope` — the closure-scope table (`React`, `useAdapter`,
26+
`Block`, `data`/`variables`/`page`) is the react runtime's injected scope. An
27+
`html` page has no closure scope at all.
28+
- `## Blocks take flat props``parse.ts` refuses every `on[A-Z]` attribute
29+
(`forbidden-attr`), so the `onRowClick` callback wiring has no html
30+
counterpart; and the `type``specType` rescue is the react runtime's
31+
(`specType` occurs nowhere else in this repo). On html the parser builds
32+
`{ type: tag, ...props }`, so a `type` attribute overwrites the discriminator
33+
— and `object-chart` declares no `type` input in `sdui.manifest.json` anyway.
34+
- `### Block — the escape hatch``compile()` whitelists
35+
`Object.keys(manifest.components)`; `block` is not one of the 57 keys, so
36+
`<Block>` is not a tag an html page may write.
37+
- `## Live data``useAdapter` and hooks exist only where the source runs, and
38+
the sample is refused by the html grammar before that matters.
39+
- `## Accepted source shapes`**inverted**. The html grammar is
40+
`document := element`: `function Page() { … }` and `() => …` fail `no-root`,
41+
and the prescribed fix `export default Page;` is a second root
42+
(`multiple-roots`). An html author following the section verbatim writes
43+
source that cannot save.
44+
- `## When something throws` — describes a runtime that executes. An html
45+
page's errors are save-time diagnostics (`jsx-forbidden-tag`,
46+
`jsx-unknown-component`, `jsx-no-root`, …), not a React error panel.
47+
- `` ## `record:*` blocks are not in this tier ``**inverted, and the
48+
sharpest**: `validateReactPageProps` skips every page whose `kind !== 'react'`,
49+
and `record:details` / `record:related_list` are registered tags in the html
50+
manifest. The heading told html authors to stop using the blocks their tier
51+
composes record pages with. Retitled to name the tier (anchor
52+
`#record-blocks-not-in-react` preserved; the only inbound link is on the same
53+
page).
54+
55+
Two sections in the middle of that run are **both-tier** and are now marked as
56+
such rather than swept up: `## Styling`'s Tailwind rule (`page.zod.ts`: "Do not
57+
author Tailwind classes in page source in either tier") and `## How you check
58+
your work`'s three commands. This is why a single marker at the top of the run
59+
would have been wrong.
60+
61+
Every marker is one bold lead-in that names the tier and then names the html
62+
counterpart — #13734's own convention, with `On this tier` spelled as
63+
``On the `react` tier`` so it cannot be read as either tier. The three
64+
occurrences of the bare phrase already on the page were normalised to match, so
65+
the page now contains none.
66+
67+
`pages.mdx:295` no longer claims the page covers both tiers "in full" — it
68+
never did, and the audit makes the gap explicit. It now says what the page is:
69+
choosing between the tiers, plus the `react` tier's guide in full.

content/docs/ui/pages.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,5 +292,5 @@ const accountRecordPage = {
292292
- [View Metadata](/docs/ui/views) — List views and form views for record display
293293
- [Dashboard Metadata](/docs/ui/dashboards) — Analytics-focused page layout
294294
- [App Metadata](/docs/ui/apps) — Organize pages into applications
295-
- [React Pages](/docs/ui/react-pages)The `html` and `react` source-authoring tiers in full
295+
- [React Pages](/docs/ui/react-pages)choosing between the `html` and `react` source-authoring tiers, and the `react` tier's authoring guide in full
296296
- [Validating metadata](/docs/deployment/validating-metadata) — Every author-time rule a page is held to

content/docs/ui/react-pages.mdx

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ never executed in the first place, which is the whole point of the split.
7070

7171
## What is in scope
7272

73+
Everything from here down is the **`react` tier's** authoring guide unless a section says
74+
otherwise, and each section below opens by naming the tier it is for.
75+
7376
Nothing is imported. A react page's source is evaluated with a closure scope the runtime
7477
builds for it:
7578

@@ -81,7 +84,11 @@ builds for it:
8184
| `useAdapter` | The live data source: `find` / `findOne` / `create` / `update`. |
8285
| `data`, `variables`, `page` | The page's data, its `variables` map, and its own schema. |
8386

84-
**On this tier** blocks are referenced by the **PascalCase form of their registered
87+
**On the `react` tier only.** A `kind:'html'` page's source is parsed and never evaluated,
88+
so it has no closure scope at all and none of the names above exist there: its source is
89+
one JSX element tree, not an expression the runtime runs.
90+
91+
**On the `react` tier** blocks are referenced by the **PascalCase form of their registered
8592
type**: `object-form``<ObjectForm>`, `list-view``<ListView>`, `object-chart`
8693
`<ObjectChart>`. A `kind:'html'` page writes the registered name itself instead —
8794
`<object-form>`, `<list-view>`, `<object-chart>`.
@@ -101,6 +108,11 @@ place for the contract to rot.
101108

102109
## Blocks take flat props
103110

111+
**On the `react` tier.** A `kind:'html'` page writes flat props too, but only *values*:
112+
its parser rejects every `on…` handler outright — `Attribute "onRowClick" is not allowed
113+
on <list-view>` — so the callback wiring below has no html counterpart. Needing one block
114+
to drive another is itself a reason to reach for `react`.
115+
104116
An injected block folds its JSX props into the block's schema, so you write flat props
105117
rather than a nested `schema` object:
106118

@@ -118,10 +130,17 @@ another:
118130
One collision is worth knowing. `type` is the SDUI envelope's component discriminator
119131
**and** a legitimate prop name on some blocks — a chart's family, for instance. The
120132
discriminator wins the `type` slot and your value is preserved beside it as `specType`
121-
for the block to read, so `<ObjectChart type="bar">` works as written.
133+
for the block to read, so `<ObjectChart type="bar">` works as written. That rescue is the
134+
react runtime's. On an `html` page the tag name *is* the node's `type`, a `type` attribute
135+
overwrites it, and `<object-chart>` declares no `type` input to write in the first place.
122136

123137
### `Block` — the escape hatch
124138

139+
**On the `react` tier.** `Block` is a component the react scope injects, not a registered
140+
type, so it is not one of the tags an `html` page may write. An `html` page reaches the
141+
same components by writing the registered name directly —
142+
`<object-kanban objectName="showcase_task" />`.
143+
125144
Any registered component, including ones outside the curated contract:
126145

127146
```jsx
@@ -136,6 +155,12 @@ escape hatch — `<ListView viewType="kanban" …>` selects the visualization di
136155

137156
## Live data
138157

158+
**On the `react` tier.** `useAdapter` and React's hooks exist only where the source is
159+
executed, and the sample below is refused on an `html` page before any of that matters —
160+
it does not begin with an element. A `kind:'html'` page binds data declaratively instead:
161+
each block declares its own `objectName` and narrows it with that block's own `filter`
162+
input.
163+
139164
`useAdapter()` returns the same data source the rest of the app queries through. Query
140165
options are OData-shaped — `$filter`, `$top`, `$skip`, `$select`, `$orderby`, `$search`:
141166

@@ -174,6 +199,10 @@ compounds spelled `['and', [...], [...]]`.
174199
175200
## Styling — a page's source is metadata, not source code [#styling]
176201
202+
**Both tiers.** The rule in the callout holds for `html` and `react` alike. The two
203+
remedies after it are the react tier's; an `html` page styles with its components'
204+
structured props plus a JSON `style` object carrying the same theme tokens.
205+
177206
<Callout type="warn">
178207
**Do not write Tailwind utility classes in page source.** A page's `source` is *runtime
179208
metadata*. The console's Tailwind is JIT-compiled at **build** time by scanning the
@@ -182,7 +211,7 @@ name in page source produces CSS only if that exact class happens to appear some
182211
the console's own source, and otherwise **produces nothing, with no error anywhere**.
183212
</Callout>
184213
185-
This is the single most expensive mistake on this tier, because the failure mode is a
214+
This is the single most expensive mistake on either tier, because the failure mode is a
186215
page that renders — correct structure, correct data, no styling — and reports nothing.
187216
It is recorded as an amendment to ADR-0080 under
188217
[ADR-0065](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0065-sdui-styling-model.md):
@@ -237,6 +266,11 @@ styling; you only style the layout around them.
237266
238267
## Accepted source shapes
239268
269+
**On the `react` tier.** None of these shapes carries over. An `html` page's source is a
270+
single root element and nothing else, so `function Page() { … }`, `() =>` and a
271+
trailing `export default Page;` are each refused at save time — `Expected a single root
272+
element` for the first two, `A page must have exactly one root element` for the export.
273+
240274
The page renders the source's **default export**. The runtime inserts an implicit
241275
`export default` when the source *starts with* JSX, a `function` declaration, `()`, or
242276
`class`:
@@ -255,16 +289,25 @@ throws with a message naming the fix.
255289
256290
## When something throws
257291
292+
**On the `react` tier**, because the source is executed. A `kind:'html'` page never gets
293+
that far: its source is parsed against the block manifest when you save it, and the same
294+
three commands below report the parse and manifest diagnostics instead —
295+
`jsx-forbidden-tag`, `jsx-forbidden-attr`, `jsx-unknown-component`, `jsx-no-root` and the
296+
rest.
297+
258298
Transpile errors, evaluation errors and errors thrown during render all surface in a
259299
**React page error** panel carrying the message. The error is held until the source or
260300
its data changes, so it neither flickers nor escapes into the generic renderer error.
261301
262302
Referencing an identifier that is not in scope is the common case, and reads as
263-
`ReferenceError: <Name> is not defined` — usually a layout container (there are none on
264-
this tier — use HTML) or a block outside the public registry (use `<Block>`).
303+
`ReferenceError: <Name> is not defined` — usually a layout container (the `react` tier
304+
injects none — use HTML) or a block outside the public registry (use `<Block>`).
265305
266306
## Page state
267307
308+
**On the `react` tier.** A `kind:'html'` page holds no state — its source is compiled once
309+
to the SDUI tree — so interactivity that needs state is itself a reason to choose `react`.
310+
268311
A react page keeps its own `React.useState` across re-renders and across lazily loaded
269312
plugin chunks. The parent record on a react page is not a framework concept — it is
270313
ordinary React state that you pass to blocks as props.
@@ -276,7 +319,14 @@ blocks inside it, so a host that constructs an adapter inline on every render re
276319
every react page on every render. Hosts should provide the adapter from state or a
277320
module constant.
278321
279-
## `record:*` blocks are not in this tier [#record-blocks-not-in-react]
322+
## `record:*` blocks are not on the `react` tier [#record-blocks-not-in-react]
323+
324+
**On the `react` tier only.** The withdrawal below is this one tier's: `os validate`
325+
applies the rule to `kind:'react'` pages and to nothing else. Everywhere else the
326+
`record:*` family is the normal way to compose a record surface — `<record:details>` and
327+
`<record:related_list>` are registered tags an `html` page may write like any other. Put
328+
them on a `type:'record'` page, which is what mounts the context they read; see
329+
[Page Metadata](/docs/ui/pages#components).
280330
281331
`<RecordDetails>`, `<RecordHighlights>`, `<RecordRelatedList>`, `<RecordPath>` and the
282332
rest of the `record:*` family are **record-page composition blocks**. Each one reads its
@@ -303,6 +353,9 @@ To use the family itself, author the page as `type:'record'` instead.
303353
304354
## How you check your work
305355
356+
**Both tiers** go through the same three commands. A `kind:'html'` page's source is parsed
357+
against the block manifest; the rule names quoted below are the `react` tier's.
358+
306359
Every `kind:'react'` page is parsed and checked at author time. `os validate`,
307360
`os lint` and `os build` all run the same rule set, so what one accepts the others do
308361
too:
@@ -348,9 +401,9 @@ rule set.
348401
349402
## A complete page
350403
351-
A master/detail console: a filtered list on the left drives a summary, a chart and a
352-
related list on the right, with edits in a drawer. Every binding is an ordinary prop —
353-
there is no record context involved.
404+
A `kind:'react'` master/detail console: a filtered list on the left drives a summary, a
405+
chart and a related list on the right, with edits in a drawer. Every binding is an
406+
ordinary prop — there is no record context involved.
354407
355408
{/* os:check */}
356409
```typescript

0 commit comments

Comments
 (0)