Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .changeset/11318-page-width-padding-refusal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@object-ui/types': minor
---

A `page` node refuses `maxWidth` and `padding` by name, and the layout guide teaches the
controls that work: `pageType` for the page's width, a `container` for a narrower column or
custom padding (objectui#11318).

**Breaking for a `page` node that carries `maxWidth` or `padding`.** Both keys are `container`
members. The page node never declared them, and `PageRenderer` never read them: it takes its
max-width class from `pageType`, and its wrapper padding is fixed. `BaseSchema`'s
`.passthrough()` kept the keys anyway, so a page carrying `maxWidth: 'lg'` or `padding: false`
parsed clean and rendered exactly as it would without them. The layout guide taught both, in
four snippets.

- `@object-ui/types`: `PageNodeSchema.maxWidth` and `PageNodeSchema.padding` are now `never` on
the TypeScript face, and the zod mirror refuses each key by name, at its own path
(`invalid_type`), with a message that names the door that works. For `maxWidth` that is
`pageType`, or a `container` in `children` with its own `maxWidth`. For `padding` it is a
`container` in `children` with its own `padding`. `safeValidateSchema` (what
`objectui validate` runs) gives the same refusal. So does the strict authoring face, which
refused both keys before only as an unnamed `unrecognized_keys`.
- Unchanged: `ContainerSchema.maxWidth` and `ContainerSchema.padding`, and every other key of
the page node, which stays open to unknown renderer props.

Migration: delete the key from the page node. To narrow the page, set `pageType` (`utility` is
the narrowest). To get a narrower column or your own padding, wrap the content in
`{ "type": "container", "maxWidth": "2xl", "padding": 8, "children": [ … ] }`.
96 changes: 70 additions & 26 deletions content/docs/guide/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,41 +220,65 @@ and `button.tsx`, which reads `schema.label`, renders a button with no text.

// Content
children: SchemaNode, // Main page content

// Layout options
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full',
padding?: boolean, // Add padding (default: true)


// Layout
pageType?: 'record' | 'home' | 'app' | 'utility' | 'list', // default 'record'; sets the content's max width
// NO `maxWidth` — refused by name (objectui#11318); use `pageType`, or a `container` in `children`
// NO `padding` — refused by name (objectui#11318); put a `container` with `padding` in `children`

// Styling
className?: string,
headerClassName?: string,
bodyClassName?: string
}
```

### Max Width Options
### Content Width

Control page content width:
A `page` node has no `maxWidth` key. **`pageType`** sets how wide the page's content may
grow: `utility` is the narrowest and suits settings and forms, `record` (the default),
`list` and `app` share a wider cap, and `home` is the widest. `getPageMaxWidth` in
`packages/components/src/renderers/layout/page.tsx` holds the mapping.

```json
{
"type": "page",
"pageType": "utility",
"title": "Settings",
"maxWidth": "lg",
"children": {
"type": "form",
"fields": []
}
}
```

Available values:
- `sm` - 640px
- `md` - 768px
- `lg` - 1024px
- `xl` - 1280px
- `2xl` - 1536px
- `full` - No maximum width (default)
`pageType` is more than a width. A `record` page leaves its heading to a `page:header`
block, so a `title` on a page with no `pageType` draws no heading. That is one more reason
the settings page above says `utility`.

For a column narrower than the page's cap, wrap the content in a `container` and set
**its** `maxWidth`. That key is a `container` member, and the
[Container reference](/docs/components/layout/container) lists its values:

```json
{
"type": "page",
"pageType": "utility",
"title": "Profile",
"children": {
"type": "container",
"maxWidth": "2xl",
"children": {
"type": "form",
"fields": []
}
}
}
```

> **⛔ `maxWidth` on a `page` node is refused by name** (objectui#11318). This page used to
> teach `"maxWidth": "lg"` beside `title`, with a list of sizes, and it changed **nothing**:
> `PageRenderer` has no read for the key and takes its width from `pageType`, while
> `BaseSchema`'s `.passthrough()` kept the value rather than refusing it. `PageNodeSchema`
> now declares the key as a refusal whose message names `pageType` and the `container`.

## Page Header (`page:header`)

Expand Down Expand Up @@ -495,11 +519,14 @@ Omit `sidebar` and the content fills the width under the top bar.

### Settings Page with Tabs

A settings page is a `utility` page, which gives it the narrowest content width (see
[Content Width](#content-width)):

```json
{
"type": "page",
"pageType": "utility",
"title": "Settings",
"maxWidth": "2xl",
"children": {
"type": "tabs",
"items": [
Expand Down Expand Up @@ -664,20 +691,31 @@ the sidebar are nodes you build, so style them where you build them, as above.

### Page Padding

Control page content padding:
A `page` node has no padding switch. Its wrapper always insets the content: `p-3`, then
`md:p-4`, then `lg:p-6`. The padding you control is a `container`'s. Its `padding` is a
number on the container's spacing scale, and `0` means none:

```json
{
"type": "page",
"padding": false,
"children": {
"type": "container",
"className": "p-8",
"maxWidth": false,
"padding": 8,
"children": []
}
}
```

`"maxWidth": false` keeps the container as wide as the page. Leave it out and the container
also caps its own width at its default, `xl`. The
[Container reference](/docs/components/layout/container) lists both scales.

> **⛔ `padding` on a `page` node is refused by name** (objectui#11318). This page used to
> teach `"padding": false` here, and it removed **nothing**: the page wrapper kept its inset,
> because `PageRenderer` has no read for the key and `BaseSchema`'s `.passthrough()` kept the
> value rather than refusing it. The refusal's message names the `container` and its `padding`.

## Best Practices

### 1. Consistent Structure
Expand Down Expand Up @@ -747,17 +785,23 @@ Place primary actions in the first `children` node, so they sit above the conten
are **action ids** resolved from the object's own actions metadata, not nodes
(see the [PageHeader reference](/docs/layout/page-header)).

### 4. Max Width for Forms
### 4. Constrained Width for Forms

Use constrained width for forms and reading content:
Give forms and reading content a narrow column: a `container` with its own `maxWidth`,
inside the page. ⛔ Not `maxWidth` on the `page` node, which is refused by name
(objectui#11318); the page's own cap comes from `pageType` (see [Content Width](#content-width)).

```json
{
"type": "page",
"maxWidth": "lg",
"pageType": "utility",
"children": {
"type": "form",
"fields": []
"type": "container",
"maxWidth": "2xl",
"children": {
"type": "form",
"fields": []
}
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@
* `### Props` table, the `<AppShell …>` opening tags in its fences, and whether
* any fence authors an `app-shell` JSON node. The same page's `page`,
* `page-header` and `sidebar-nav` sections are NOT audited here and must not be
* swept in — `page` in particular declares `headerClassName` / `bodyClassName`
* for a different component (`PageRenderer` in `@object-ui/components`), so a
* file-wide ban on those spellings would be wrong. `packages/layout/README.md`
* swept in — they describe different components with pins of their own (the
* `page` node's are the `page-*-refusal-*` files in `@object-ui/types`), so a
* file-wide ban on this file's spellings would judge material it does not own.
* (The `page` section's Schema API block used to declare `headerClassName` /
* `bodyClassName`; objectui#11318 removed them as unread.) `packages/layout/README.md`
* belongs to `readme-app-shell-example.test.ts` (objectui#4817) and
* `content/docs/layout/app-shell.mdx` to `app-shell-docs-nav-example.test.ts`
* (objectui#4793 / #4808); pinning the whole docs tree to source is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,19 @@ const CASES: Record<string, Case> = {
// exempted: a local REFUSAL, not a local capability.
// `../__tests__/page-breadcrumbs-refusal-8871.test.ts` owns the behaviour.
//
// ⚠️ These two are the ONLY members of this row that are refusals. Adding a
// third means a third undeclared key was found surviving `.passthrough()` on
// `maxWidth` and `padding` are the THIRD and FOURTH (objectui#11318, same
// gate, same helper). They are `container` members the layout guide taught
// on this node, and that census is the one this note asked for: no other
// `type: 'page'` object in the tree carried either key, and the node's open
// envelope stays pinned below, so the answer was two more named refusals and
// not a strict node. `../__tests__/page-width-padding-refusal-11318.test.ts`
// owns the behaviour.
//
// ⚠️ These four are the ONLY members of this row that are refusals. Adding a
// fifth means another undeclared key was found surviving `.passthrough()` on
// this node — which is the census that decides between one more named refusal
// and finally making the node strict. ⛔ Do not grow this list reflexively.
local: ['title', 'pageType', 'body', 'children', 'actions', 'breadcrumbs'],
local: ['title', 'pageType', 'body', 'children', 'actions', 'breadcrumbs', 'maxWidth', 'padding'],
},
App: {
spec: SpecAppSchema,
Expand Down
Loading
Loading