Skip to content

Commit 5e9b1a0

Browse files
committed
chore(spec): regenerate the step-18 semantic region, the component reference page and the ui strictness counts (#21464, stage 3)
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1e956d6 commit 5e9b1a0

3 files changed

Lines changed: 76 additions & 9 deletions

File tree

‎content/docs/references/ui/component.mdx‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ Sort field and direction pair
539539
| **drawerWidth** | `string \| number` | optional | Drawer width (drawer) |
540540
| **modalSize** | `Enum<'sm' \| 'default' \| 'lg' \| 'xl' \| 'full'>` | optional | Modal size (modal) |
541541
| **modalCloseButton** | `boolean` | optional | Show the modal close button (modal) |
542-
| **contentLayout** | `any` | optional | Modal content layout config (modal) |
542+
| **contentLayout** | `Enum<'simple' \| 'tabbed'>` | optional | How the modal presentation lays out its sections (modal) — 'simple' stacks them (the default); 'tabbed' puts each section on its own tab once more than one section has a field to show |
543543
| **confirmOnDiscard** | `boolean` | optional | Confirm before discarding edits (drawer/modal) |
544544
| **submitText** | `string \| Record<string, string>` | optional | Submit button label |
545545
| **cancelText** | `string \| Record<string, string>` | optional | Cancel button label |
@@ -548,14 +548,32 @@ Sort field and direction pair
548548
| **showSubmit** | `boolean` | optional | Show the submit button |
549549
| **showCancel** | `boolean` | optional | Show the cancel button |
550550
| **showReset** | `boolean` | optional | Show the reset button |
551-
| **submitBehavior** | `any` | optional | What happens after a successful submit (`{ kind: 'thank-you' \| …, title?, message? }`) |
551+
| **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | What happens after a successful submit — the same block a form view's `submitBehavior` declares: `{ kind: 'thank-you', title?, message? }`, `{ kind: 'redirect', url, delayMs? }` (a relative `url`, interpolating declared record fields as `{{record.field_name}}`), `{ kind: 'continue' }` or `{ kind: 'next-record' }`. Takes precedence over `navigateOnSuccess` and `resetOnSuccess` |
552552
| **successMessage** | `string \| Record<string, string>` | optional | Toast message on successful submit |
553553
| **resetOnSuccess** | `boolean` | optional | Reset the form after a successful submit |
554-
| **navigateOnSuccess** | `any` | optional | Navigate after a successful submit |
554+
| **navigateOnSuccess** | `string` | optional | Relative path to navigate to after a successful create/update — `{id}` / `{recordId}` are replaced with the saved record's id, URL-escaped; an absolute URL is refused at submit and reported on the success toast. Ignored when `submitBehavior` is set — prefer `submitBehavior` |
555555
| **readOnly** | `boolean` | optional | Render every field read-only |
556556
| **initialValues** | `Record<string, any>` | optional | Prefill values (create mode) |
557557
| **initialData** | `Record<string, any>` | optional | Alternate spelling of `initialValues` the renderer also reads |
558-
| **mobile** | `any` | optional | Mobile presentation overrides |
558+
| **mobile** | `{ stickyActions?: boolean; stepper?: boolean \| 'auto'; stepperMinFields?: integer; stepperFieldsPerStep?: integer; … }` | optional | Phone presentation options, each opt-in — `{ stickyActions?, stepper?, stepperMinFields?, stepperFieldsPerStep?, fullscreenLongText? }`. Read by the flat (simple, sectionless) form |
559+
560+
### Nested Shape: `ObjectFormProps.submitBehavior[kind='redirect']`
561+
562+
| Property | Type | Required | Description |
563+
| :--- | :--- | :--- | :--- |
564+
| **kind** | `'redirect'` | ✅ | |
565+
| **url** | `string` | ✅ | Where the browser goes after a successful submit. Ruled 2026-08-11: (1) RELATIVE paths only — it must start with `/`, and absolute or protocol-relative URLs are refused, which is what closes the open-redirect face; (2) interpolation ONLY from declared record fields, spelled `{{record.field_name}}`, and every interpolated value is URL-escaped when the redirect is built; (3) a verbatim redirect on the resolved relative path is the intended consumption. To send the browser OUT of the app, use an app navigation item (`{ type: 'url', url }`) instead. |
566+
| **delayMs** | `integer` | optional | |
567+
568+
### Nested Shape: `ObjectFormProps.mobile`
569+
570+
| Property | Type | Required | Description |
571+
| :--- | :--- | :--- | :--- |
572+
| **stickyActions** | `boolean` | optional | Pin the submit / cancel bar to the bottom of a phone-width viewport |
573+
| **stepper** | `boolean \| 'auto'` | optional | One-step-at-a-time wizard for a flat form — `true` always, `'auto'` only on a phone-width viewport once the form has `stepperMinFields` fields, `false` (the default) never |
574+
| **stepperMinFields** | `integer` | optional | The field count at which `stepper: 'auto'` steps up (default 8) |
575+
| **stepperFieldsPerStep** | `integer` | optional | Fields shown per stepper step (default 1) |
576+
| **fullscreenLongText** | `boolean` | optional | Offer a fullscreen editor on textarea and rich-text fields |
559577

560578

561579
---

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The `strict` column is the one the campaign schedules against; it counts both th
2121

2222
| Dir | Sites | strict | passthrough | catchall | strip |
2323
|---|---|---|---|---|---|
24-
| `ui/` | 191 | 180 | 4 | 0 | 7 |
24+
| `ui/` | 192 | 181 | 4 | 0 | 7 |
2525

2626
## `ui/` — sites
2727

@@ -36,7 +36,7 @@ classify and is not listed (it becomes reportable the day it grows its first sit
3636
| `app.zod.ts` | 19 |
3737
| `bulk-action.zod.ts` | 4 |
3838
| `chart.zod.ts` | 8 |
39-
| `component.zod.ts` | 61 |
39+
| `component.zod.ts` | 62 |
4040
| `dashboard.zod.ts` | 11 |
4141
| `dataset.zod.ts` | 4 |
4242
| `i18n.zod.ts` | 1 |
@@ -46,23 +46,23 @@ classify and is not listed (it becomes reportable the day it grows its first sit
4646
| `sharing.zod.ts` | 1 |
4747
| `view.zod.ts` | 60 |
4848
| `widget.zod.ts` | 1 |
49-
| **total** | **191** |
49+
| **total** | **192** |
5050

5151
## `ui/` — open
5252

5353
Per file, how many of its sites still silently discard unknown keys. The `Class`
5454
column that decides the bucket split is hand-written in the ledger; the arithmetic
5555
over it is here.
5656

57-
**7 strip of 191**, in 4 file(s).
57+
**7 strip of 192**, in 4 file(s).
5858

5959
| File | Strip | Sites |
6060
|---|---|---|
6161
| `action-params.zod.ts` | 1 | 1 |
6262
| `app.zod.ts` | 1 | 19 |
6363
| `view.zod.ts` | 4 | 60 |
6464
| `widget.zod.ts` | 1 | 1 |
65-
| **total** | **7** | **191** |
65+
| **total** | **7** | **192** |
6666

6767
| Bucket | Sites |
6868
|---|---|

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19486,6 +19486,55 @@ const step18: MigrationStep = {
1948619486
+ 'group per value of that field, and a board that showed one swimlane shows one swimlane per value — '
1948719487
+ 'check that this is the grouping you meant.',
1948819488
},
19489+
// #21464 — four members of the `object-form` page block were `z.unknown()`
19490+
// although the form reads each with a fixed shape, so an off-shape value passed
19491+
// the component-props gate and the form fell back or ignored it in silence. The
19492+
// row now takes the form view's own `submitBehavior` by reference and the
19493+
// measured shape for `contentLayout`, `navigateOnSuccess` and `mobile`. The
19494+
// form's `fields` and `sections` and the master-detail form's two are held at
19495+
// `z.unknown()` (the form draws a `{ name }` field entry and an inline runtime
19496+
// field inside a section, which the typed shapes would refuse), and
19497+
// `customFields` waits for the spec to declare objectui's runtime form field.
19498+
// D3 only: page-component `properties` is not parsed on the metadata save or
19499+
// load path, so a stored page is never refused; an off-shape value has no
19500+
// rewrite that says what the author meant; and the authored census found no
19501+
// authored value to respell — the refused values are fixtures probing that the
19502+
// form refuses them.
19503+
{
19504+
id: 'ui-object-form-members-typed',
19505+
surface: 'page `object-form` components — `properties.contentLayout`, `.submitBehavior`, '
19506+
+ '`.navigateOnSuccess` and `.mobile` (which used to accept any value)',
19507+
replacement: 'the shape the form reads: `contentLayout` `\'simple\'` or `\'tabbed\'`; `submitBehavior` the '
19508+
+ 'form view\'s own block — `{ kind: \'thank-you\', title?, message? }`, `{ kind: \'redirect\', url, '
19509+
+ 'delayMs? }` with a relative `url`, `{ kind: \'continue\' }` or `{ kind: \'next-record\' }`; '
19510+
+ '`navigateOnSuccess` a relative path string; `mobile` `{ stickyActions?, stepper?, stepperMinFields?, '
19511+
+ 'stepperFieldsPerStep?, fullscreenLongText? }`, with `stepper` `true`, `false` or `\'auto\'` and the two '
19512+
+ 'counts positive integers. Write a `submitBehavior` `kind` as one of the four; move a `redirect` '
19513+
+ 'destination to a relative path; write `heading` as `title`.',
19514+
reason: 'The form reads these members with one shape, and the page-component row declared them '
19515+
+ '`z.unknown()`, so any value passed the component-props gate and the form answered an off-shape one '
19516+
+ 'with a silent default: a `submitBehavior` `kind` it does not know fell through to the thank-you panel; '
19517+
+ 'a misspelled `contentLayout` such as `\'tabs\'` stacked the sections; a `navigateOnSuccess` that is not a '
19518+
+ 'string threw after the record was written, so the submit reported a failure; and a `mobile` member it '
19519+
+ 'does not read, or a `stepper` outside `true` / `false` / `\'auto\'`, was ignored. The row now takes '
19520+
+ 'the form view\'s own `submitBehavior` by reference — the block the renderers already judge a redirect '
19521+
+ '`url` through — so one value is judged the same way on the form view and the block, and the measured '
19522+
+ 'shape for the other three. The form\'s `fields` and `sections` and the master-detail form\'s two stay '
19523+
+ 'open, because the form draws a `{ name }` field entry and an inline runtime field inside a section, '
19524+
+ 'which the typed shapes would refuse; and `customFields` stays open until the spec declares the '
19525+
+ 'runtime form field its entries are. It is read where every page component\'s props are: the '
19526+
+ 'component-props gate reports a refused value as an advisory `component-props-invalid` / '
19527+
+ '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and '
19528+
+ '`objectstack lint`, and a stored page still saves and loads, because a page component\'s '
19529+
+ '`properties` is not parsed on the metadata save or load path. No conversion is registered: nothing '
19530+
+ 'on the load path refuses the shape, and an off-shape value has no rewrite that both keeps what the '
19531+
+ 'form shows today and honours what the author wrote — which is the judgment this entry leaves to the '
19532+
+ 'upgrader. Deployed metadata NOT MEASURED.',
19533+
acceptanceCriteria: 'Every `object-form` node validates: `objectstack validate` reports no '
19534+
+ '`component-props-invalid` / `component-props-unknown-key` finding under the four members\' paths. '
19535+
+ 'Each form that set one of them now shows it: the post-submit behaviour it names, the modal\'s tabbed '
19536+
+ 'sections, the navigation after a save, and the phone presentation.',
19537+
},
1948919538
// #21229 — an `object-grid` page block's `exportOptions` was `z.unknown()`, so a
1949019539
// bare format array (the list view's legacy spelling, which the list view lifts
1949119540
// to `{ formats }`) was accepted on the grid, whose renderer reads

0 commit comments

Comments
 (0)